OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/web/public/test/earl_grey/web_view_actions.h" |
| 6 |
| 7 #include "base/callback_helpers.h" |
| 8 #include "base/logging.h" |
| 9 #include "base/mac/bind_objc_block.h" |
| 10 #include "base/strings/stringprintf.h" |
| 11 #include "base/test/ios/wait_util.h" |
| 12 #include "base/values.h" |
| 13 #import "ios/testing/earl_grey/wait_util.h" |
| 14 #import "ios/web/public/test/earl_grey/web_view_matchers.h" |
| 15 #import "ios/web/public/test/web_view_interaction_test_util.h" |
| 16 #import "ios/web/web_state/web_state_impl.h" |
| 17 |
| 18 using web::test::ExecuteJavaScript; |
| 19 |
| 20 namespace { |
| 21 |
| 22 // Long press duration to trigger context menu. |
| 23 const NSTimeInterval kContextMenuLongPressDuration = 0.3; |
| 24 |
| 25 // Callback prefix for injected verifiers. |
| 26 const std::string CallbackPrefixForElementId(const std::string& element_id) { |
| 27 return "__web_test_" + element_id + "_interaction"; |
| 28 } |
| 29 |
| 30 // Generic verification injector. Injects one-time mousedown verification into |
| 31 // |web_state| that will set the boolean pointed to by |verified| to true when |
| 32 // |web_state|'s webview registers the mousedown event. |
| 33 // RemoveVerifierForElementWithId() should be called after this to ensure |
| 34 // future tests can add verifiers with the same prefix. |
| 35 bool AddVerifierToElementWithId(web::WebState* web_state, |
| 36 const std::string& element_id, |
| 37 bool* verified) { |
| 38 const std::string kCallbackPrefix = CallbackPrefixForElementId(element_id); |
| 39 const char kCallbackCommand[] = "verified"; |
| 40 const std::string kCallbackInvocation = |
| 41 kCallbackPrefix + '.' + kCallbackCommand; |
| 42 |
| 43 const char kAddInteractionVerifierScriptTemplate[] = |
| 44 "(function() {" |
| 45 // First template param: element ID. |
| 46 " var elementId = '%1$s';" |
| 47 " var element = document.getElementById(elementId);" |
| 48 " if (!element)" |
| 49 " return 'Element ' + elementId + ' not found';" |
| 50 " var invokeType = typeof __gCrWeb.message;" |
| 51 " if (invokeType != 'object')" |
| 52 " return 'Host invocation not installed (' + invokeType + ')';" |
| 53 " var options = {'capture' : true, 'once' : true, 'passive' : true};" |
| 54 " element.addEventListener('mousedown', function(event) {" |
| 55 " __gCrWeb.message.invokeOnHost(" |
| 56 // Second template param: callback command. |
| 57 " {'command' : '%2$s' });" |
| 58 " }, options);" |
| 59 " return true;" |
| 60 "})();"; |
| 61 |
| 62 const std::string kAddVerifierScript = |
| 63 base::StringPrintf(kAddInteractionVerifierScriptTemplate, |
| 64 element_id.c_str(), kCallbackInvocation.c_str()); |
| 65 NSDate* deadline = |
| 66 [NSDate dateWithTimeIntervalSinceNow:testing::kWaitForUIElementTimeout]; |
| 67 bool verifier_added = false; |
| 68 while (([[NSDate date] compare:deadline] != NSOrderedDescending) && |
| 69 !verifier_added) { |
| 70 std::unique_ptr<base::Value> value = |
| 71 web::test::ExecuteJavaScript(web_state, kAddVerifierScript); |
| 72 if (value) { |
| 73 std::string error; |
| 74 if (value->GetAsString(&error)) { |
| 75 DLOG(ERROR) << "Verifier injection failed: " << error << ", retrying."; |
| 76 } else if (value->GetAsBoolean(&verifier_added)) { |
| 77 verifier_added = true; |
| 78 } |
| 79 } |
| 80 base::test::ios::SpinRunLoopWithMaxDelay( |
| 81 base::TimeDelta::FromSecondsD(testing::kSpinDelaySeconds)); |
| 82 } |
| 83 |
| 84 if (!verifier_added) |
| 85 return false; |
| 86 |
| 87 // The callback doesn't care about any of the parameters, just whether it is |
| 88 // called or not. |
| 89 auto callback = base::BindBlock(^bool(const base::DictionaryValue& /* json */, |
| 90 const GURL& /* origin_url */, |
| 91 bool /* user_is_interacting */) { |
| 92 *verified = true; |
| 93 return true; |
| 94 }); |
| 95 |
| 96 static_cast<web::WebStateImpl*>(web_state)->AddScriptCommandCallback( |
| 97 callback, kCallbackPrefix); |
| 98 return true; |
| 99 } |
| 100 |
| 101 // Removes the injected callback. |
| 102 void RemoveVerifierForElementWithId(web::WebState* web_state, |
| 103 const std::string& element_id) { |
| 104 static_cast<web::WebStateImpl*>(web_state)->RemoveScriptCommandCallback( |
| 105 CallbackPrefixForElementId(element_id)); |
| 106 } |
| 107 |
| 108 } // namespace |
| 109 |
| 110 namespace web { |
| 111 |
| 112 id<GREYAction> webViewVerifiedActionOnElement(WebState* state, |
| 113 id<GREYAction> action, |
| 114 const std::string& element_id) { |
| 115 NSString* action_name = |
| 116 [NSString stringWithFormat:@"Verified action (%@) on webview element %s.", |
| 117 action.name, element_id.c_str()]; |
| 118 |
| 119 GREYPerformBlock verified_tap = ^BOOL(id element, __strong NSError** error) { |
| 120 // A pointer to |verified| is passed into AddVerifierToElementWithId() so |
| 121 // the verifier can update its value, but |verified| also needs to be marked |
| 122 // as __block so that waitUntilCondition(), below, can access it by |
| 123 // reference. |
| 124 __block bool verified = false; |
| 125 |
| 126 // Ensure that RemoveVerifierForElementWithId() is run regardless of how |
| 127 // the block exits. |
| 128 base::ScopedClosureRunner cleanup( |
| 129 base::Bind(&RemoveVerifierForElementWithId, state, element_id)); |
| 130 |
| 131 // Inject the vefifier. |
| 132 bool verifier_added = |
| 133 AddVerifierToElementWithId(state, element_id, &verified); |
| 134 if (!verifier_added) { |
| 135 NSString* description = [NSString |
| 136 stringWithFormat:@"It wasn't possible to add the verification " |
| 137 @"javascript for element_id %s", |
| 138 element_id.c_str()]; |
| 139 NSDictionary* user_info = @{NSLocalizedDescriptionKey : description}; |
| 140 *error = [NSError errorWithDomain:kGREYInteractionErrorDomain |
| 141 code:kGREYInteractionActionFailedErrorCode |
| 142 userInfo:user_info]; |
| 143 return NO; |
| 144 } |
| 145 |
| 146 // Run the action. |
| 147 [[EarlGrey selectElementWithMatcher:webViewInWebState(state)] |
| 148 performAction:action |
| 149 error:error]; |
| 150 |
| 151 if (*error) { |
| 152 return NO; |
| 153 } |
| 154 |
| 155 // Wait for the verified to trigger and set |verified|. |
| 156 NSString* verification_timeout_message = |
| 157 [NSString stringWithFormat:@"The action (%@) on element_id %s wasn't " |
| 158 @"verified before timing out.", |
| 159 action.name, element_id.c_str()]; |
| 160 testing::WaitUntilCondition(testing::kWaitForJSCompletionTimeout, |
| 161 verification_timeout_message, ^{ |
| 162 return verified; |
| 163 }); |
| 164 |
| 165 // If |verified| is not true, the wait condition should have already exited |
| 166 // this control flow, so sanity check that it has in fact been set to |
| 167 // true by this point. |
| 168 DCHECK(verified); |
| 169 return YES; |
| 170 }; |
| 171 |
| 172 return [GREYActionBlock actionWithName:action_name |
| 173 constraints:webViewInWebState(state) |
| 174 performBlock:verified_tap]; |
| 175 } |
| 176 |
| 177 id<GREYAction> webViewLongPressElementForContextMenu( |
| 178 WebState* state, |
| 179 const std::string& element_id, |
| 180 bool triggers_context_menu) { |
| 181 CGRect rect = web::test::GetBoundingRectOfElementWithId(state, element_id); |
| 182 // Check if |rect| is empty; if it is, return an action that just throws an |
| 183 // error. |
| 184 if (CGRectIsEmpty(rect)) { |
| 185 NSString* description = [NSString |
| 186 stringWithFormat:@"Couldn't locate a bounding rect for element_id %s; " |
| 187 @"either it isn't there or it has no area.", |
| 188 element_id.c_str()]; |
| 189 GREYPerformBlock throw_error = ^BOOL(id /* element */, |
| 190 __strong NSError** error) { |
| 191 NSDictionary* user_info = @{NSLocalizedDescriptionKey : description}; |
| 192 *error = [NSError errorWithDomain:kGREYInteractionErrorDomain |
| 193 code:kGREYInteractionActionFailedErrorCode |
| 194 userInfo:user_info]; |
| 195 return NO; |
| 196 }; |
| 197 return [GREYActionBlock actionWithName:@"Locate element bounds" |
| 198 performBlock:throw_error]; |
| 199 } |
| 200 |
| 201 // If there's a usable rect, long-press in the center. |
| 202 CGPoint point = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); |
| 203 |
| 204 id<GREYAction> longpress = |
| 205 grey_longPressAtPointWithDuration(point, kContextMenuLongPressDuration); |
| 206 id<GREYAction> action = longpress; |
| 207 |
| 208 if (!triggers_context_menu) { |
| 209 action = webViewVerifiedActionOnElement(state, longpress, element_id); |
| 210 } |
| 211 |
| 212 return action; |
| 213 } |
| 214 |
| 215 } // namespace web |
OLD | NEW |