[go: nahoru, domu]

blob: 0d29525dcb7f9867865ba48a84b7b97cd8b90420 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/public/test/earl_grey/web_view_actions.h"
#import <WebKit/WebKit.h>
#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/logging.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/time/time.h"
#import "base/values.h"
#import "ios/testing/earl_grey/earl_grey_app.h"
#import "ios/web/public/test/earl_grey/web_view_matchers.h"
#import "ios/web/public/test/element_selector.h"
#import "ios/web/public/test/web_state_test_util.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#import "ios/web/public/web_state.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
using web::test::ExecuteJavaScript;
namespace {
// Long press duration to trigger context menu.
constexpr base::TimeDelta kContextMenuLongPressDuration = base::Seconds(1);
// Duration to wait for verification of JavaScript action.
// TODO(crbug.com/670910): Reduce duration if the time required for verification
// is reduced on devices.
constexpr base::TimeDelta kWaitForVerificationTimeout = base::Seconds(8);
// Returns a no element found error.
id<GREYAction> WebViewElementNotFound(ElementSelector* selector) {
NSString* description = [NSString
stringWithFormat:@"Couldn't locate a bounding rect for element %@; "
@"either it isn't there or it has no area.",
selector.selectorDescription];
GREYPerformBlock throw_error =
^BOOL(id /* element */, __strong NSError** error) {
NSDictionary* user_info = @{NSLocalizedDescriptionKey : description};
*error = [NSError errorWithDomain:kGREYInteractionErrorDomain
code:kGREYInteractionActionFailedErrorCode
userInfo:user_info];
return NO;
};
return [GREYActionBlock actionWithName:@"Locate element bounds"
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 {
id<GREYAction> WebViewVerifiedActionOnElement(WebState* state,
id<GREYAction> action,
ElementSelector* selector) {
NSString* action_name =
[NSString stringWithFormat:@"Verified action (%@) on webview element %@.",
action.name, selector.selectorDescription];
GREYPerformBlock verified_tap = ^BOOL(id element, __strong NSError** error) {
NSString* verifier_script = [NSString
stringWithFormat:
@"return await new Promise((resolve) => {"
" var element = %@;"
" if (!element) {"
" resolve('No element');"
" }"
" const timeoutId = setTimeout(() => {"
" resolve(false);"
// JS timeout slightly shorter than `kWaitForVerificationTimeout`
" }, 7900);"
" var options = { 'capture': true, 'once': true, 'passive': true "
"};"
" element.addEventListener('mousedown', function(event) {"
" clearTimeout(timeoutId);"
" resolve(true);"
" }, options);"
"});",
selector.selectorScript];
__block bool async_call_complete = false;
__block bool verified = false;
// GREYPerformBlock executes on background thread by default in EG2.
// Dispatch any call involving UI API to UI thread as they can't be executed
// on background thread. See go/eg2-migration#greyactions-threading-behavior
grey_dispatch_sync_on_main_thread(^{
WKWebView* web_view =
[web::test::GetWebController(state) ensureWebViewCreated];
[web_view
callAsyncJavaScript:verifier_script
arguments:nil
inFrame:nil
inContentWorld:[WKContentWorld pageWorld]
completionHandler:^(id result, NSError* async_error) {
if (!async_error) {
if ([result isKindOfClass:[NSString class]]) {
DLOG(ERROR) << base::SysNSStringToUTF8(result);
} else if ([result isKindOfClass:[NSNumber class]]) {
verified = [result boolValue];
}
}
async_call_complete = true;
}];
});
// Run the action and wait for the UI to settle.
NSError* actionError = nil;
[[[GREYElementInteraction alloc]
initWithElementMatcher:WebViewInWebState(state)]
performAction:action
error:&actionError];
if (actionError) {
*error = actionError;
return NO;
}
[[GREYUIThreadExecutor sharedInstance] drainUntilIdle];
// Wait for the verified to trigger and set `verified`.
bool success = base::test::ios::WaitUntilConditionOrTimeout(
kWaitForVerificationTimeout, ^{
return async_call_complete;
});
if (!success || !verified) {
DLOG(WARNING) << base::SysNSStringToUTF8([NSString
stringWithFormat:@"The action (%@) on element %@ wasn't "
@"verified before timing out.",
action.name, selector.selectorDescription]);
return NO;
}
return YES;
};
return [GREYActionBlock actionWithName:action_name
constraints:WebViewInWebState(state)
performBlock:verified_tap];
}
id<GREYAction> WebViewLongPressElementForContextMenu(
WebState* state,
ElementSelector* selector,
bool triggers_context_menu) {
CGRect rect = web::test::GetBoundingRectOfElement(state, selector);
if (CGRectIsEmpty(rect)) {
return WebViewElementNotFound(selector);
}
CGPoint point = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
id<GREYAction> longpress = grey_longPressAtPointWithDuration(
point, kContextMenuLongPressDuration.InSecondsF());
if (triggers_context_menu) {
return longpress;
}
return WebViewVerifiedActionOnElement(state, longpress, selector);
}
id<GREYAction> WebViewTapElement(WebState* state,
ElementSelector* selector,
bool verified) {
CGRect rect = web::test::GetBoundingRectOfElement(state, selector);
if (CGRectIsEmpty(rect)) {
return WebViewElementNotFound(selector);
}
CGPoint point = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
id<GREYAction> tap_action = grey_tapAtPoint(point);
if (!verified) {
return tap_action;
}
return WebViewVerifiedActionOnElement(state, tap_action, selector);
}
id<GREYAction> WebViewScrollElementToVisible(WebState* state,
ElementSelector* selector) {
const char kScrollToVisibleTemplate[] = "%1$s.scrollIntoView();";
std::string selector_script =
base::SysNSStringToUTF8(selector.selectorScript);
const std::string kScrollToVisibleScript =
base::StringPrintf(kScrollToVisibleTemplate, selector_script.c_str());
NSString* action_name =
[NSString stringWithFormat:@"Scroll element %@ to visible",
selector.selectorDescription];
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::apple::ObjCCast<WKWebView>(element);
if (!web_view) {
*error_or_nil = error_block(@"WebView not found.");
return NO;
}
__block BOOL success = NO;
// GREYPerformBlock executes on background thread by default in EG2.
// Dispatch any call involving UI API to UI thread as they can't be
// executed on background thread. See
// go/eg2-migration#greyactions-threading-behavior
grey_dispatch_sync_on_main_thread(^{
// 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;
}
if (IsRectVisibleInView(rect, web_view)) {
success = YES;
return;
}
// Ask the element to scroll itself into view.
web::test::ExecuteJavaScript(state, kScrollToVisibleScript);
// Wait until the element is visible.
bool check = base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, ^{
CGRect newRect =
web::test::GetBoundingRectOfElement(state, selector);
return IsRectVisibleInView(newRect, web_view);
});
if (!check) {
*error_or_nil = error_block(@"Element still not visible.");
return;
}
success = YES;
});
return success;
}];
return scroll_to_visible;
}
} // namespace web