OLD | NEW |
(Empty) | |
| 1 // Copyright 2013 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 "components/autofill/ios/browser/js_suggestion_manager.h" |
| 6 |
| 7 #import <Foundation/Foundation.h> |
| 8 |
| 9 #include "base/strings/sys_string_conversions.h" |
| 10 #import "base/test/ios/wait_util.h" |
| 11 #include "ios/chrome/browser/web/chrome_web_test.h" |
| 12 #import "ios/web/public/test/js_test_util.h" |
| 13 #import "ios/web/public/test/web_js_test.h" |
| 14 #import "ios/web/public/web_state/js/crw_js_injection_receiver.h" |
| 15 #import "ios/web/public/web_state/web_state.h" |
| 16 #import "testing/gtest_mac.h" |
| 17 |
| 18 #if !defined(__has_feature) || !__has_feature(objc_arc) |
| 19 #error "This file requires ARC support." |
| 20 #endif |
| 21 |
| 22 namespace { |
| 23 |
| 24 // Test fixture to test suggestions. |
| 25 class JsSuggestionManagerTest : public web::WebJsTest<ChromeWebTest> { |
| 26 protected: |
| 27 JsSuggestionManagerTest() |
| 28 : web::WebJsTest<ChromeWebTest>(@[ @"suggestion_controller" ]) {} |
| 29 // Loads the given HTML and initializes the Autofill JS scripts. |
| 30 void LoadHtml(NSString* html); |
| 31 // Helper method that initializes a form with three fields. Can be used to |
| 32 // test whether adding an attribute on the second field causes it to be |
| 33 // skipped (or not, as is appropriate) by selectNextElement. |
| 34 void SequentialNavigationSkipCheck(NSString* attribute, BOOL shouldSkip); |
| 35 // Returns the active element name from the JS side. |
| 36 NSString* GetActiveElementName() { |
| 37 return ExecuteJavaScript(@"document.activeElement.name"); |
| 38 } |
| 39 JsSuggestionManager* manager_; |
| 40 }; |
| 41 |
| 42 void JsSuggestionManagerTest::LoadHtml(NSString* html) { |
| 43 WebJsTest<ChromeWebTest>::LoadHtml(html); |
| 44 manager_ = |
| 45 static_cast<JsSuggestionManager*>([web_state()->GetJSInjectionReceiver() |
| 46 instanceOfClass:[JsSuggestionManager class]]); |
| 47 [manager_ inject]; |
| 48 } |
| 49 |
| 50 TEST_F(JsSuggestionManagerTest, InitAndInject) { |
| 51 LoadHtml(@"<html></html>"); |
| 52 EXPECT_TRUE([manager_ hasBeenInjected]); |
| 53 } |
| 54 |
| 55 TEST_F(JsSuggestionManagerTest, SelectElementInTabOrder) { |
| 56 NSString* htmlFragment = |
| 57 @"<html> <body>" |
| 58 "<input id='1 (0)' tabIndex=1 href='http://www.w3schools.com'>1 (0)</a>" |
| 59 "<input id='0 (0)' tabIndex=0 href='http://www.w3schools.com'>0 (0)</a>" |
| 60 "<input id='2' tabIndex=2 href='http://www.w3schools.com'>2</a>" |
| 61 "<input id='0 (1)' tabIndex=0 href='http://www.w3schools.com'>0 (1)</a>" |
| 62 "<input id='-2' tabIndex=-2 href='http://www.w3schools.com'>-2</a>" |
| 63 "<a href='http://www.w3schools.com'></a>" |
| 64 "<input id='-1 (0)' tabIndex=-1 href='http://www.w3schools.com'>-1</a>" |
| 65 "<input id='-2 (2)' tabIndex=-2 href='http://www.w3schools.com'>-2</a>" |
| 66 "<input id='0 (2)' tabIndex=0 href='http://www.w3schools.com'>0 - 2</a>" |
| 67 "<input id='3' tabIndex=3 href='http://www.w3schools.com'>3</a>" |
| 68 "<input id='1 (1)' tabIndex=1 href='http://www.w3schools.com'>1 (1)</a>" |
| 69 "<input id='-1 (1)' tabIndex=-1 href='http://www.w3schools.com'>-1 </a>" |
| 70 "<input id='0 (3)' tabIndex=0 href='http://www.w3schools.com'>0 (3)</a>" |
| 71 "</body></html>"; |
| 72 LoadHtmlAndInject(htmlFragment); |
| 73 |
| 74 // clang-format off |
| 75 NSDictionary* next_expected_ids = @ { |
| 76 @"1 (0)" : @"1 (1)", |
| 77 @"0 (0)" : @"0 (1)", |
| 78 @"2" : @"3", |
| 79 @"0 (1)" : @"0 (2)", |
| 80 @"-2" : @"0 (2)", |
| 81 @"-1 (0)" : @"0 (2)", |
| 82 @"-2 (2)" : @"0 (2)", |
| 83 @"0 (2)" : @"0 (3)", |
| 84 @"3" : @"0 (0)", |
| 85 @"1 (1)" : @"2", |
| 86 @"-1 (1)" : @"0 (3)", |
| 87 @"0 (3)" : @"null" |
| 88 }; |
| 89 // clang-format on |
| 90 |
| 91 for (NSString* element_id : next_expected_ids) { |
| 92 NSString* expected_id = [next_expected_ids objectForKey:element_id]; |
| 93 EXPECT_NSEQ(expected_id, |
| 94 ExecuteJavaScriptWithFormat( |
| 95 @"var elements=document.getElementsByTagName('input');" |
| 96 "var element=document.getElementById('%@');" |
| 97 "var next = __gCrWeb.suggestion.getNextElementInTabOrder(" |
| 98 " element, elements);" |
| 99 "next ? next.id : 'null';", |
| 100 element_id)) |
| 101 << "Wrong when selecting next element of element with element id " |
| 102 << base::SysNSStringToUTF8(element_id); |
| 103 } |
| 104 EXPECT_NSEQ(@YES, |
| 105 ExecuteJavaScriptWithFormat( |
| 106 @"var elements=document.getElementsByTagName('input');" |
| 107 "var element=document.getElementsByTagName('a')[0];" |
| 108 "var next = __gCrWeb.suggestion.getNextElementInTabOrder(" |
| 109 " element, elements); next===null")) |
| 110 << "Wrong when selecting the next element of an element not in the " |
| 111 << "element list."; |
| 112 |
| 113 for (NSString* element_id : next_expected_ids) { |
| 114 NSString* expected_id = [next_expected_ids objectForKey:element_id]; |
| 115 if ([expected_id isEqualToString:@"null"]) { |
| 116 // If the expected next element is null, the focus is not moved. |
| 117 expected_id = element_id; |
| 118 } |
| 119 EXPECT_NSEQ(expected_id, ExecuteJavaScriptWithFormat( |
| 120 @"document.getElementById('%@').focus();" |
| 121 "__gCrWeb.suggestion.selectNextElement();" |
| 122 "document.activeElement.id", |
| 123 element_id)) |
| 124 << "Wrong when selecting next element with active element " |
| 125 << base::SysNSStringToUTF8(element_id); |
| 126 } |
| 127 |
| 128 for (NSString* element_id : next_expected_ids) { |
| 129 // If the expected next element is null, there is no next element. |
| 130 BOOL expected = ![next_expected_ids[element_id] isEqualToString:@"null"]; |
| 131 EXPECT_NSEQ(@(expected), ExecuteJavaScriptWithFormat( |
| 132 @"document.getElementById('%@').focus();" |
| 133 "__gCrWeb.suggestion.hasNextElement()", |
| 134 element_id)) |
| 135 << "Wrong when checking hasNextElement() for " |
| 136 << base::SysNSStringToUTF8(element_id); |
| 137 } |
| 138 |
| 139 // clang-format off |
| 140 NSDictionary* prev_expected_ids = @{ |
| 141 @"1 (0)" : @"null", |
| 142 @"0 (0)" : @"3", |
| 143 @"2" : @"1 (1)", |
| 144 @"0 (1)" : @"0 (0)", |
| 145 @"-2" : @"0 (1)", |
| 146 @"-1 (0)": @"0 (1)", |
| 147 @"-2 (2)": @"0 (1)", |
| 148 @"0 (2)" : @"0 (1)", |
| 149 @"3" : @"2", |
| 150 @"1 (1)" : @"1 (0)", |
| 151 @"-1 (1)": @"1 (1)", |
| 152 @"0 (3)" : @"0 (2)", |
| 153 }; |
| 154 // clang-format on |
| 155 |
| 156 for (NSString* element_id : prev_expected_ids) { |
| 157 NSString* expected_id = [prev_expected_ids objectForKey:element_id]; |
| 158 EXPECT_NSEQ( |
| 159 expected_id, |
| 160 ExecuteJavaScriptWithFormat( |
| 161 @"var elements=document.getElementsByTagName('input');" |
| 162 "var element=document.getElementById('%@');" |
| 163 "var prev = __gCrWeb.suggestion.getPreviousElementInTabOrder(" |
| 164 " element, elements);" |
| 165 "prev ? prev.id : 'null';", |
| 166 element_id)) |
| 167 << "Wrong when selecting prev element of element with element id " |
| 168 << base::SysNSStringToUTF8(element_id); |
| 169 } |
| 170 EXPECT_NSEQ( |
| 171 @YES, ExecuteJavaScriptWithFormat( |
| 172 @"var elements=document.getElementsByTagName('input');" |
| 173 "var element=document.getElementsByTagName('a')[0];" |
| 174 "var prev = __gCrWeb.suggestion.getPreviousElementInTabOrder(" |
| 175 " element, elements); prev===null")) |
| 176 << "Wrong when selecting the previous element of an element not in the " |
| 177 << "element list"; |
| 178 |
| 179 for (NSString* element_id : prev_expected_ids) { |
| 180 NSString* expected_id = [prev_expected_ids objectForKey:element_id]; |
| 181 if ([expected_id isEqualToString:@"null"]) { |
| 182 // If the expected previous element is null, the focus is not moved. |
| 183 expected_id = element_id; |
| 184 } |
| 185 EXPECT_NSEQ(expected_id, ExecuteJavaScriptWithFormat( |
| 186 @"document.getElementById('%@').focus();" |
| 187 "__gCrWeb.suggestion.selectPreviousElement();" |
| 188 "document.activeElement.id", |
| 189 element_id)) |
| 190 << "Wrong when selecting previous element with active element " |
| 191 << base::SysNSStringToUTF8(element_id); |
| 192 } |
| 193 |
| 194 for (NSString* element_id : prev_expected_ids) { |
| 195 // If the expected next element is null, there is no next element. |
| 196 BOOL expected = ![prev_expected_ids[element_id] isEqualToString:@"null"]; |
| 197 EXPECT_NSEQ(@(expected), ExecuteJavaScriptWithFormat( |
| 198 @"document.getElementById('%@').focus();" |
| 199 "__gCrWeb.suggestion.hasPreviousElement()", |
| 200 element_id)) |
| 201 << "Wrong when checking hasPreviousElement() for " |
| 202 << base::SysNSStringToUTF8(element_id); |
| 203 } |
| 204 } |
| 205 |
| 206 TEST_F(JsSuggestionManagerTest, SequentialNavigation) { |
| 207 LoadHtml(@"<html><body><form name='testform' method='post'>" |
| 208 "<input type='text' name='firstname'/>" |
| 209 "<input type='text' name='lastname'/>" |
| 210 "<input type='email' name='email'/>" |
| 211 "</form></body></html>"); |
| 212 |
| 213 [manager_ |
| 214 executeJavaScript:@"document.getElementsByName('firstname')[0].focus()" |
| 215 completionHandler:nil]; |
| 216 [manager_ selectNextElement]; |
| 217 EXPECT_NSEQ(@"lastname", GetActiveElementName()); |
| 218 __block BOOL block_was_called = NO; |
| 219 [manager_ fetchPreviousAndNextElementsPresenceWithCompletionHandler:^void( |
| 220 BOOL has_previous_element, BOOL has_next_element) { |
| 221 block_was_called = YES; |
| 222 EXPECT_TRUE(has_previous_element); |
| 223 EXPECT_TRUE(has_next_element); |
| 224 }]; |
| 225 base::test::ios::WaitUntilCondition(^bool() { |
| 226 return block_was_called; |
| 227 }); |
| 228 [manager_ selectNextElement]; |
| 229 EXPECT_NSEQ(@"email", GetActiveElementName()); |
| 230 [manager_ selectPreviousElement]; |
| 231 EXPECT_NSEQ(@"lastname", GetActiveElementName()); |
| 232 } |
| 233 |
| 234 void JsSuggestionManagerTest::SequentialNavigationSkipCheck(NSString* attribute, |
| 235 BOOL shouldSkip) { |
| 236 LoadHtml([NSString stringWithFormat:@"<html><body>" |
| 237 "<form name='testform' method='post'>" |
| 238 "<input type='text' name='firstname'/>" |
| 239 "<%@ name='middlename'/>" |
| 240 "<input type='text' name='lastname'/>" |
| 241 "</form></body></html>", |
| 242 attribute]); |
| 243 [manager_ |
| 244 executeJavaScript:@"document.getElementsByName('firstname')[0].focus()" |
| 245 completionHandler:nil]; |
| 246 NSString* const kActiveElementNameJS = @"document.activeElement.name"; |
| 247 EXPECT_NSEQ(@"firstname", |
| 248 web::ExecuteJavaScript(manager_, kActiveElementNameJS)); |
| 249 [manager_ selectNextElement]; |
| 250 NSString* activeElementNameJS = GetActiveElementName(); |
| 251 if (shouldSkip) |
| 252 EXPECT_NSEQ(@"lastname", activeElementNameJS); |
| 253 else |
| 254 EXPECT_NSEQ(@"middlename", activeElementNameJS); |
| 255 } |
| 256 |
| 257 TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipText) { |
| 258 SequentialNavigationSkipCheck(@"input type='text'", NO); |
| 259 } |
| 260 |
| 261 TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipTextArea) { |
| 262 SequentialNavigationSkipCheck(@"input type='textarea'", NO); |
| 263 } |
| 264 |
| 265 TEST_F(JsSuggestionManagerTest, SequentialNavigationOverInvisibleElement) { |
| 266 SequentialNavigationSkipCheck(@"input type='text' style='display:none'", YES); |
| 267 } |
| 268 |
| 269 TEST_F(JsSuggestionManagerTest, SequentialNavigationOverHiddenElement) { |
| 270 SequentialNavigationSkipCheck(@"input type='text' style='visibility:hidden'", |
| 271 YES); |
| 272 } |
| 273 |
| 274 TEST_F(JsSuggestionManagerTest, SequentialNavigationOverDisabledElement) { |
| 275 SequentialNavigationSkipCheck(@"type='text' disabled", YES); |
| 276 } |
| 277 |
| 278 TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipPassword) { |
| 279 SequentialNavigationSkipCheck(@"input type='password'", NO); |
| 280 } |
| 281 |
| 282 TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipSubmit) { |
| 283 SequentialNavigationSkipCheck(@"input type='submit'", YES); |
| 284 } |
| 285 |
| 286 TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipImage) { |
| 287 SequentialNavigationSkipCheck(@"input type='image'", YES); |
| 288 } |
| 289 |
| 290 TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipButton) { |
| 291 SequentialNavigationSkipCheck(@"input type='button'", YES); |
| 292 } |
| 293 |
| 294 TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipRange) { |
| 295 SequentialNavigationSkipCheck(@"input type='range'", YES); |
| 296 } |
| 297 |
| 298 TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipRadio) { |
| 299 SequentialNavigationSkipCheck(@"type='radio'", YES); |
| 300 } |
| 301 |
| 302 TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipCheckbox) { |
| 303 SequentialNavigationSkipCheck(@"type='checkbox'", YES); |
| 304 } |
| 305 |
| 306 // Special test for a condition where the closeKeyboard script would cause an |
| 307 // illegal JS recursion if a blur event results in an event that triggers a |
| 308 // crwebinvoke:// back, such as a page change. |
| 309 TEST_F(JsSuggestionManagerTest, CloseKeyboardSafetyTest) { |
| 310 LoadHtml(@"<select id='select'>Select</select>"); |
| 311 ExecuteJavaScript( |
| 312 @"select.onblur = function(){window.location.href = '#test'}"); |
| 313 ExecuteJavaScript(@"select.focus()"); |
| 314 // In the failure condition the app will crash during the next line. |
| 315 [manager_ closeKeyboard]; |
| 316 // TODO(crbug.com/661624): add a check for the keyboard actually being |
| 317 // dismissed; unfortunately it is not known how to adapt |
| 318 // WaitForBackgroundTasks to yield for events wrapped with window.setTimeout() |
| 319 // or other deferred events. |
| 320 } |
| 321 |
| 322 // Test fixture to test |
| 323 // |fetchPreviousAndNextElementsPresenceWithCompletionHandler|. |
| 324 class FetchPreviousAndNextExceptionTest : public JsSuggestionManagerTest { |
| 325 public: |
| 326 void SetUp() override { |
| 327 JsSuggestionManagerTest::SetUp(); |
| 328 LoadHtml(@"<html></html>"); |
| 329 } |
| 330 |
| 331 protected: |
| 332 // Evaluates JS and tests that the completion handler passed to |
| 333 // |fetchPreviousAndNextElementsPresenceWithCompletionHandler| is called with |
| 334 // (NO, NO) indicating no previous and next element. |
| 335 void EvaluateJavaScriptAndExpectNoPreviousAndNextElement(NSString* js) { |
| 336 ExecuteJavaScript(js); |
| 337 __block BOOL block_was_called = NO; |
| 338 id completionHandler = ^(BOOL hasPreviousElement, BOOL hasNextElement) { |
| 339 EXPECT_FALSE(hasPreviousElement); |
| 340 EXPECT_FALSE(hasNextElement); |
| 341 block_was_called = YES; |
| 342 }; |
| 343 [manager_ fetchPreviousAndNextElementsPresenceWithCompletionHandler: |
| 344 completionHandler]; |
| 345 base::test::ios::WaitUntilCondition(^bool() { |
| 346 return block_was_called; |
| 347 }); |
| 348 } |
| 349 }; |
| 350 |
| 351 // Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works |
| 352 // when |__gCrWeb.suggestion.hasPreviousElement| throws an exception. |
| 353 TEST_F(FetchPreviousAndNextExceptionTest, HasPreviousElementException) { |
| 354 EvaluateJavaScriptAndExpectNoPreviousAndNextElement( |
| 355 @"__gCrWeb.suggestion.hasPreviousElement = function() { bar.foo1; }"); |
| 356 } |
| 357 |
| 358 // Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works |
| 359 // when |__gCrWeb.suggestion.hasNextElement| throws an exception. |
| 360 TEST_F(FetchPreviousAndNextExceptionTest, HasNextElementException) { |
| 361 EvaluateJavaScriptAndExpectNoPreviousAndNextElement( |
| 362 @"__gCrWeb.suggestion.hasNextElement = function() { bar.foo1; }"); |
| 363 } |
| 364 |
| 365 // Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works |
| 366 // when |Array.toString| has been overridden to return a malformed string |
| 367 // without a ",". |
| 368 TEST_F(FetchPreviousAndNextExceptionTest, HasPreviousElementNull) { |
| 369 EvaluateJavaScriptAndExpectNoPreviousAndNextElement( |
| 370 @"Array.prototype.toString = function() { return 'Hello'; }"); |
| 371 } |
| 372 |
| 373 } // namespace |
OLD | NEW |