OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/passwords/password_generation_agent.h" |
| 6 |
| 7 #include <algorithm> |
| 8 #include <memory> |
| 9 |
| 10 #include "base/logging.h" |
| 11 #include "base/mac/foundation_util.h" |
| 12 #import "base/mac/scoped_nsobject.h" |
| 13 #include "base/mac/scoped_objc_class_swizzler.h" |
| 14 #include "base/macros.h" |
| 15 #include "base/strings/string16.h" |
| 16 #include "base/strings/sys_string_conversions.h" |
| 17 #include "base/strings/utf_string_conversions.h" |
| 18 #include "components/autofill/core/common/form_data.h" |
| 19 #include "components/autofill/core/common/form_field_data.h" |
| 20 #include "components/autofill/core/common/password_form.h" |
| 21 #import "components/autofill/ios/browser/js_suggestion_manager.h" |
| 22 #include "google_apis/gaia/gaia_urls.h" |
| 23 #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" |
| 24 #import "ios/chrome/browser/passwords/js_password_manager.h" |
| 25 #import "ios/chrome/browser/passwords/password_generation_offer_view.h" |
| 26 #import "ios/chrome/browser/passwords/passwords_ui_delegate.h" |
| 27 #import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
| 28 #include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
| 29 #import "ios/testing/ocmock_complex_type_helper.h" |
| 30 #include "ios/web/public/test/test_web_state.h" |
| 31 #include "ios/web/public/web_state/url_verification_constants.h" |
| 32 #import "ios/web/public/test/web_test_with_web_state.h" |
| 33 #include "testing/gtest_mac.h" |
| 34 #include "third_party/ocmock/OCMock/OCMock.h" |
| 35 #include "third_party/ocmock/gtest_support.h" |
| 36 #include "url/gurl.h" |
| 37 |
| 38 namespace { |
| 39 |
| 40 NSString* const kAccountCreationFormName = @"create-foo-account"; |
| 41 NSString* const kAccountCreationFieldName = @"password"; |
| 42 NSString* const kAccountCreationOrigin = @"http://foo.com/login"; |
| 43 NSString* const kEmailFieldName = @"email"; |
| 44 |
| 45 // Static storage to access arguments passed to swizzled method of UIWindow. |
| 46 static id g_chrome_execute_command_sender = nil; |
| 47 |
| 48 } // namespace |
| 49 |
| 50 @interface MockPasswordsUiDelegate : NSObject<PasswordsUiDelegate> |
| 51 |
| 52 - (instancetype)init; |
| 53 |
| 54 @property(nonatomic, readonly) BOOL UIShown; |
| 55 |
| 56 @end |
| 57 |
| 58 @implementation MockPasswordsUiDelegate { |
| 59 // YES if showGenerationAlertWithPassword was called more recently than |
| 60 // hideGenerationAlert, NO otherwise. |
| 61 BOOL _UIShown; |
| 62 } |
| 63 |
| 64 - (instancetype)init { |
| 65 self = [super init]; |
| 66 if (self) { |
| 67 _UIShown = NO; |
| 68 } |
| 69 return self; |
| 70 } |
| 71 |
| 72 @synthesize UIShown = _UIShown; |
| 73 |
| 74 - (void)showGenerationAlertWithPassword:(NSString*)password |
| 75 andPromptDelegate: |
| 76 (id<PasswordGenerationPromptDelegate>)delegate { |
| 77 _UIShown = YES; |
| 78 } |
| 79 |
| 80 - (void)hideGenerationAlert { |
| 81 _UIShown = NO; |
| 82 } |
| 83 |
| 84 @end |
| 85 |
| 86 // A donor class that provides a chromeExecuteCommand method that can be |
| 87 // swapped with UIWindow. |
| 88 @interface DonorWindow : NSObject |
| 89 |
| 90 - (void)chromeExecuteCommand:(id)sender; |
| 91 |
| 92 @end |
| 93 |
| 94 @implementation DonorWindow |
| 95 |
| 96 - (void)chromeExecuteCommand:(id)sender { |
| 97 g_chrome_execute_command_sender = [sender retain]; |
| 98 } |
| 99 |
| 100 @end |
| 101 |
| 102 namespace { |
| 103 |
| 104 // A helper to swizzle chromeExecuteCommand method on UIWindow. |
| 105 class ScopedWindowSwizzler { |
| 106 public: |
| 107 ScopedWindowSwizzler() |
| 108 : class_swizzler_([UIWindow class], |
| 109 [DonorWindow class], |
| 110 @selector(chromeExecuteCommand:)) { |
| 111 DCHECK(!g_chrome_execute_command_sender); |
| 112 } |
| 113 |
| 114 ~ScopedWindowSwizzler() { |
| 115 [g_chrome_execute_command_sender release]; |
| 116 g_chrome_execute_command_sender = nil; |
| 117 } |
| 118 |
| 119 private: |
| 120 base::mac::ScopedObjCClassSwizzler class_swizzler_; |
| 121 |
| 122 DISALLOW_COPY_AND_ASSIGN(ScopedWindowSwizzler); |
| 123 }; |
| 124 |
| 125 // Returns a form that should be marked as an account creation form by local |
| 126 // heuristics. |
| 127 autofill::PasswordForm GetAccountCreationForm() { |
| 128 autofill::FormFieldData name_field; |
| 129 name_field.name = base::ASCIIToUTF16("name"); |
| 130 name_field.form_control_type = "text"; |
| 131 |
| 132 autofill::FormFieldData email_field; |
| 133 email_field.name = base::ASCIIToUTF16("email"); |
| 134 email_field.form_control_type = "email"; |
| 135 |
| 136 autofill::FormFieldData password_field; |
| 137 password_field.name = base::SysNSStringToUTF16(kAccountCreationFieldName); |
| 138 password_field.form_control_type = "password"; |
| 139 |
| 140 autofill::FormFieldData confirmPasswordField; |
| 141 confirmPasswordField.name = base::ASCIIToUTF16("confirm"); |
| 142 confirmPasswordField.form_control_type = "password"; |
| 143 |
| 144 autofill::FormData form; |
| 145 form.name = base::SysNSStringToUTF16(kAccountCreationFormName); |
| 146 form.origin = GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)); |
| 147 form.action = GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)); |
| 148 |
| 149 form.fields.push_back(name_field); |
| 150 form.fields.push_back(email_field); |
| 151 form.fields.push_back(password_field); |
| 152 form.fields.push_back(confirmPasswordField); |
| 153 |
| 154 autofill::PasswordForm password_form; |
| 155 password_form.origin = form.origin; |
| 156 password_form.username_element = email_field.name; |
| 157 password_form.password_element = password_field.name; |
| 158 |
| 159 password_form.form_data = form; |
| 160 |
| 161 return password_form; |
| 162 } |
| 163 |
| 164 // Executes each block in |blocks|, where each block must have the type |
| 165 // void^(void). |
| 166 void ExecuteBlocks(NSArray* blocks) { |
| 167 for (void (^block)(void) in blocks) |
| 168 block(); |
| 169 } |
| 170 |
| 171 // Returns a form that has the same origin as GAIA. |
| 172 autofill::PasswordForm GetGAIAForm() { |
| 173 autofill::PasswordForm form(GetAccountCreationForm()); |
| 174 form.origin = GaiaUrls::GetInstance()->gaia_login_form_realm(); |
| 175 form.signon_realm = form.origin.GetOrigin().spec(); |
| 176 return form; |
| 177 } |
| 178 |
| 179 // Returns a form with no text fields. |
| 180 autofill::PasswordForm GetFormWithNoTextFields() { |
| 181 autofill::PasswordForm form(GetAccountCreationForm()); |
| 182 form.form_data.fields.clear(); |
| 183 return form; |
| 184 } |
| 185 |
| 186 // Returns true if |field| has type "password" and false otherwise. |
| 187 bool IsPasswordField(const autofill::FormFieldData& field) { |
| 188 return field.form_control_type == "password"; |
| 189 } |
| 190 |
| 191 // Returns all password fields in |form|. |
| 192 std::vector<autofill::FormFieldData> GetPasswordFields( |
| 193 const autofill::PasswordForm& form) { |
| 194 std::vector<autofill::FormFieldData> fields; |
| 195 fields.reserve(form.form_data.fields.size()); |
| 196 for (const auto& field : form.form_data.fields) { |
| 197 if (IsPasswordField(field)) |
| 198 fields.push_back(field); |
| 199 } |
| 200 return fields; |
| 201 } |
| 202 |
| 203 // Returns a form with no password fields. |
| 204 autofill::PasswordForm GetFormWithNoPasswordFields() { |
| 205 autofill::PasswordForm form(GetAccountCreationForm()); |
| 206 form.form_data.fields.erase( |
| 207 std::remove_if(form.form_data.fields.begin(), form.form_data.fields.end(), |
| 208 &IsPasswordField), |
| 209 form.form_data.fields.end()); |
| 210 return form; |
| 211 } |
| 212 |
| 213 // Test fixture for testing PasswordGenerationAgent. |
| 214 class PasswordGenerationAgentTest : public web::WebTestWithWebState { |
| 215 public: |
| 216 void SetUp() override { |
| 217 web::WebTestWithWebState::SetUp(); |
| 218 mock_js_suggestion_manager_.reset( |
| 219 [[OCMockObject niceMockForClass:[JsSuggestionManager class]] retain]); |
| 220 mock_js_password_manager_.reset( |
| 221 [[OCMockObject niceMockForClass:[JsPasswordManager class]] retain]); |
| 222 mock_ui_delegate_.reset([[MockPasswordsUiDelegate alloc] init]); |
| 223 test_web_state_.reset(new web::TestWebState); |
| 224 agent_.reset([[PasswordGenerationAgent alloc] |
| 225 initWithWebState:test_web_state_.get() |
| 226 passwordManager:nullptr |
| 227 passwordManagerDriver:nullptr |
| 228 JSPasswordManager:mock_js_password_manager_ |
| 229 JSSuggestionManager:mock_js_suggestion_manager_ |
| 230 passwordsUiDelegate:mock_ui_delegate_]); |
| 231 @autoreleasepool { |
| 232 accessory_view_controller_.reset([[FormInputAccessoryViewController alloc] |
| 233 initWithWebState:test_web_state_.get() |
| 234 providers:@[ agent_ ]]); |
| 235 } |
| 236 } |
| 237 |
| 238 // Sends form data, autofill data, and password manager data to the |
| 239 // generation agent so that it can find an account creation form and password |
| 240 // field. |
| 241 void LoadAccountCreationForm() { |
| 242 autofill::PasswordForm password_form(GetAccountCreationForm()); |
| 243 [agent() allowPasswordGenerationForForm:password_form]; |
| 244 std::vector<autofill::PasswordForm> password_forms; |
| 245 password_forms.push_back(password_form); |
| 246 [agent() processParsedPasswordForms:password_forms]; |
| 247 SetCurrentURLAndTrustLevel( |
| 248 GURL(base::SysNSStringToUTF8(kAccountCreationOrigin)), |
| 249 web::URLVerificationTrustLevel::kAbsolute); |
| 250 SetContentIsHTML(YES); |
| 251 } |
| 252 |
| 253 // Sets up the web controller mock to use the specified URL and trust level. |
| 254 void SetCurrentURLAndTrustLevel( |
| 255 GURL url, |
| 256 web::URLVerificationTrustLevel url_trust_level) { |
| 257 test_web_state_->SetCurrentURL(url); |
| 258 test_web_state_->SetTrustLevel(url_trust_level); |
| 259 } |
| 260 |
| 261 // Swizzles the current web controller to set whether the content is HTML. |
| 262 void SetContentIsHTML(BOOL content_is_html) { |
| 263 test_web_state_->SetContentIsHTML(content_is_html); |
| 264 } |
| 265 |
| 266 // Simulates an event on the specified form/field. |
| 267 void SimulateFormActivity(NSString* form_name, |
| 268 NSString* field_name, |
| 269 NSString* type) { |
| 270 [accessory_view_controller_ webState:test_web_state_.get() |
| 271 didRegisterFormActivityWithFormNamed:base::SysNSStringToUTF8(form_name) |
| 272 fieldName:base::SysNSStringToUTF8(field_name) |
| 273 type:base::SysNSStringToUTF8(type) |
| 274 value:"" |
| 275 keyCode:web::WebStateObserver:: |
| 276 kInvalidFormKeyCode |
| 277 inputMissing:false]; |
| 278 } |
| 279 |
| 280 // Returns a mock of JsSuggestionManager. |
| 281 id mock_js_suggestion_manager() { return mock_js_suggestion_manager_; } |
| 282 |
| 283 // Returns a mock of JsPasswordManager. |
| 284 id mock_js_password_manager() { return mock_js_password_manager_; } |
| 285 |
| 286 MockPasswordsUiDelegate* mock_ui_delegate() { return mock_ui_delegate_; } |
| 287 |
| 288 protected: |
| 289 // Returns the current generation agent. |
| 290 PasswordGenerationAgent* agent() { return agent_.get(); } |
| 291 |
| 292 // Returns the current accessory view controller. |
| 293 FormInputAccessoryViewController* accessory_controller() { |
| 294 return accessory_view_controller_.get(); |
| 295 } |
| 296 |
| 297 private: |
| 298 // Test WebState. |
| 299 std::unique_ptr<web::TestWebState> test_web_state_; |
| 300 |
| 301 // Mock for JsSuggestionManager; |
| 302 base::scoped_nsobject<id> mock_js_suggestion_manager_; |
| 303 |
| 304 // Mock for JsPasswordManager. |
| 305 base::scoped_nsobject<id> mock_js_password_manager_; |
| 306 |
| 307 // Mock for the UI delegate. |
| 308 base::scoped_nsobject<MockPasswordsUiDelegate> mock_ui_delegate_; |
| 309 |
| 310 // Controller that shows custom input accessory views. |
| 311 base::scoped_nsobject<FormInputAccessoryViewController> |
| 312 accessory_view_controller_; |
| 313 |
| 314 // The current generation agent. |
| 315 base::scoped_nsobject<PasswordGenerationAgent> agent_; |
| 316 }; |
| 317 |
| 318 // Tests that local heuristics skip forms with GAIA realm. |
| 319 TEST_F(PasswordGenerationAgentTest, |
| 320 OnParsedForms_ShouldIgnoreFormsWithGaiaRealm) { |
| 321 // Send only a form with GAIA origin to the agent. |
| 322 std::vector<autofill::PasswordForm> forms; |
| 323 forms.push_back(GetGAIAForm()); |
| 324 [agent() processParsedPasswordForms:forms]; |
| 325 |
| 326 // No account creation form should have been found. |
| 327 EXPECT_FALSE(agent().possibleAccountCreationForm); |
| 328 EXPECT_TRUE(agent().passwordFields.empty()); |
| 329 } |
| 330 |
| 331 // Tests that local heuristics skip forms with no text fields. |
| 332 TEST_F(PasswordGenerationAgentTest, |
| 333 OnParsedForms_ShouldIgnoreFormsWithNotEnoughTextFields) { |
| 334 // Send only a form with GAIA origin to the agent. |
| 335 std::vector<autofill::PasswordForm> forms; |
| 336 forms.push_back(GetFormWithNoTextFields()); |
| 337 [agent() processParsedPasswordForms:forms]; |
| 338 |
| 339 // No account creation form should have been found. |
| 340 EXPECT_FALSE(agent().possibleAccountCreationForm); |
| 341 EXPECT_TRUE(agent().passwordFields.empty()); |
| 342 } |
| 343 |
| 344 // Tests that local heuristics skip forms with no password fields. |
| 345 TEST_F(PasswordGenerationAgentTest, |
| 346 OnParsedForms_ShouldIgnoreFormsWithNoPasswordFields) { |
| 347 // Send only a form with GAIA origin to the agent. |
| 348 std::vector<autofill::PasswordForm> forms; |
| 349 forms.push_back(GetFormWithNoPasswordFields()); |
| 350 [agent() processParsedPasswordForms:forms]; |
| 351 |
| 352 // No account creation form should have been found. |
| 353 EXPECT_FALSE(agent().possibleAccountCreationForm); |
| 354 EXPECT_TRUE(agent().passwordFields.empty()); |
| 355 } |
| 356 |
| 357 // Tests that local heuristics extract an account creation form from the page |
| 358 // when one exists, along with its password fields. |
| 359 TEST_F(PasswordGenerationAgentTest, OnParsedForms) { |
| 360 // Send several forms. One should be selected. |
| 361 std::vector<autofill::PasswordForm> forms; |
| 362 forms.push_back(GetGAIAForm()); |
| 363 forms.push_back(GetFormWithNoTextFields()); |
| 364 forms.push_back(GetFormWithNoPasswordFields()); |
| 365 forms.push_back(GetAccountCreationForm()); |
| 366 [agent() processParsedPasswordForms:forms]; |
| 367 |
| 368 // Should have found an account creation form and extracted its password |
| 369 // fields. |
| 370 EXPECT_EQ(forms[3], *agent().possibleAccountCreationForm); |
| 371 std::vector<autofill::FormFieldData> expectedPasswordFields( |
| 372 GetPasswordFields(forms[3])); |
| 373 EXPECT_EQ(expectedPasswordFields.size(), agent().passwordFields.size()); |
| 374 for (size_t i = 0; i < expectedPasswordFields.size(); ++i) { |
| 375 EXPECT_FORM_FIELD_DATA_EQUALS(expectedPasswordFields[i], |
| 376 agent().passwordFields[i]); |
| 377 } |
| 378 } |
| 379 |
| 380 // Tests that password generation field identification waits until it has |
| 381 // approval from autofill and the password manager and an account creation |
| 382 // form has been identified with local heuristics.. |
| 383 TEST_F(PasswordGenerationAgentTest, DeterminePasswordGenerationField) { |
| 384 std::vector<autofill::PasswordForm> forms; |
| 385 forms.push_back(GetAccountCreationForm()); |
| 386 |
| 387 autofill::PasswordForm form(GetAccountCreationForm()); |
| 388 std::vector<autofill::FormFieldData> passwordFields(GetPasswordFields(form)); |
| 389 |
| 390 // The signals can be received in any order, so test them accordingly by |
| 391 // breaking the steps into blocks and executing them in different orders. |
| 392 id sendForms = ^{ |
| 393 std::vector<autofill::PasswordForm> forms; |
| 394 forms.push_back(form); |
| 395 [agent() processParsedPasswordForms:forms]; |
| 396 }; |
| 397 id sendPasswordManagerWhitelist = ^{ |
| 398 [agent() allowPasswordGenerationForForm:form]; |
| 399 }; |
| 400 id expectFieldNotFound = ^{ |
| 401 EXPECT_FALSE(agent().passwordGenerationField); |
| 402 }; |
| 403 id expectFieldFound = ^{ |
| 404 // When there are multiple password fields in the account creation form, |
| 405 // the first one is used as the generation field. |
| 406 EXPECT_FORM_FIELD_DATA_EQUALS(passwordFields[0], |
| 407 (*agent().passwordGenerationField)); |
| 408 }; |
| 409 |
| 410 // For each permutation of steps, the field should only be set after the third |
| 411 // signal is received. |
| 412 @autoreleasepool { |
| 413 ExecuteBlocks(@[ |
| 414 sendForms, expectFieldNotFound, sendPasswordManagerWhitelist, |
| 415 expectFieldFound |
| 416 ]); |
| 417 [agent() clearState]; |
| 418 |
| 419 ExecuteBlocks(@[ |
| 420 sendPasswordManagerWhitelist, expectFieldNotFound, sendForms, |
| 421 expectFieldFound |
| 422 ]); |
| 423 [agent() clearState]; |
| 424 } |
| 425 } |
| 426 |
| 427 // Tests that the password generation UI is shown when the user focuses the |
| 428 // password field in the account creation form. |
| 429 TEST_F(PasswordGenerationAgentTest, |
| 430 ShouldStartGenerationWhenPasswordFieldFocused) { |
| 431 LoadAccountCreationForm(); |
| 432 id mock = [OCMockObject partialMockForObject:accessory_controller()]; |
| 433 [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; |
| 434 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
| 435 @"focus"); |
| 436 |
| 437 EXPECT_OCMOCK_VERIFY(mock); |
| 438 [mock stop]; |
| 439 } |
| 440 |
| 441 // Tests that requesting password generation shows the alert UI. |
| 442 TEST_F(PasswordGenerationAgentTest, ShouldShowAlertWhenGenerationRequested) { |
| 443 LoadAccountCreationForm(); |
| 444 id mock = [OCMockObject partialMockForObject:accessory_controller()]; |
| 445 [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; |
| 446 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
| 447 @"focus"); |
| 448 EXPECT_EQ(NO, mock_ui_delegate().UIShown); |
| 449 |
| 450 [agent() generatePassword]; |
| 451 EXPECT_EQ(YES, mock_ui_delegate().UIShown); |
| 452 |
| 453 EXPECT_OCMOCK_VERIFY(mock); |
| 454 [mock stop]; |
| 455 } |
| 456 |
| 457 // Tests that the password generation UI is hidden when the user changes focus |
| 458 // from the password field. |
| 459 TEST_F(PasswordGenerationAgentTest, |
| 460 ShouldStopGenerationWhenDifferentFieldFocused) { |
| 461 LoadAccountCreationForm(); |
| 462 id mock = [OCMockObject partialMockForObject:accessory_controller()]; |
| 463 [[mock expect] showCustomInputAccessoryView:[OCMArg any]]; |
| 464 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
| 465 @"focus"); |
| 466 |
| 467 [[mock expect] restoreDefaultInputAccessoryView]; |
| 468 SimulateFormActivity(kAccountCreationFormName, kEmailFieldName, @"focus"); |
| 469 |
| 470 EXPECT_OCMOCK_VERIFY(mock); |
| 471 [mock stop]; |
| 472 } |
| 473 |
| 474 // Tests that the password field is filled when the user accepts a generated |
| 475 // password. |
| 476 TEST_F(PasswordGenerationAgentTest, |
| 477 ShouldFillPasswordFieldAndDismissAlertWhenUserAcceptsGeneratedPassword) { |
| 478 LoadAccountCreationForm(); |
| 479 // Focus the password field to start generation. |
| 480 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
| 481 @"focus"); |
| 482 NSString* password = @"abc"; |
| 483 |
| 484 [[[mock_js_password_manager() stub] andDo:^(NSInvocation* invocation) { |
| 485 void (^completion_handler)(BOOL); |
| 486 [invocation getArgument:&completion_handler atIndex:4]; |
| 487 completion_handler(YES); |
| 488 }] fillPasswordForm:kAccountCreationFormName |
| 489 withGeneratedPassword:password |
| 490 completionHandler:[OCMArg any]]; |
| 491 |
| 492 [agent() generatePassword]; |
| 493 EXPECT_EQ(YES, mock_ui_delegate().UIShown); |
| 494 |
| 495 [agent() acceptPasswordGeneration:nil]; |
| 496 EXPECT_EQ(NO, mock_ui_delegate().UIShown); |
| 497 EXPECT_OCMOCK_VERIFY(mock_js_password_manager()); |
| 498 } |
| 499 |
| 500 // Tests that the Save Passwords setting screen is shown when the user taps |
| 501 // "show saved passwords". |
| 502 TEST_F(PasswordGenerationAgentTest, |
| 503 ShouldShowPasswordsAndDismissAlertWhenUserTapsShow) { |
| 504 ScopedWindowSwizzler swizzler; |
| 505 LoadAccountCreationForm(); |
| 506 // Focus the password field to start generation. |
| 507 SimulateFormActivity(kAccountCreationFormName, kAccountCreationFieldName, |
| 508 @"focus"); |
| 509 [agent() generatePassword]; |
| 510 EXPECT_EQ(YES, mock_ui_delegate().UIShown); |
| 511 |
| 512 [agent() showSavedPasswords:nil]; |
| 513 EXPECT_EQ(NO, mock_ui_delegate().UIShown); |
| 514 |
| 515 GenericChromeCommand* command = base::mac::ObjCCast<GenericChromeCommand>( |
| 516 g_chrome_execute_command_sender); |
| 517 EXPECT_TRUE(command); |
| 518 EXPECT_EQ(IDC_SHOW_SAVE_PASSWORDS_SETTINGS, command.tag); |
| 519 } |
| 520 |
| 521 } // namespace |
OLD | NEW |