blob: 710ddd054f1a33dd9cccbb346a8d63820e2d5028 [file] [log] [blame]
// Copyright 2016 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 "ios/web/public/test/earl_grey/web_view_actions.h"
#include "base/callback_helpers.h"
#include "base/logging.h"
#import "base/mac/bind_objc_block.h"
#include "base/strings/stringprintf.h"
#import "base/test/ios/wait_util.h"
#include "base/values.h"
#import "ios/testing/wait_util.h"
#import "ios/web/public/test/earl_grey/web_view_matchers.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#import "ios/web/web_state/web_state_impl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using web::test::ExecuteJavaScript;
namespace {
// Long press duration to trigger context menu.
const NSTimeInterval kContextMenuLongPressDuration = 0.3;
// Duration to wait for verification of JavaScript action.
// TODO(crbug.com/670910): Reduce duration if the time required for verification
// is reduced on devices.
const NSTimeInterval kWaitForVerificationTimeout = 8.0;
// Callback prefix for injected verifiers.
const std::string CallbackPrefixForElementId(const std::string& element_id) {
return "__web_test_" + element_id + "_interaction";
}
// Generic verification injector. Injects one-time mousedown verification into
// |web_state| that will set the boolean pointed to by |verified| to true when
// |web_state|'s webview registers the mousedown event.
// RemoveVerifierForElementWithId() should be called after this to ensure
// future tests can add verifiers with the same prefix.
bool AddVerifierToElementWithId(web::WebState* web_state,
const std::string& element_id,
bool* verified) {
const std::string kCallbackPrefix = CallbackPrefixForElementId(element_id);
const char kCallbackCommand[] = "verified";
const std::string kCallbackInvocation =
kCallbackPrefix + '.' + kCallbackCommand;
const char kAddInteractionVerifierScriptTemplate[] =
"(function() {"
// First template param: element ID.
" var elementId = '%1$s';"
" var element = document.getElementById(elementId);"
" if (!element)"
" return 'Element ' + elementId + ' not found';"
" var invokeType = typeof __gCrWeb.message;"
" if (invokeType != 'object')"
" return 'Host invocation not installed (' + invokeType + ')';"
" var options = {'capture' : true, 'once' : true, 'passive' : true};"
" element.addEventListener('mousedown', function(event) {"
" __gCrWeb.message.invokeOnHost("
// Second template param: callback command.
" {'command' : '%2$s' });"
" }, options);"
" return true;"
"})();";
const std::string kAddVerifierScript =
base::StringPrintf(kAddInteractionVerifierScriptTemplate,
element_id.c_str(), kCallbackInvocation.c_str());
bool success =
testing::WaitUntilConditionOrTimeout(testing::kWaitForUIElementTimeout, ^{
bool verifier_added = false;
std::unique_ptr<base::Value> value =
web::test::ExecuteJavaScript(web_state, kAddVerifierScript);
if (value) {
std::string error;
if (value->GetAsString(&error)) {
DLOG(ERROR) << "Verifier injection failed: " << error
<< ", retrying.";
} else if (value->GetAsBoolean(&verifier_added)) {
return true;
}
}
return false;
});
if (!success)
return false;
// The callback doesn't care about any of the parameters, just whether it is
// called or not.
auto callback = base::BindBlockArc(
^bool(const base::DictionaryValue& /* json */,
const GURL& /* origin_url */, bool /* user_is_interacting */) {
*verified = true;
return true;
});
static_cast<web::WebStateImpl*>(web_state)->AddScriptCommandCallback(
callback, kCallbackPrefix);
return true;
}
// Removes the injected callback.
void RemoveVerifierForElementWithId(web::WebState* web_state,
const std::string& element_id) {
static_cast<web::WebStateImpl*>(web_state)->RemoveScriptCommandCallback(
CallbackPrefixForElementId(element_id));
}
// Returns a no element found error.
id<GREYAction> WebViewElementNotFound(const std::string& element_id) {
NSString* description = [NSString
stringWithFormat:@"Couldn't locate a bounding rect for element_id %s; "
@"either it isn't there or it has no area.",
element_id.c_str()];
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];
}
} // namespace
namespace web {
id<GREYAction> WebViewVerifiedActionOnElement(WebState* state,
id<GREYAction> action,
const std::string& element_id) {
NSString* action_name =
[NSString stringWithFormat:@"Verified action (%@) on webview element %s.",
action.name, element_id.c_str()];
GREYPerformBlock verified_tap = ^BOOL(id element, __strong NSError** error) {
// A pointer to |verified| is passed into AddVerifierToElementWithId() so
// the verifier can update its value, but |verified| also needs to be marked
// as __block so that waitUntilCondition(), below, can access it by
// reference.
__block bool verified = false;
// Ensure that RemoveVerifierForElementWithId() is run regardless of how
// the block exits.
base::ScopedClosureRunner cleanup(
base::Bind(&RemoveVerifierForElementWithId, state, element_id));
// Inject the verifier.
bool verifier_added =
AddVerifierToElementWithId(state, element_id, &verified);
if (!verifier_added) {
NSString* description = [NSString
stringWithFormat:@"It wasn't possible to add the verification "
@"javascript for element_id %s",
element_id.c_str()];
NSDictionary* user_info = @{NSLocalizedDescriptionKey : description};
*error = [NSError errorWithDomain:kGREYInteractionErrorDomain
code:kGREYInteractionActionFailedErrorCode
userInfo:user_info];
return NO;
}
// Run the action.
[[EarlGrey selectElementWithMatcher:WebViewInWebState(state)]
performAction:action
error:error];
if (*error) {
return NO;
}
// Wait for the verified to trigger and set |verified|.
NSString* verification_timeout_message =
[NSString stringWithFormat:@"The action (%@) on element_id %s wasn't "
@"verified before timing out.",
action.name, element_id.c_str()];
GREYAssert(testing::WaitUntilConditionOrTimeout(
kWaitForVerificationTimeout,
^{
return verified;
}),
verification_timeout_message);
// If |verified| is not true, the wait condition should have already exited
// this control flow, so sanity check that it has in fact been set to
// true by this point.
DCHECK(verified);
return YES;
};
return [GREYActionBlock actionWithName:action_name
constraints:WebViewInWebState(state)
performBlock:verified_tap];
}
id<GREYAction> WebViewLongPressElementForContextMenu(
WebState* state,
const std::string& element_id,
bool triggers_context_menu) {
CGRect rect = web::test::GetBoundingRectOfElementWithId(state, element_id);
if (CGRectIsEmpty(rect)) {
return WebViewElementNotFound(element_id);
}
CGPoint point = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
id<GREYAction> longpress =
grey_longPressAtPointWithDuration(point, kContextMenuLongPressDuration);
if (triggers_context_menu) {
return longpress;
}
return WebViewVerifiedActionOnElement(state, longpress, element_id);
}
id<GREYAction> WebViewTapElement(WebState* state,
const std::string& element_id) {
CGRect rect = web::test::GetBoundingRectOfElementWithId(state, element_id);
CGPoint point = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
return CGRectIsEmpty(rect) ? WebViewElementNotFound(element_id)
: WebViewVerifiedActionOnElement(
state, grey_tapAtPoint(point), element_id);
}
} // namespace web