Skip to content

Commit

Permalink
JasonLeyba: Deprecating bot.script.execute in favor of bot.inject.exe…
Browse files Browse the repository at this point in the history
…cuteAsyncScript and

migrating the iPhone from bot.script.

r12110
  • Loading branch information
jleyba committed May 4, 2011
1 parent eedfe77 commit 129069f
Show file tree
Hide file tree
Showing 8 changed files with 2,263 additions and 1,923 deletions.
10 changes: 5 additions & 5 deletions iphone/src/objc/HTTPVirtualDirectory+ExecuteScript.mm
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ -(id) executeAsyncJsFunction:(NSString*)script
[SimpleObserver simpleObserverForAction:@"webdriver:executeAsyncScript"
andSender:[self viewController]];

args = [NSArray arrayWithObjects:script, args,
[NSNumber numberWithDouble:timeout * 1000], nil];
NSString* atom =
[NSString stringWithUTF8String:webdriver::atoms::EXECUTE_ASYNC_SCRIPT];
[self executeScript:atom withArgs:args];
[[self viewController] jsEval:@"(%@)(function(){%@\n},%@,%@)",
[NSString stringWithUTF8String:webdriver::atoms::EXECUTE_ASYNC_SCRIPT],
script,
[args JSONRepresentation],
[NSNumber numberWithDouble:timeout * 1000]];

NSDictionary* resultDict = [observer waitForData];

Expand Down
3,658 changes: 1,903 additions & 1,755 deletions iphone/src/objc/atoms.h

Large diffs are not rendered by default.

88 changes: 72 additions & 16 deletions javascript/atoms/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,23 @@ goog.provide('bot.inject.cache');

goog.require('bot.Error');
goog.require('bot.ErrorCode');
goog.require('bot.script');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.events');
goog.require('goog.json');
goog.require('goog.object');



/**
* WebDriver wire protocol definition of a command response.
* @typedef {{status:bot.ErrorCode, value:*}}
* @see http://code.google.com/p/selenium/wiki/JsonWireProtocol#Responses
*/
bot.inject.Response;


/**
* Key used to identify DOM elements in the WebDriver wire protocol.
* @type {string}
Expand Down Expand Up @@ -190,12 +198,17 @@ bot.inject.executeScript = function(fn, args, opt_stringify) {
* Executes an injected script, which is expected to finish asynchronously
* before the given {@code timeout}. When the script finishes or an error
* occurs, the given {@code onDone} callback will be invoked. This callback
* will have a single argument, a dictionary containing a status code and
* a value.
* will have a single argument, a {@code bot.inject.Response} object.
*
* The script signals its completion by invoking a supplied callback given
* as its last argument. The callback may be invoked with a single value.
*
* The script timeout event will be scheduled with the provided window,
* ensuring the timeout is synchronized with that window's event queue.
* Furthermore, asynchronous scripts do not work across new page loads; if an
* "unload" event is fired on the window while an asynchronous script is
* pending, the script will be aborted and an error will be returned.
*
* Like {@code bot.inject.executeScript}, this function should only be called
* from an external source. It handles wrapping and unwrapping of input/output
* values.
Expand All @@ -205,33 +218,76 @@ bot.inject.executeScript = function(fn, args, opt_stringify) {
* @param {Array.<*>} args An array of wrapped script arguments, as defined by
* the WebDriver wire protocol.
* @param {number} timeout The amount of time, in milliseconds, the script
* should be permitted to run.
* @param {function((string|{status:bot.ErrorCode, value:*}))} onDone
* should be permitted to run; must be non-negative.
* @param {function(string|bot.inject.Response)} onDone
* The function to call when the given {@code fn} invokes its callback,
* or when an exception or timeout occurs. This will always be called.
* @param {boolean=} opt_stringify Whether the result should be returned as a
* serialized JSON string.
* @param {Window=} opt_window The window to synchronize the script with;
* defaults to the current window
*/
bot.inject.executeAsyncScript = function(fn, args, timeout, onDone,
opt_stringify) {
function invokeCallback(result) {
onDone(opt_stringify ? goog.json.serialize(result) : result);
opt_stringify, opt_window) {
var win = opt_window || window;
var timeoutId, onunloadKey;
var responseSent = false;

function sendResponse(status, value) {
if (!responseSent) {
responseSent = true;
goog.events.unlistenByKey(onunloadKey);
win.clearTimeout(timeoutId);
if (status != bot.ErrorCode.SUCCESS) {
var err = new bot.Error(status, value.message || value + '');
err.stack = value.stack;
value = bot.inject.wrapError_(err);
} else {
value = bot.inject.wrapResponse_(value);
}
onDone(opt_stringify ? goog.json.serialize(value) : value);
}
}
var sendError = goog.partial(sendResponse, bot.ErrorCode.UNKNOWN_ERROR);

if (win.closed) {
return sendError('Unable to execute script; the target window is closed.');
}

function onSuccess(value) {
invokeCallback(bot.inject.wrapResponse_(value));
if (goog.isString(fn)) {
fn = new Function(fn);
}

function onFailure(err) {
invokeCallback(bot.inject.wrapError_(err));
// If necessary, recompile the injected function in the context of the
// target window so symbol references are correct. This should also prevent
// against leaking globals from this window (as would be the case using the
// "with" keyword).
if (win != window) {
fn = win.eval('(' + fn + ')');
}

args.push(goog.partial(sendResponse, bot.ErrorCode.SUCCESS));
onunloadKey = goog.events.listen(win, goog.events.EventType.UNLOAD,
function() {
sendResponse(bot.ErrorCode.UNKNOWN_ERROR,
Error('Detected a page unload event; asynchronous script ' +
'execution does not work across page loads.'));
}, true);

var startTime = goog.now();
try {
var unwrappedArgs = (/**@type {Object}*/bot.inject.unwrapValue(args));
bot.script.execute(fn, unwrappedArgs, timeout, onSuccess, onFailure);
fn.apply(win, args);

// Register our timeout *after* the function has been invoked. This will
// ensure we don't timeout on a function that invokes its callback after
// a 0-based timeout.
timeoutId = win.setTimeout(function() {
sendResponse(bot.ErrorCode.SCRIPT_TIMEOUT,
Error('Timed out waiting for asyncrhonous script result ' +
'after ' + (goog.now() - startTime) + ' ms'));
}, Math.max(0, timeout));
} catch (ex) {
onFailure(new bot.Error(ex.code || bot.ErrorCode.UNKNOWN_ERROR,
ex.message));
sendResponse(ex.code || bot.ErrorCode.UNKNOWN_ERROR, ex);
}
};

Expand Down
1 change: 1 addition & 0 deletions javascript/atoms/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ goog.require('goog.events.EventType');
* to the current window. Asynchronous scripts will have their timeouts
* scheduled with this window. Furthermore, asynchronous scripts will
* be aborted if this window fires an unload event.
* @deprecated Use bot.inject.executeAsyncScript.
*/
bot.script.execute = function(script, args, timeout, onSuccess, onFailure,
opt_window) {
Expand Down
116 changes: 116 additions & 0 deletions javascript/atoms/test/inject_async_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html>
<head>
<title>inject_async_test</title>
<script src="test_bootstrap.js"></script>
<script>
goog.require('bot.ErrorCode');
goog.require('bot.inject');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
goog.require('goog.userAgent.product');
goog.require('goog.userAgent.product.isVersion');
</script>
</head>
<body>
<script>
var TEST_CASE = goog.testing.AsyncTestCase.createAndInstall(document.title);
var testWindow;

function setUp() {
testWindow = window.open('');
assertNotNull(testWindow);
assertNotUndefined(testWindow);
goog.global.onload = goog.partial(fail, 'Unexpected onload call');
}

function tearDown() {
testWindow.close();
testWindow = null;
}

function testSynchronizesTimeoutsWithGivenWindow() {
if (goog.userAgent.product.SAFARI &&
goog.userAgent.getUserAgentString().search('Version/5') == -1) {
// Fails in Safari prior to v5.
return;
}

if (goog.userAgent.IE && !goog.userAgent.product.isVersion(8)) {
// Fails in IE 7.
return;
}

function onDone(response) {
TEST_CASE.continueTesting();
assertEquals(bot.ErrorCode.SUCCESS, response['status']);
assertEquals(3, response['value']);
}

TEST_CASE.waitForAsync('bot.inject.executeAsyncScript');
bot.inject.executeAsyncScript(function() {
var callback = arguments[arguments.length - 1];
var count = 0;
window.setTimeout(function() {count += 1;}, 250);
window.setTimeout(function() {count += 2;}, 250);
window.setTimeout(function() {callback(count);}, 250);
}, [], 250, onDone, false, testWindow);
}

function testShouldExecuteInTheContextOfTheProvidedWindow() {
if (goog.userAgent.IE || goog.userAgent.product.SAFARI) {
// IE has trouble communicating with testWindow.
// Safari cannot execute script in testWindow's context.
return;
}

function onDone(response) {
TEST_CASE.continueTesting();
assertEquals(bot.ErrorCode.SUCCESS, response['status']);
assertEquals('script_data', response['value']);
}

TEST_CASE.waitForAsync('waiting for test window to load page');
goog.global.onload = function(loadedWindow) {
assertEquals('wrong window', testWindow, loadedWindow);

TEST_CASE.waitForAsync('bot.inject.executeAsyncScript');
bot.inject.executeAsyncScript(function() {
arguments[arguments.length - 1](document.title);
}, [], 0, onDone, false, testWindow);
};
testWindow.location = './testdata/script_data.html';
}

function testCanDetectPageUnloadEventsAndFailsTheScriptWhenTheyOccur() {
if (goog.userAgent.IE || goog.userAgent.product.SAFARI) {
// IE has trouble communicating with testWindow.
// Safari cannot execute script in testWindow's context.
return;
}

var navigateTo = window.location.protocol + '//' + window.location.host;

function onDone(response) {
TEST_CASE.continueTesting();
assertEquals(bot.ErrorCode.UNKNOWN_ERROR, response['status']);

var message = response['value']['message'];
assertTrue(message, /Detected a page unload event;/.test(message));
}

TEST_CASE.waitForAsync('waiting for test window to load page');
goog.global.onload = function(loadedWindow) {
assertEquals('wrong window', testWindow, loadedWindow);

TEST_CASE.waitForAsync('bot.inject.executeAsyncScript');
bot.inject.executeAsyncScript(function() {
window.location = arguments[0];
}, [navigateTo], 250, onDone, false, testWindow);
};
testWindow.location = './testdata/script_data.html';
}
</script>
</body>
</html>
32 changes: 32 additions & 0 deletions javascript/atoms/test/inject_test.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<iframe id="test-frame"></iframe>
</div>
<script type="text/javascript">
goog.global.MY_GLOBAL_CONSTANT = 123;

// We need to use an AsyncTestCase since a few of the tests require a
// window.setTimeout delay for the conditions under test.
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
Expand Down Expand Up @@ -352,6 +354,36 @@
"arguments[1](arguments[0])", args, 0, onDone);
}

function testShouldExecuteAsyncScriptsInTheContextOfTheSpecifiedWindow() {
var testFrameWin = goog.dom.getFrameContentWindow(
goog.dom.$('test-frame'));

function onDone(response) {
asyncTestCase.continueTesting();
assertTrue(goog.object.containsKey(result, 'status'));
assertEquals(bot.ErrorCode.SUCCESS, result['status']);

assertTrue(goog.object.containsKey(result, 'value'));
var value = response['value'];

assertTrue(value['this == window']);
assertFalse(value['this == window.top']);
assertEquals('undefined', value['typeof window.MY_GLOBAL_CONSTANT']);
assertEquals('number', value['typeof window.top.MY_GLOBAL_CONSTANT']);
}

asyncTestCase.waitForAsync('callback');
bot.inject.executeAsyncScript(function(callback) {
callback({
'this == window': (this == window),
'this == top': (this == window.top),
'typeof window.MY_GLOBAL_CONSTANT': (typeof MY_GLOBAL_CONSTANT),
'typeof window.top.MY_GLOBAL_CONSTANT':
(typeof window.top.MY_GLOBAL_CONSTANT)
});
}, [], 0, onDone, false, testFrameWin);
}

</script>
</body>
</html>
Loading

0 comments on commit 129069f

Please sign in to comment.