Commit 962335e6 authored by Eric Noyau's avatar Eric Noyau Committed by Commit Bot

Autofill automation: Adds the ability to click on page elements.

Bug: None
Cq-Include-Trybots: luci.chromium.try:ios-simulator-full-configs;master.tryserver.chromium.mac:ios-simulator-cronet
Change-Id: If151d8de6e0a3e0f9aaec878f4b123909bbb7752
Reviewed-on: https://chromium-review.googlesource.com/1107804
Commit-Queue: Eric Noyau <noyau@chromium.org>
Reviewed-by: default avatarMoe Ahmadi <mahmadi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#574867}
parent 421412e7
......@@ -6,6 +6,8 @@ source_set("eg_tests") {
configs += [ "//build/config/compiler:enable_arc" ]
testonly = true
sources = [
"automation_action.h",
"automation_action.mm",
"automation_egtest.mm",
]
......
// 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.
#ifndef IOS_CHROME_BROWSER_AUTOFILL_AUTOMATION_AUTOMATION_ACTION_H_
#define IOS_CHROME_BROWSER_AUTOFILL_AUTOMATION_AUTOMATION_ACTION_H_
#import <Foundation/Foundation.h>
#include "base/values.h"
// AutomationAction consumes description of actions in base::Value format,
// generated in json by an extension and executes them on the current
// active web page. AutomationAction is an abstract superclass for a class
// cluster, the class method -actionWithValueDictionary: returns concrete
// subclasses for the various possible actions.
@interface AutomationAction : NSObject
// Returns an concrete instance of a subclass of AutomationAction.
+ (instancetype)actionWithValueDictionary:
(const base::DictionaryValue&)actionDictionary;
// Prevents creating rogue instances, the init methods are private.
- (instancetype)init NS_UNAVAILABLE;
// For subclasses to implement, execute the action. Use GREYAssert in case of
// issue.
- (void)execute;
@end
#endif // IOS_CHROME_BROWSER_AUTOFILL_AUTOMATION_AUTOMATION_ACTION_H_
// 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.
#import <EarlGrey/EarlGrey.h>
#import "ios/chrome/browser/autofill/automation/automation_action.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/web/public/test/earl_grey/web_view_actions.h"
#import "ios/web/public/test/earl_grey/web_view_matchers.h"
#include "ios/web/public/test/element_selector.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using web::test::ElementSelector;
@interface AutomationAction () {
std::unique_ptr<const base::DictionaryValue> actionDictionary_;
}
@property(nonatomic, readonly)
const std::unique_ptr<const base::DictionaryValue>& actionDictionary;
// Selects the proper subclass in the class cluster for the given type. Called
// from the class method creating the actions.
+ (Class)classForType:(NSString*)type;
- (instancetype)initWithValueDictionary:
(const base::DictionaryValue&)actionDictionary NS_DESIGNATED_INITIALIZER;
@end
// An action that always fails.
@interface AutomationActionUnrecognized : AutomationAction
@end
// An action that simply tap on an element on the page.
@interface AutomationActionClick : AutomationAction
@end
@implementation AutomationAction
+ (instancetype)actionWithValueDictionary:
(const base::DictionaryValue&)actionDictionary {
const base::Value* typeValue =
actionDictionary.FindKeyOfType("type", base::Value::Type::STRING);
GREYAssert(typeValue, @"Type is missing in action.");
const std::string type(typeValue->GetString());
GREYAssert(!type.empty(), @"Type is an empty value.");
return [[[self classForType:base::SysUTF8ToNSString(type)] alloc]
initWithValueDictionary:actionDictionary];
}
+ (Class)classForType:(NSString*)type {
static NSDictionary* classForType = @{
@"click" : [AutomationActionClick class],
// More to come.
};
return classForType[type] ?: [AutomationActionUnrecognized class];
}
- (instancetype)initWithValueDictionary:
(const base::DictionaryValue&)actionDictionary {
self = [super init];
if (self) {
actionDictionary_ = actionDictionary.DeepCopyWithoutEmptyChildren();
}
return self;
}
- (void)execute {
GREYAssert(NO, @"Should not be called!");
}
- (const std::unique_ptr<const base::DictionaryValue>&)actionDictionary {
return actionDictionary_;
}
@end
@implementation AutomationActionClick
- (void)execute {
// Right now this always assumes a click event of the following format:
// {
// "selectorType": "xpath",
// "selector": "//*[@id=\"add-to-cart-button\"]",
// "context": [],
// "type": "click"
// },
const base::Value* xpathValue(self.actionDictionary->FindKeyOfType(
"selector", base::Value::Type::STRING));
GREYAssert(xpathValue, @"Selector is missing in action.");
const std::string xpath(xpathValue->GetString());
GREYAssert(!xpath.empty(), @"selector is an empty value.");
auto selector(ElementSelector::ElementSelectorXPath(xpath));
web::WebState* web_state = chrome_test_util::GetCurrentWebState();
// Wait for the element to be visible on the page.
[ChromeEarlGrey waitForWebViewContainingElement:selector];
// Potentially scroll into view if below the fold.
[[EarlGrey selectElementWithMatcher:web::WebViewInWebState(web_state)]
performAction:WebViewScrollElementToVisible(web_state, selector)];
// Tap on the element.
[[EarlGrey selectElementWithMatcher:web::WebViewInWebState(web_state)]
performAction:web::WebViewTapElement(web_state, selector)];
}
@end
@implementation AutomationActionUnrecognized
- (void)execute {
const base::Value* typeValue =
self.actionDictionary->FindKeyOfType("type", base::Value::Type::STRING);
const std::string type(typeValue->GetString());
GREYAssert(NO, @"Unknown action of type %s", type.c_str());
}
@end
......@@ -9,7 +9,7 @@
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/values.h"
#import "ios/chrome/browser/autofill/automation/automation_action.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
......@@ -24,7 +24,9 @@ static const char kAutofillAutomationSwitch[] = "autofillautomation";
// The autofill automation test case is intended to run a script against a
// captured web site. It gets the script from the command line.
@interface AutofillAutomationTestCase : ChromeTestCase
@interface AutofillAutomationTestCase : ChromeTestCase {
NSMutableArray<AutomationAction*>* actions_;
}
@end
@implementation AutofillAutomationTestCase
......@@ -86,11 +88,32 @@ static const char kAutofillAutomationSwitch[] = "autofillautomation";
const GURL startUrl(startUrlString);
// Extract the actions.
base::Value* actionValue =
recipeRoot->FindKeyOfType("actions", base::Value::Type::LIST);
GREYAssert(actionValue, @"Test file is missing actions.");
const base::Value::ListStorage& actionsValues(actionValue->GetList());
GREYAssert(actionsValues.size(), @"Test file has empty actions.");
actions_ = [[NSMutableArray alloc] initWithCapacity:actionsValues.size()];
for (auto const& actionValue : actionsValues) {
GREYAssert(actionValue.is_dict(),
@"Expecting each action to be a dictionary in the JSON file.");
[actions_ addObject:[AutomationAction
actionWithValueDictionary:
static_cast<const base::DictionaryValue&>(
actionValue)]];
}
// Load the initial page of the recipe.
[ChromeEarlGrey loadURL:startUrl];
}
- (void)testSomething {
- (void)testActions {
for (AutomationAction* action in actions_) {
[action execute];
}
}
@end
{
"name": "rsolomakhin.github.io",
"startingURL": "https://rsolomakhin.github.io/",
"actions": [
{
"selectorType": "xpath",
"selector": "//*[@href=\"autofill/\"]",
"context": [],
"type": "click"
}
]
}
......@@ -38,6 +38,11 @@ id<GREYAction> WebViewLongPressElementForContextMenu(
id<GREYAction> WebViewTapElement(WebState* state,
web::test::ElementSelector selector);
// Scrolls the WebView so the element selected by |selector| is visible.
id<GREYAction> WebViewScrollElementToVisible(
WebState* state,
web::test::ElementSelector selector);
} // namespace web
#endif // IOS_WEB_PUBLIC_TEST_EARL_GREY_WEB_VIEW_ACTIONS_H_
......@@ -4,9 +4,12 @@
#import "ios/web/public/test/earl_grey/web_view_actions.h"
#import <WebKit/WebKit.h>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/strings/stringprintf.h"
#import "base/test/ios/wait_util.h"
#include "base/values.h"
......@@ -126,6 +129,32 @@ id<GREYAction> WebViewElementNotFound(web::test::ElementSelector selector) {
performBlock:throw_error];
}
// Checks that a rectangle in a view (expressed in this view's coordinate
// system) is actually visible and potentially tappable.
bool IsRectVisibleInView(CGRect rect, UIView* view) {
// Take a point at the center of the element.
CGPoint point_in_view = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
// Converts its coordinates to window coordinates.
CGPoint point_in_window =
[view convertPoint:point_in_view toView:view.window];
// Check if this point is actually on screen.
if (!CGRectContainsPoint(view.window.frame, point_in_window)) {
return false;
}
// Check that the view is not covered by another view).
UIView* hit = [view.window hitTest:point_in_window withEvent:nil];
while (hit) {
if (hit == view) {
return true;
}
hit = hit.superview;
}
return false;
}
} // namespace
namespace web {
......@@ -229,4 +258,65 @@ id<GREYAction> WebViewTapElement(WebState* state,
std::move(selector));
}
id<GREYAction> WebViewScrollElementToVisible(
WebState* state,
web::test::ElementSelector selector) {
const char kScrollToVisibleTemplate[] = "%1$s.scrollIntoView();";
const std::string kScrollToVisibleScript = base::StringPrintf(
kScrollToVisibleTemplate, selector.GetSelectorScript().c_str());
NSString* action_name =
[NSString stringWithFormat:@"Scroll element %s to visible",
selector.GetSelectorDescription().c_str()];
NSError* (^error_block)(NSString* error) = ^NSError*(NSString* error) {
return [NSError errorWithDomain:kGREYInteractionErrorDomain
code:kGREYInteractionActionFailedErrorCode
userInfo:@{NSLocalizedDescriptionKey : error}];
};
GREYActionBlock* scroll_to_visible = [GREYActionBlock
actionWithName:action_name
constraints:WebViewInWebState(state)
performBlock:^BOOL(id element, __strong NSError** error_or_nil) {
// Checks that the element is indeed a WKWebView.
WKWebView* web_view = base::mac::ObjCCast<WKWebView>(element);
if (!web_view) {
*error_or_nil = error_block(@"WebView not found.");
return NO;
}
// First checks if there is really a need to scroll, if the element is
// already visible just returns early.
CGRect rect = web::test::GetBoundingRectOfElement(state, selector);
if (CGRectIsEmpty(rect)) {
*error_or_nil = error_block(@"Element not found.");
return false;
}
if (IsRectVisibleInView(rect, web_view)) {
return YES;
}
// Ask the element to scroll itself into view.
web::test::ExecuteJavaScript(state, kScrollToVisibleScript);
// Wait until the element is visible.
bool check = testing::WaitUntilConditionOrTimeout(
testing::kWaitForUIElementTimeout, ^{
CGRect rect =
web::test::GetBoundingRectOfElement(state, selector);
return IsRectVisibleInView(rect, web_view);
});
if (!check) {
*error_or_nil = error_block(@"Element still not visible.");
return NO;
}
return YES;
}];
return scroll_to_visible;
}
} // namespace web
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