Commit c44dadf2 authored by Javier Ernesto Flores Robles's avatar Javier Ernesto Flores Robles Committed by Commit Bot

[iOS][MF] Support iFrames in Manual Fallback

Enables the injection handler used in Manual Fallback to work on frames.
Adds testing utilities to support tapping an element in a window
frame.
Adds the frame messaging flag to the manual fallback test bot.

Bug: 845472
Change-Id: Id054361af1f4be5450f13cd0afabe46e24ea11ff
Reviewed-on: https://chromium-review.googlesource.com/c/1292409
Commit-Queue: Javier Ernesto Flores Robles <javierrobles@chromium.org>
Reviewed-by: default avatarOlivier Robin <olivierrobin@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Reviewed-by: default avatarBen Pastene <bpastene@chromium.org>
Reviewed-by: default avatarMoe Ahmadi <mahmadi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#604364}
parent 8ba674d9
......@@ -10,7 +10,7 @@
{
"app": "ios_chrome_manual_fill_egtests",
"test args": [
"--enable-features=AutofillManualFallback"
"--enable-features=AutofillManualFallback,WebFrameMessaging"
],
"xctest": true
},
......
......@@ -123,6 +123,7 @@ source_set("eg_tests") {
"//base",
"//base/test:test_support",
"//components/autofill/core/common",
"//components/autofill/ios/browser",
"//components/keyed_service/core",
"//components/password_manager/core/browser",
"//ios/chrome/browser/passwords",
......@@ -133,6 +134,6 @@ source_set("eg_tests") {
"//ios/third_party/earl_grey:earl_grey+link",
"//ios/web:earl_grey_test_support",
"//ios/web/public/test/http_server",
"//third_party/ocmock:ocmock",
"//third_party/ocmock",
]
}
......@@ -4,7 +4,16 @@
#import "ios/chrome/browser/ui/autofill/manual_fill/manual_fill_injection_handler.h"
#include <memory>
#include <string>
#include <vector>
#include "base/json/string_escape.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/values.h"
#include "components/autofill/ios/browser/autofill_switches.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/browser/js_suggestion_manager.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#include "components/autofill/ios/form_util/form_activity_params.h"
......@@ -12,6 +21,8 @@
#import "ios/chrome/browser/ui/autofill/manual_fill/form_observer_helper.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#include "ios/web/public/web_state/web_frame.h"
#include "ios/web/public/web_state/web_frame_util.h"
#include "ios/web/public/web_state/web_frames_manager.h"
#include "ios/web/public/web_state/web_state.h"
#include "url/gurl.h"
......@@ -20,24 +31,38 @@
#error "This file requires ARC support."
#endif
namespace {
// The timeout for any JavaScript call in this file.
const int64_t kJavaScriptExecutionTimeoutInSeconds = 1;
}
@interface ManualFillInjectionHandler ()<FormActivityObserver>
// The object in charge of listening to form events and reporting back.
@property(nonatomic, strong) FormObserverHelper* formHelper;
// Convenience getter for the current injection reciever.
@property(nonatomic, readonly) CRWJSInjectionReceiver* injectionReceiver;
// Convenience getter for the current suggestion manager.
@property(nonatomic, readonly) JsSuggestionManager* suggestionManager;
// The WebStateList with the relevant active web state for the injection.
@property(nonatomic, assign) WebStateList* webStateList;
// YES if the last focused element is secure within its web frame. To be secure
// means it has a password type, the web is https and the URL can trusted.
@property(nonatomic, assign) BOOL lastActiveElementIsSecure;
@property(nonatomic, assign) BOOL lastFocusedElementIsSecure;
// The last seen frame ID with focus activity.
@property(nonatomic, assign) std::string lastFocusedElementFrameIdentifier;
// The last seen focused element identifier.
@property(nonatomic, assign) std::string lastFocusedElementIdentifier;
@end
@implementation ManualFillInjectionHandler
@synthesize formHelper = _formHelper;
@synthesize lastActiveElementIsSecure = _lastActiveElementIsSecure;
@synthesize webStateList = _webStateList;
- (instancetype)initWithWebStateList:(WebStateList*)webStateList {
self = [super init];
......@@ -52,7 +77,7 @@
#pragma mark - ManualFillViewDelegate
- (void)userDidPickContent:(NSString*)content isSecure:(BOOL)isSecure {
if (isSecure && !self.lastActiveElementIsSecure) {
if (isSecure && !self.lastFocusedElementIsSecure) {
return;
}
[self fillLastSelectedFieldWithString:content];
......@@ -66,13 +91,18 @@
if (params.type != "focus") {
return;
}
web::URLVerificationTrustLevel trustLevel;
const GURL pageURL(webState->GetCurrentURL(&trustLevel));
self.lastActiveElementIsSecure = YES;
if (trustLevel != web::URLVerificationTrustLevel::kAbsolute ||
!pageURL.SchemeIs(url::kHttpsScheme) || !webState->ContentIsHTML() ||
params.field_type != "password") {
self.lastActiveElementIsSecure = NO;
BOOL isContextSecure = autofill::IsContextSecureForWebState(webState);
BOOL isPasswordField = params.field_type == "password";
self.lastFocusedElementIsSecure = isContextSecure && isPasswordField;
self.lastFocusedElementIdentifier = params.field_identifier;
if (autofill::switches::IsAutofillIFrameMessagingEnabled()) {
DCHECK(frame);
self.lastFocusedElementFrameIdentifier = frame->GetFrameId();
const GURL frameSecureOrigin = frame->GetSecurityOrigin();
if (!frameSecureOrigin.SchemeIsCryptographic()) {
self.lastFocusedElementIsSecure = NO;
}
}
}
......@@ -100,7 +130,32 @@
// Injects the passed string to the active field and jumps to the next field.
- (void)fillLastSelectedFieldWithString:(NSString*)string {
// TODO:(https://crbug.com/878388) validation / escaping of string.
if (autofill::switches::IsAutofillIFrameMessagingEnabled()) {
web::WebState* activeWebState = self.webStateList->GetActiveWebState();
if (!activeWebState) {
return;
}
web::WebFrame* activeWebFrame = web::GetWebFrameWithId(
activeWebState, self.lastFocusedElementFrameIdentifier);
if (!activeWebFrame || !activeWebFrame->CanCallJavaScriptFunction()) {
return;
}
base::DictionaryValue data = base::DictionaryValue();
data.SetString("identifier", self.lastFocusedElementIdentifier);
data.SetString("value", base::SysNSStringToUTF16(string));
std::vector<base::Value> parameters;
parameters.push_back(std::move(data));
activeWebFrame->CallJavaScriptFunction(
"autofill.fillActiveFormField", parameters,
base::BindOnce(^(const base::Value*) {
[self jumpToNextField];
}),
base::TimeDelta::FromSeconds(kJavaScriptExecutionTimeoutInSeconds));
return;
}
// Frame messaging is disabled, use the old injection reciever.
NSString* javaScriptQuery =
[NSString stringWithFormat:
@"__gCrWeb.fill.setInputElementValue(\"%@\", "
......
......@@ -147,6 +147,10 @@ NSString* const OtherPasswordsAccessibilityIdentifier =
net::registry_controlled_domains::GetDomainAndRegistry(
visibleURL.host(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
// Sometimes the site_name can be empty. i.e. if the host is an IP address.
if (site_name.empty()) {
site_name = visibleURL.host();
}
NSString* siteName = base::SysUTF8ToNSString(site_name);
NSPredicate* predicate =
......
......@@ -11,6 +11,7 @@
#import "base/test/ios/wait_util.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/password_form.h"
#include "components/autofill/ios/browser/autofill_switches.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/browser/password_store_consumer.h"
......@@ -25,8 +26,10 @@
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#include "ios/web/public/features.h"
#import "ios/web/public/test/earl_grey/web_view_matchers.h"
#include "ios/web/public/test/element_selector.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h"
......@@ -43,6 +46,7 @@ const char kExampleUsername[] = "concrete username";
const char kExamplePassword[] = "concrete password";
const char kFormHTMLFile[] = "/username_password_field_form.html";
const char kIFrameHTMLFile[] = "/iframe_form.html";
// Returns a matcher for the password icon in the keyboard accessory bar.
id<GREYMatcher> PasswordIconMatcher() {
......@@ -190,7 +194,17 @@ void SaveExamplePasswordForm() {
autofill::PasswordForm example;
example.username_value = base::ASCIIToUTF16(kExampleUsername);
example.password_value = base::ASCIIToUTF16(kExamplePassword);
example.origin = GURL("https://example.com");
example.origin = GURL("https://example.com/");
example.signon_realm = example.origin.spec();
SaveToPasswordStore(example);
}
// Saves an example form in the store.
void SaveLocalPasswordForm() {
autofill::PasswordForm example;
example.username_value = base::ASCIIToUTF16(kExampleUsername);
example.password_value = base::ASCIIToUTF16(kExamplePassword);
example.origin = GURL("http://127.0.0.1:55264");
example.signon_realm = example.origin.spec();
SaveToPasswordStore(example);
}
......@@ -204,6 +218,21 @@ void ClearPasswordStore() {
@"PasswordStore was not cleared.");
}
// Polls the JavaScript query |java_script_condition| until the returned
// |boolValue| is YES with a kWaitForActionTimeout timeout.
BOOL WaitForJavaScriptCondition(NSString* java_script_condition) {
auto verify_block = ^BOOL {
id value = chrome_test_util::ExecuteJavaScript(java_script_condition, nil);
return [value isEqual:@YES];
};
NSTimeInterval timeout = base::test::ios::kWaitForActionTimeout;
NSString* condition_name = [NSString
stringWithFormat:@"Wait for JS condition: %@", java_script_condition];
GREYCondition* condition =
[GREYCondition conditionWithName:condition_name block:verify_block];
return [condition waitWithTimeout:timeout];
}
} // namespace
// Integration Tests for Mannual Fallback Passwords View Controller.
......@@ -475,7 +504,7 @@ void ClearPasswordStore() {
assertWithMatcher:grey_notVisible()];
}
// Test that after switching fields the content size of the table view didn't
// Tests that after switching fields the content size of the table view didn't
// grow.
- (void)testPasswordControllerKeepsRightSize {
// Bring up the keyboard.
......@@ -503,7 +532,7 @@ void ClearPasswordStore() {
assertWithMatcher:grey_sufficientlyVisible()];
}
// Test that the Password View Controller stays on rotation.
// Tests that the Password View Controller stays on rotation.
- (void)testPasswordControllerSupportsRotation {
// Bring up the keyboard.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
......@@ -525,4 +554,47 @@ void ClearPasswordStore() {
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that content is injected in iframe messaging.
- (void)testPasswordControllerSupportsIFrameMessaging {
// Iframe messaging is not supported on iOS < 11.3.
if (!base::ios::IsRunningOnOrLater(11, 3, 0)) {
EARL_GREY_TEST_SKIPPED(@"Skipped for iOS < 11.3");
}
GREYAssert(base::FeatureList::IsEnabled(web::features::kWebFrameMessaging),
@"Frame Messaging must be enabled for this Test Case");
const GURL URL = self.testServer->GetURL(kIFrameHTMLFile);
[ChromeEarlGrey loadURL:URL];
[ChromeEarlGrey waitForWebViewContainingText:"iFrame"];
SaveLocalPasswordForm();
// Bring up the keyboard.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
performAction:chrome_test_util::TapWebElementInFrame(kFormElementUsername,
0)];
// Wait for the accessory icon to appear.
[GREYKeyboard waitForKeyboardToAppear];
// Tap on the passwords icon.
[[EarlGrey selectElementWithMatcher:PasswordIconMatcher()]
performAction:grey_tap()];
// Verify the password controller table view is visible.
[[EarlGrey selectElementWithMatcher:PasswordTableViewMatcher()]
assertWithMatcher:grey_sufficientlyVisible()];
// Select a username.
[[EarlGrey selectElementWithMatcher:UsernameButtonMatcher()]
performAction:grey_tap()];
// Verify Web Content.
NSString* javaScriptCondition = [NSString
stringWithFormat:
@"window.frames[0].document.getElementById('%s').value === '%s'",
kFormElementUsername, kExampleUsername];
XCTAssertTrue(WaitForJavaScriptCondition(javaScriptCondition));
}
@end
......@@ -34,6 +34,13 @@ id<GREYAction> TurnSyncSwitchOn(BOOL on);
// state.
id<GREYAction> TapWebElement(const std::string& element_id);
// Action to tap a web element in iframe with the given |element_id| on the
// current web state. iframe is an immediate child of the main frame with the
// given index. The action fails if target iframe has a different origin from
// the main frame.
id<GREYAction> TapWebElementInFrame(const std::string& element_id,
const int frame_index);
} // namespace chrome_test_util
#endif // IOS_CHROME_TEST_EARL_GREY_CHROME_ACTIONS_H_
......@@ -74,4 +74,12 @@ id<GREYAction> TapWebElement(const std::string& element_id) {
web::test::ElementSelector::ElementSelectorId(element_id));
}
id<GREYAction> TapWebElementInFrame(const std::string& element_id,
const int frame_index) {
return web::WebViewTapElement(
chrome_test_util::GetCurrentWebState(),
web::test::ElementSelector::ElementSelectorIdInFrame(element_id,
frame_index));
}
} // namespace chrome_test_util
......@@ -77,6 +77,7 @@ bundle_data("http_server_bundle_data") {
"data/http_server_files/history.js",
"data/http_server_files/history_go.html",
"data/http_server_files/history_go.js",
"data/http_server_files/iframe_form.html",
"data/http_server_files/iframe_host.html",
"data/http_server_files/links.html",
"data/http_server_files/memory_usage.html",
......
<!DOCTYPE html>
<!-- Copyright 2018 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. -->
<p>iFrame
<iframe src="username_password_field_form.html"></iframe>
......@@ -18,6 +18,14 @@ class ElementSelector {
// Returns an ElementSelector to retrieve an element by ID.
static const ElementSelector ElementSelectorId(const std::string element_id);
// Returns an ElementSelector to retrieve an element in iframe by ID. iframe
// is an immediate child of the main frame with the given index. The script of
// this selector will throw an exception if target iframe has a different
// origin from the main frame.
static const ElementSelector ElementSelectorIdInFrame(
const std::string element_id,
const int frame_index);
// Returns an ElementSelector to retrieve an element by a CSS selector.
static const ElementSelector ElementSelectorCss(
const std::string css_selector);
......
......@@ -21,6 +21,17 @@ const ElementSelector ElementSelector::ElementSelectorId(
base::StringPrintf("with ID %s", element_id.c_str()));
}
// Static.
const ElementSelector ElementSelector::ElementSelectorIdInFrame(
const std::string element_id,
const int frame_index) {
return ElementSelector(
base::StringPrintf("window.frames[%d].document.getElementById('%s')",
frame_index, element_id.c_str()),
base::StringPrintf("in iframe with index %d, with ID %s", frame_index,
element_id.c_str()));
}
// Static.
const ElementSelector ElementSelector::ElementSelectorCss(
const std::string css_selector) {
......
......@@ -691,7 +691,7 @@
"label": "//ios/chrome/test/earl_grey:ios_chrome_manual_fill_egtests",
"type": "raw",
"args": [
"--enable-features=AutofillManualFallback",
"--enable-features=AutofillManualFallback,WebFrameMessaging",
],
},
"ios_chrome_reading_list_egtests": {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment