| Index: ios/chrome/browser/autofill/js_suggestion_manager_unittest.mm
|
| diff --git a/ios/chrome/browser/autofill/js_suggestion_manager_unittest.mm b/ios/chrome/browser/autofill/js_suggestion_manager_unittest.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..7f9c20ce3322763faa8f8d83a9c9638a43760a19
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/autofill/js_suggestion_manager_unittest.mm
|
| @@ -0,0 +1,373 @@
|
| +// Copyright 2013 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +#import "components/autofill/ios/browser/js_suggestion_manager.h"
|
| +
|
| +#import <Foundation/Foundation.h>
|
| +
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#import "base/test/ios/wait_util.h"
|
| +#include "ios/chrome/browser/web/chrome_web_test.h"
|
| +#import "ios/web/public/test/js_test_util.h"
|
| +#import "ios/web/public/test/web_js_test.h"
|
| +#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
|
| +#import "ios/web/public/web_state/web_state.h"
|
| +#import "testing/gtest_mac.h"
|
| +
|
| +#if !defined(__has_feature) || !__has_feature(objc_arc)
|
| +#error "This file requires ARC support."
|
| +#endif
|
| +
|
| +namespace {
|
| +
|
| +// Test fixture to test suggestions.
|
| +class JsSuggestionManagerTest : public web::WebJsTest<ChromeWebTest> {
|
| + protected:
|
| + JsSuggestionManagerTest()
|
| + : web::WebJsTest<ChromeWebTest>(@[ @"suggestion_controller" ]) {}
|
| + // Loads the given HTML and initializes the Autofill JS scripts.
|
| + void LoadHtml(NSString* html);
|
| + // Helper method that initializes a form with three fields. Can be used to
|
| + // test whether adding an attribute on the second field causes it to be
|
| + // skipped (or not, as is appropriate) by selectNextElement.
|
| + void SequentialNavigationSkipCheck(NSString* attribute, BOOL shouldSkip);
|
| + // Returns the active element name from the JS side.
|
| + NSString* GetActiveElementName() {
|
| + return ExecuteJavaScript(@"document.activeElement.name");
|
| + }
|
| + JsSuggestionManager* manager_;
|
| +};
|
| +
|
| +void JsSuggestionManagerTest::LoadHtml(NSString* html) {
|
| + WebJsTest<ChromeWebTest>::LoadHtml(html);
|
| + manager_ =
|
| + static_cast<JsSuggestionManager*>([web_state()->GetJSInjectionReceiver()
|
| + instanceOfClass:[JsSuggestionManager class]]);
|
| + [manager_ inject];
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, InitAndInject) {
|
| + LoadHtml(@"<html></html>");
|
| + EXPECT_TRUE([manager_ hasBeenInjected]);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SelectElementInTabOrder) {
|
| + NSString* htmlFragment =
|
| + @"<html> <body>"
|
| + "<input id='1 (0)' tabIndex=1 href='http://www.w3schools.com'>1 (0)</a>"
|
| + "<input id='0 (0)' tabIndex=0 href='http://www.w3schools.com'>0 (0)</a>"
|
| + "<input id='2' tabIndex=2 href='http://www.w3schools.com'>2</a>"
|
| + "<input id='0 (1)' tabIndex=0 href='http://www.w3schools.com'>0 (1)</a>"
|
| + "<input id='-2' tabIndex=-2 href='http://www.w3schools.com'>-2</a>"
|
| + "<a href='http://www.w3schools.com'></a>"
|
| + "<input id='-1 (0)' tabIndex=-1 href='http://www.w3schools.com'>-1</a>"
|
| + "<input id='-2 (2)' tabIndex=-2 href='http://www.w3schools.com'>-2</a>"
|
| + "<input id='0 (2)' tabIndex=0 href='http://www.w3schools.com'>0 - 2</a>"
|
| + "<input id='3' tabIndex=3 href='http://www.w3schools.com'>3</a>"
|
| + "<input id='1 (1)' tabIndex=1 href='http://www.w3schools.com'>1 (1)</a>"
|
| + "<input id='-1 (1)' tabIndex=-1 href='http://www.w3schools.com'>-1 </a>"
|
| + "<input id='0 (3)' tabIndex=0 href='http://www.w3schools.com'>0 (3)</a>"
|
| + "</body></html>";
|
| + LoadHtmlAndInject(htmlFragment);
|
| +
|
| + // clang-format off
|
| + NSDictionary* next_expected_ids = @ {
|
| + @"1 (0)" : @"1 (1)",
|
| + @"0 (0)" : @"0 (1)",
|
| + @"2" : @"3",
|
| + @"0 (1)" : @"0 (2)",
|
| + @"-2" : @"0 (2)",
|
| + @"-1 (0)" : @"0 (2)",
|
| + @"-2 (2)" : @"0 (2)",
|
| + @"0 (2)" : @"0 (3)",
|
| + @"3" : @"0 (0)",
|
| + @"1 (1)" : @"2",
|
| + @"-1 (1)" : @"0 (3)",
|
| + @"0 (3)" : @"null"
|
| + };
|
| + // clang-format on
|
| +
|
| + for (NSString* element_id : next_expected_ids) {
|
| + NSString* expected_id = [next_expected_ids objectForKey:element_id];
|
| + EXPECT_NSEQ(expected_id,
|
| + ExecuteJavaScriptWithFormat(
|
| + @"var elements=document.getElementsByTagName('input');"
|
| + "var element=document.getElementById('%@');"
|
| + "var next = __gCrWeb.suggestion.getNextElementInTabOrder("
|
| + " element, elements);"
|
| + "next ? next.id : 'null';",
|
| + element_id))
|
| + << "Wrong when selecting next element of element with element id "
|
| + << base::SysNSStringToUTF8(element_id);
|
| + }
|
| + EXPECT_NSEQ(@YES,
|
| + ExecuteJavaScriptWithFormat(
|
| + @"var elements=document.getElementsByTagName('input');"
|
| + "var element=document.getElementsByTagName('a')[0];"
|
| + "var next = __gCrWeb.suggestion.getNextElementInTabOrder("
|
| + " element, elements); next===null"))
|
| + << "Wrong when selecting the next element of an element not in the "
|
| + << "element list.";
|
| +
|
| + for (NSString* element_id : next_expected_ids) {
|
| + NSString* expected_id = [next_expected_ids objectForKey:element_id];
|
| + if ([expected_id isEqualToString:@"null"]) {
|
| + // If the expected next element is null, the focus is not moved.
|
| + expected_id = element_id;
|
| + }
|
| + EXPECT_NSEQ(expected_id, ExecuteJavaScriptWithFormat(
|
| + @"document.getElementById('%@').focus();"
|
| + "__gCrWeb.suggestion.selectNextElement();"
|
| + "document.activeElement.id",
|
| + element_id))
|
| + << "Wrong when selecting next element with active element "
|
| + << base::SysNSStringToUTF8(element_id);
|
| + }
|
| +
|
| + for (NSString* element_id : next_expected_ids) {
|
| + // If the expected next element is null, there is no next element.
|
| + BOOL expected = ![next_expected_ids[element_id] isEqualToString:@"null"];
|
| + EXPECT_NSEQ(@(expected), ExecuteJavaScriptWithFormat(
|
| + @"document.getElementById('%@').focus();"
|
| + "__gCrWeb.suggestion.hasNextElement()",
|
| + element_id))
|
| + << "Wrong when checking hasNextElement() for "
|
| + << base::SysNSStringToUTF8(element_id);
|
| + }
|
| +
|
| + // clang-format off
|
| + NSDictionary* prev_expected_ids = @{
|
| + @"1 (0)" : @"null",
|
| + @"0 (0)" : @"3",
|
| + @"2" : @"1 (1)",
|
| + @"0 (1)" : @"0 (0)",
|
| + @"-2" : @"0 (1)",
|
| + @"-1 (0)": @"0 (1)",
|
| + @"-2 (2)": @"0 (1)",
|
| + @"0 (2)" : @"0 (1)",
|
| + @"3" : @"2",
|
| + @"1 (1)" : @"1 (0)",
|
| + @"-1 (1)": @"1 (1)",
|
| + @"0 (3)" : @"0 (2)",
|
| + };
|
| + // clang-format on
|
| +
|
| + for (NSString* element_id : prev_expected_ids) {
|
| + NSString* expected_id = [prev_expected_ids objectForKey:element_id];
|
| + EXPECT_NSEQ(
|
| + expected_id,
|
| + ExecuteJavaScriptWithFormat(
|
| + @"var elements=document.getElementsByTagName('input');"
|
| + "var element=document.getElementById('%@');"
|
| + "var prev = __gCrWeb.suggestion.getPreviousElementInTabOrder("
|
| + " element, elements);"
|
| + "prev ? prev.id : 'null';",
|
| + element_id))
|
| + << "Wrong when selecting prev element of element with element id "
|
| + << base::SysNSStringToUTF8(element_id);
|
| + }
|
| + EXPECT_NSEQ(
|
| + @YES, ExecuteJavaScriptWithFormat(
|
| + @"var elements=document.getElementsByTagName('input');"
|
| + "var element=document.getElementsByTagName('a')[0];"
|
| + "var prev = __gCrWeb.suggestion.getPreviousElementInTabOrder("
|
| + " element, elements); prev===null"))
|
| + << "Wrong when selecting the previous element of an element not in the "
|
| + << "element list";
|
| +
|
| + for (NSString* element_id : prev_expected_ids) {
|
| + NSString* expected_id = [prev_expected_ids objectForKey:element_id];
|
| + if ([expected_id isEqualToString:@"null"]) {
|
| + // If the expected previous element is null, the focus is not moved.
|
| + expected_id = element_id;
|
| + }
|
| + EXPECT_NSEQ(expected_id, ExecuteJavaScriptWithFormat(
|
| + @"document.getElementById('%@').focus();"
|
| + "__gCrWeb.suggestion.selectPreviousElement();"
|
| + "document.activeElement.id",
|
| + element_id))
|
| + << "Wrong when selecting previous element with active element "
|
| + << base::SysNSStringToUTF8(element_id);
|
| + }
|
| +
|
| + for (NSString* element_id : prev_expected_ids) {
|
| + // If the expected next element is null, there is no next element.
|
| + BOOL expected = ![prev_expected_ids[element_id] isEqualToString:@"null"];
|
| + EXPECT_NSEQ(@(expected), ExecuteJavaScriptWithFormat(
|
| + @"document.getElementById('%@').focus();"
|
| + "__gCrWeb.suggestion.hasPreviousElement()",
|
| + element_id))
|
| + << "Wrong when checking hasPreviousElement() for "
|
| + << base::SysNSStringToUTF8(element_id);
|
| + }
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigation) {
|
| + LoadHtml(@"<html><body><form name='testform' method='post'>"
|
| + "<input type='text' name='firstname'/>"
|
| + "<input type='text' name='lastname'/>"
|
| + "<input type='email' name='email'/>"
|
| + "</form></body></html>");
|
| +
|
| + [manager_
|
| + executeJavaScript:@"document.getElementsByName('firstname')[0].focus()"
|
| + completionHandler:nil];
|
| + [manager_ selectNextElement];
|
| + EXPECT_NSEQ(@"lastname", GetActiveElementName());
|
| + __block BOOL block_was_called = NO;
|
| + [manager_ fetchPreviousAndNextElementsPresenceWithCompletionHandler:^void(
|
| + BOOL has_previous_element, BOOL has_next_element) {
|
| + block_was_called = YES;
|
| + EXPECT_TRUE(has_previous_element);
|
| + EXPECT_TRUE(has_next_element);
|
| + }];
|
| + base::test::ios::WaitUntilCondition(^bool() {
|
| + return block_was_called;
|
| + });
|
| + [manager_ selectNextElement];
|
| + EXPECT_NSEQ(@"email", GetActiveElementName());
|
| + [manager_ selectPreviousElement];
|
| + EXPECT_NSEQ(@"lastname", GetActiveElementName());
|
| +}
|
| +
|
| +void JsSuggestionManagerTest::SequentialNavigationSkipCheck(NSString* attribute,
|
| + BOOL shouldSkip) {
|
| + LoadHtml([NSString stringWithFormat:@"<html><body>"
|
| + "<form name='testform' method='post'>"
|
| + "<input type='text' name='firstname'/>"
|
| + "<%@ name='middlename'/>"
|
| + "<input type='text' name='lastname'/>"
|
| + "</form></body></html>",
|
| + attribute]);
|
| + [manager_
|
| + executeJavaScript:@"document.getElementsByName('firstname')[0].focus()"
|
| + completionHandler:nil];
|
| + NSString* const kActiveElementNameJS = @"document.activeElement.name";
|
| + EXPECT_NSEQ(@"firstname",
|
| + web::ExecuteJavaScript(manager_, kActiveElementNameJS));
|
| + [manager_ selectNextElement];
|
| + NSString* activeElementNameJS = GetActiveElementName();
|
| + if (shouldSkip)
|
| + EXPECT_NSEQ(@"lastname", activeElementNameJS);
|
| + else
|
| + EXPECT_NSEQ(@"middlename", activeElementNameJS);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipText) {
|
| + SequentialNavigationSkipCheck(@"input type='text'", NO);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipTextArea) {
|
| + SequentialNavigationSkipCheck(@"input type='textarea'", NO);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationOverInvisibleElement) {
|
| + SequentialNavigationSkipCheck(@"input type='text' style='display:none'", YES);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationOverHiddenElement) {
|
| + SequentialNavigationSkipCheck(@"input type='text' style='visibility:hidden'",
|
| + YES);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationOverDisabledElement) {
|
| + SequentialNavigationSkipCheck(@"type='text' disabled", YES);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipPassword) {
|
| + SequentialNavigationSkipCheck(@"input type='password'", NO);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipSubmit) {
|
| + SequentialNavigationSkipCheck(@"input type='submit'", YES);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipImage) {
|
| + SequentialNavigationSkipCheck(@"input type='image'", YES);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipButton) {
|
| + SequentialNavigationSkipCheck(@"input type='button'", YES);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipRange) {
|
| + SequentialNavigationSkipCheck(@"input type='range'", YES);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipRadio) {
|
| + SequentialNavigationSkipCheck(@"type='radio'", YES);
|
| +}
|
| +
|
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipCheckbox) {
|
| + SequentialNavigationSkipCheck(@"type='checkbox'", YES);
|
| +}
|
| +
|
| +// Special test for a condition where the closeKeyboard script would cause an
|
| +// illegal JS recursion if a blur event results in an event that triggers a
|
| +// crwebinvoke:// back, such as a page change.
|
| +TEST_F(JsSuggestionManagerTest, CloseKeyboardSafetyTest) {
|
| + LoadHtml(@"<select id='select'>Select</select>");
|
| + ExecuteJavaScript(
|
| + @"select.onblur = function(){window.location.href = '#test'}");
|
| + ExecuteJavaScript(@"select.focus()");
|
| + // In the failure condition the app will crash during the next line.
|
| + [manager_ closeKeyboard];
|
| + // TODO(crbug.com/661624): add a check for the keyboard actually being
|
| + // dismissed; unfortunately it is not known how to adapt
|
| + // WaitForBackgroundTasks to yield for events wrapped with window.setTimeout()
|
| + // or other deferred events.
|
| +}
|
| +
|
| +// Test fixture to test
|
| +// |fetchPreviousAndNextElementsPresenceWithCompletionHandler|.
|
| +class FetchPreviousAndNextExceptionTest : public JsSuggestionManagerTest {
|
| + public:
|
| + void SetUp() override {
|
| + JsSuggestionManagerTest::SetUp();
|
| + LoadHtml(@"<html></html>");
|
| + }
|
| +
|
| + protected:
|
| + // Evaluates JS and tests that the completion handler passed to
|
| + // |fetchPreviousAndNextElementsPresenceWithCompletionHandler| is called with
|
| + // (NO, NO) indicating no previous and next element.
|
| + void EvaluateJavaScriptAndExpectNoPreviousAndNextElement(NSString* js) {
|
| + ExecuteJavaScript(js);
|
| + __block BOOL block_was_called = NO;
|
| + id completionHandler = ^(BOOL hasPreviousElement, BOOL hasNextElement) {
|
| + EXPECT_FALSE(hasPreviousElement);
|
| + EXPECT_FALSE(hasNextElement);
|
| + block_was_called = YES;
|
| + };
|
| + [manager_ fetchPreviousAndNextElementsPresenceWithCompletionHandler:
|
| + completionHandler];
|
| + base::test::ios::WaitUntilCondition(^bool() {
|
| + return block_was_called;
|
| + });
|
| + }
|
| +};
|
| +
|
| +// Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works
|
| +// when |__gCrWeb.suggestion.hasPreviousElement| throws an exception.
|
| +TEST_F(FetchPreviousAndNextExceptionTest, HasPreviousElementException) {
|
| + EvaluateJavaScriptAndExpectNoPreviousAndNextElement(
|
| + @"__gCrWeb.suggestion.hasPreviousElement = function() { bar.foo1; }");
|
| +}
|
| +
|
| +// Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works
|
| +// when |__gCrWeb.suggestion.hasNextElement| throws an exception.
|
| +TEST_F(FetchPreviousAndNextExceptionTest, HasNextElementException) {
|
| + EvaluateJavaScriptAndExpectNoPreviousAndNextElement(
|
| + @"__gCrWeb.suggestion.hasNextElement = function() { bar.foo1; }");
|
| +}
|
| +
|
| +// Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works
|
| +// when |Array.toString| has been overridden to return a malformed string
|
| +// without a ",".
|
| +TEST_F(FetchPreviousAndNextExceptionTest, HasPreviousElementNull) {
|
| + EvaluateJavaScriptAndExpectNoPreviousAndNextElement(
|
| + @"Array.prototype.toString = function() { return 'Hello'; }");
|
| +}
|
| +
|
| +} // namespace
|
|
|