Commit 604d29e8 authored by Darren Shen's avatar Darren Shen Committed by Commit Bot

Revert "[ChromeDriver] Implements ExecuteAsyncScript promise-handling."

This reverts commit 207c5a51.

Reason for revert: Possible culprit for 970084

Original change's description:
> [ChromeDriver] Implements ExecuteAsyncScript promise-handling.
> 
> Handles promises according to W3C-spec
> (https://w3c.github.io/webdriver/#execute-async-script) for asynchronous
> user scripts. This removes the global "result" object that contains
> results of the latest script to execute and allows for scripts to wait
> until results are ready before returning (instead of polling for
> results after the script completes).
> 
> run_py_tests.py.
> 
> Tested: WPTs webdriver/test/execute_async_script, and python
> Bug: chromedriver:2398
> Change-Id: I9f6cdeb5b3e93fcfd3e63090ce8b3aed275d71be
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1636627
> Commit-Queue: Rohan Pavone <rohpavone@chromium.org>
> Reviewed-by: John Chen <johnchen@chromium.org>
> Reviewed-by: Caleb Rouleau <crouleau@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#665589}

TBR=crouleau@chromium.org,johnchen@chromium.org,rohpavone@chromium.org

Change-Id: I9e6c7aa0ac7f3df265c584a841afa9828a2cec4f
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: chromedriver:2398
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1642431Reviewed-by: default avatarDarren Shen <shend@chromium.org>
Reviewed-by: default avatarJohn Chen <johnchen@chromium.org>
Commit-Queue: Darren Shen <shend@chromium.org>
Cr-Commit-Position: refs/heads/master@{#665827}
parent c00cd97d
...@@ -996,14 +996,58 @@ Status WebViewImpl::CallAsyncFunctionInternal( ...@@ -996,14 +996,58 @@ Status WebViewImpl::CallAsyncFunctionInternal(
async_args.AppendString("return (" + function + ").apply(null, arguments);"); async_args.AppendString("return (" + function + ").apply(null, arguments);");
async_args.Append(args.CreateDeepCopy()); async_args.Append(args.CreateDeepCopy());
async_args.AppendBoolean(is_user_supplied); async_args.AppendBoolean(is_user_supplied);
async_args.AppendInteger(timeout.InMilliseconds());
std::unique_ptr<base::Value> tmp; std::unique_ptr<base::Value> tmp;
Status status = CallFunctionWithTimeout(frame, kExecuteAsyncScriptScript, Status status = CallFunctionWithTimeout(frame, kExecuteAsyncScriptScript,
async_args, timeout, &tmp); async_args, timeout, &tmp);
if (status.IsError()) if (status.IsError())
return status; return status;
*result = std::move(tmp); const char kDocUnloadError[] = "document unloaded while waiting for result";
return status; std::string kQueryResult = base::StringPrintf(
"function() {"
" var info = document.$chrome_asyncScriptInfo;"
" if (!info)"
" return {status: %d, value: '%s'};"
" var result = info.result;"
" if (!result)"
" return {status: 0};"
" delete info.result;"
" return result;"
"}",
kJavaScriptError,
kDocUnloadError);
while (true) {
base::ListValue no_args;
std::unique_ptr<base::Value> query_value;
Status status = CallFunction(frame, kQueryResult, no_args, &query_value);
if (status.IsError()) {
if (status.code() == kNoSuchFrame)
return Status(kJavaScriptError, kDocUnloadError);
return status;
}
base::DictionaryValue* result_info = NULL;
if (!query_value->GetAsDictionary(&result_info))
return Status(kUnknownError, "async result info is not a dictionary");
int status_code;
if (!result_info->GetInteger("status", &status_code))
return Status(kUnknownError, "async result info has no int 'status'");
if (status_code != kOk) {
std::string message;
result_info->GetString("value", &message);
return Status(static_cast<StatusCode>(status_code), message);
}
base::Value* value = NULL;
if (result_info->Get("value", &value)) {
result->reset(value->DeepCopy());
return Status(kOk);
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
}
} }
Status WebViewImpl::IsNotPendingNavigation(const std::string& frame_id, Status WebViewImpl::IsNotPendingNavigation(const std::string& frame_id,
......
...@@ -13,9 +13,28 @@ var StatusCode = { ...@@ -13,9 +13,28 @@ var StatusCode = {
SCRIPT_TIMEOUT: 28, SCRIPT_TIMEOUT: 28,
}; };
/**
* Dictionary key for asynchronous script info.
* @const
*/
var ASYNC_INFO_KEY = '$chrome_asyncScriptInfo';
/**
* Return the information of asynchronous script execution.
*
* @return {Object<?>} Information of asynchronous script execution.
*/
function getAsyncScriptInfo() {
if (!(ASYNC_INFO_KEY in document))
document[ASYNC_INFO_KEY] = {'id': 0};
return document[ASYNC_INFO_KEY];
}
/** /**
* Execute the given script and return a promise containing its result. * Execute the given script and save its asynchronous result.
*
* If script1 finishes after script2 is executed, then script1's result will be
* discarded while script2's will be saved.
* *
* @param {string} script The asynchronous script to be executed. The script * @param {string} script The asynchronous script to be executed. The script
* should be a proper function body. It will be wrapped in a function and * should be a proper function body. It will be wrapped in a function and
...@@ -26,49 +45,57 @@ var StatusCode = { ...@@ -26,49 +45,57 @@ var StatusCode = {
* If not, UnknownError will be used instead of JavaScriptError if an * If not, UnknownError will be used instead of JavaScriptError if an
* exception occurs during the script, and an additional error callback will * exception occurs during the script, and an additional error callback will
* be supplied to the script. * be supplied to the script.
* @param {?number} opt_timeoutMillis The timeout, in milliseconds, to use.
* If the timeout is exceeded and the callback has not been invoked, a error
* result will be saved and future invocation of the callback will be
* ignored.
*/ */
function executeAsyncScript(script, args, isUserSupplied) { function executeAsyncScript(script, args, isUserSupplied, opt_timeoutMillis) {
function isThenable(value) { var info = getAsyncScriptInfo();
return typeof value === 'object' && typeof value.then === 'function'; info.id++;
} delete info.result;
let resolveHandle; var id = info.id;
let rejectHandle;
var promise = new Promise((resolve, reject) => {
resolveHandle = resolve;
rejectHandle = reject;
});
args.push(resolveHandle); // Append resolve to end of arguments list. function report(status, value) {
if (id != info.id)
return;
info.id++;
// Undefined value is skipped when the object is converted to JSON.
// Replace it with null so we don't lose the value.
if (value === undefined)
value = null;
info.result = {status: status, value: value};
}
function reportValue(value) {
report(StatusCode.OK, value);
}
function reportScriptError(error) {
var code = isUserSupplied ? StatusCode.JAVASCRIPT_ERROR :
(error.code || StatusCode.UNKNOWN_ERROR);
var message = error.message;
if (error.stack) {
message += "\nJavaScript stack:\n" + error.stack;
}
report(code, message);
}
args.push(reportValue);
if (!isUserSupplied) if (!isUserSupplied)
args.push(rejectHandle); args.push(reportScriptError);
// This confusing, round-about way accomplishing this script execution is to
// follow the W3C execute-async-script spec.
try { try {
// The assumption is that each script is an asynchronous script. new Function(script).apply(null, args);
const scriptResult = new Function(script).apply(null, args); } catch (error) {
reportScriptError(error);
// First case is for user-scripts - they are all wrapped in an async return;
// function in order to allow for "await" commands. As a result, all async
// scripts from users will return a Promise that is thenable by default,
// even if it doesn't return anything.
if (isThenable(scriptResult)) {
const resolvedPromise = Promise.resolve(scriptResult);
resolvedPromise.then((value) => {
// Must be thenable if user-supplied.
if (!isUserSupplied || isThenable(value))
resolveHandle(value);
})
.catch(rejectHandle);
}
} catch(error) {
rejectHandle(error);
} }
return promise.catch((error) => { if (typeof(opt_timeoutMillis) != 'undefined') {
const code = isUserSupplied ? StatusCode.JAVASCRIPT_ERROR : window.setTimeout(function() {
(error.code || StatusCode.UNKNOWN_ERROR); var code = isUserSupplied ? StatusCode.SCRIPT_TIMEOUT :
error.code = code; StatusCode.UNKNOWN_ERROR;
throw error; var errorMsg = 'result was not received in ' + opt_timeoutMillis / 1000 +
}); ' seconds';
report(code, errorMsg);
}, opt_timeoutMillis);
}
} }
...@@ -4,28 +4,23 @@ ...@@ -4,28 +4,23 @@
<script src='execute_async_script.js'></script> <script src='execute_async_script.js'></script>
<script> <script>
async function testScriptThrows() { function resetAsyncScriptInfo() {
let promise = executeAsyncScript('f(123);', [], true).then((result) => { delete document[ASYNC_INFO_KEY];
assert(false);
}).catch((error) => {
assertEquals(StatusCode.JAVASCRIPT_ERROR, error.code);
return 1;
});
let result = await promise;
assertEquals(1, result);
executeAsyncScript('f(123);', [], false).then((result) => {
assert(false);
}).catch((error) => {
assertEquals(StatusCode.UNKNOWN_ERROR, error.code);
return 1;
});
result = await promise;
assertEquals(1, result);
} }
async function testUserScriptWithArgs() { function testScriptThrows() {
resetAsyncScriptInfo();
var info = getAsyncScriptInfo();
executeAsyncScript('f(123);', [], true);
assertEquals(StatusCode.JAVASCRIPT_ERROR, info.result.status);
executeAsyncScript('f(123);', [], false);
assertEquals(StatusCode.UNKNOWN_ERROR, info.result.status);
}
function testUserScriptWithArgs() {
resetAsyncScriptInfo();
var injectedArgs = null; var injectedArgs = null;
function captureArguments(args) { function captureArguments(args) {
injectedArgs = args; injectedArgs = args;
...@@ -35,49 +30,106 @@ async function testUserScriptWithArgs() { ...@@ -35,49 +30,106 @@ async function testUserScriptWithArgs() {
var script = var script =
'var args = arguments; args[0](args); args[args.length - 1](args[1]);'; 'var args = arguments; args[0](args); args[args.length - 1](args[1]);';
var script_args = [captureArguments, 1]; var script_args = [captureArguments, 1];
await executeAsyncScript(script, script_args, true).then((result) => { executeAsyncScript(script, script_args, true);
assertEquals(1, result);
});
assertEquals(3, injectedArgs.length); assertEquals(3, injectedArgs.length);
assertEquals(captureArguments, injectedArgs[0]); assertEquals(captureArguments, injectedArgs[0]);
assertEquals(1, injectedArgs[1]); assertEquals(1, injectedArgs[1]);
var info = getAsyncScriptInfo();
assertEquals(0, info.result.status);
assertEquals(1, info.result.value);
assertEquals(2, info.id);
}
function testNonUserScript() {
resetAsyncScriptInfo();
var info = getAsyncScriptInfo();
executeAsyncScript('arguments[1](arguments[0])', [33], false);
assertEquals(0, info.result.status);
assertEquals(33, info.result.value);
executeAsyncScript('arguments[2](new Error("ERR"))', [33], false);
assertEquals(StatusCode.UNKNOWN_ERROR, info.result.status);
assertEquals(0, info.result.value.indexOf('ERR'));
executeAsyncScript('var e = new Error("ERR"); e.code = 111; arguments[1](e)',
[], false);
assertEquals(111, info.result.status);
assertEquals(0, info.result.value.indexOf('ERR'));
}
function testNoResultBeforeTimeout() {
resetAsyncScriptInfo();
var info = getAsyncScriptInfo();
executeAsyncScript(
'var a = arguments; window.setTimeout(function() {a[0](33)}, 0);',
[], true, 0);
assert(!info.result);
} }
async function testNonUserScript() { function testZeroTimeout(runner) {
await executeAsyncScript('arguments[1](arguments[0])', [33], resetAsyncScriptInfo();
false).then((result) => { var info = getAsyncScriptInfo();
assertEquals(33, result);
}); executeAsyncScript(
'var a = arguments; window.setTimeout(function() {a[0](33)}, 0);',
await executeAsyncScript('arguments[2](new Error("ERR"))', [33], [], true, 0);
false).then((result) => {
assert(false); window.setTimeout(function() {
}).catch((error) => { assertEquals(0, info.result.status);
assertEquals(StatusCode.UNKNOWN_ERROR, error.code); assertEquals(33, info.result.value);
assertEquals(0, error.message.indexOf('ERR')); runner.continueTesting();
}); }, 0);
runner.waitForAsync();
await executeAsyncScript(` }
var e = new Error("ERR");
e.code = 111; function testUserScriptTimesOut(runner) {
arguments[1](e)`, resetAsyncScriptInfo();
[], false).then((result) => { assert(false); }).catch((error) => { var info = getAsyncScriptInfo();
assertEquals(111, error.code);
assertEquals(0, error.message.indexOf('ERR')); executeAsyncScript('', [], true, 500);
});
window.setTimeout(function() {
assertEquals(StatusCode.SCRIPT_TIMEOUT, info.result.status);
assert(info.result.value.indexOf('0.5') != -1);
runner.continueTesting();
}, 500);
runner.waitForAsync();
} }
async function testFirstScriptFinishAfterSecondScriptExecute() {
let firstCompleted = false; function testNonUserScriptTimesOut(runner) {
resetAsyncScriptInfo();
var info = getAsyncScriptInfo();
executeAsyncScript('', [], false, 500);
window.setTimeout(function() {
assertEquals(StatusCode.UNKNOWN_ERROR, info.result.status);
assert(info.result.value.indexOf('0.5') != -1);
runner.continueTesting();
}, 500);
runner.waitForAsync();
}
function testFirstScriptFinishAfterSecondScriptExecute() {
resetAsyncScriptInfo();
executeAsyncScript( executeAsyncScript(
`var f = arguments[0]; 'var f = arguments[0]; setTimeout(function(){ f(1); }, 100000);', []);
setTimeout(function(){ f(1); }, 100000);`, []).then((result) => { var info = getAsyncScriptInfo();
firstCompleted = true; assert(!info.hasOwnProperty('result'));
}); assertEquals(1, info.id);
await executeAsyncScript('var fn = arguments[0]; fn(2);',
[]).then((result) => { executeAsyncScript('var fn = arguments[0]; fn(2);', []);
assert(!firstCompleted); assertEquals(0, info.result.status);
assertEquals(2, result); assertEquals(2, info.result.value);
}); assertEquals(3, info.id);
} }
</script> </script>
......
...@@ -601,7 +601,7 @@ Status ExecuteExecuteAsyncScript(Session* session, ...@@ -601,7 +601,7 @@ Status ExecuteExecuteAsyncScript(Session* session,
script = script + "\n"; script = script + "\n";
Status status = web_view->CallUserAsyncFunction( Status status = web_view->CallUserAsyncFunction(
session->GetCurrentFrameId(), "async function(){" + script + "}", *args, session->GetCurrentFrameId(), "function(){" + script + "}", *args,
session->script_timeout, value); session->script_timeout, value);
if (status.code() == kTimeout) if (status.code() == kTimeout)
return Status(kScriptTimeout); return Status(kScriptTimeout);
......
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