blob: ad6a03ec7df464d74f8f3d179d76bd94076fbe4c [file] [log] [blame]
// Copyright 2012 Selenium committers
// Copyright 2012 Software Freedom Conservancy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Defines utilities for exchanging messages between the
* sandboxed SafariDriver injected script and its corresponding content page.
*/
goog.provide('safaridriver.inject.page');
goog.require('bot.Error');
goog.require('bot.ErrorCode');
goog.require('bot.response');
goog.require('goog.array');
goog.require('goog.debug.Logger');
goog.require('safaridriver.console');
goog.require('safaridriver.inject.Encoder');
goog.require('safaridriver.inject.message');
goog.require('safaridriver.message');
goog.require('safaridriver.message.Alert');
goog.require('safaridriver.message.Command');
goog.require('safaridriver.message.Load');
goog.require('safaridriver.message.MessageTarget');
goog.require('safaridriver.message.Response');
goog.require('webdriver.CommandName');
goog.require('webdriver.atoms.element');
goog.require('webdriver.promise');
/**
* @type {!goog.debug.Logger}
* @const
* @private
*/
safaridriver.inject.page.LOG_ = goog.debug.Logger.getLogger(
'safaridriver.inject.page');
/**
* @type {!safaridriver.inject.Encoder}
* @private
*/
safaridriver.inject.page.encoder_;
/**
* Initializes this module for exchanging messages with the primary injected
* script driven by {@link safaridriver.inject.Tab}.
*/
safaridriver.inject.page.init = function() {
safaridriver.console.init();
safaridriver.inject.page.LOG_.info(
'Loaded page script for ' + window.location);
var messageTarget = new safaridriver.message.MessageTarget(window, true);
messageTarget.setLogger(safaridriver.inject.page.LOG_);
messageTarget.on(safaridriver.message.Command.TYPE,
safaridriver.inject.page.onCommand_);
safaridriver.inject.page.encoder_ =
new safaridriver.inject.Encoder(messageTarget);
var message = new safaridriver.message.Load(window !== window.top);
safaridriver.inject.page.LOG_.info('Sending ' + message);
message.send(window);
wrapDialogFunction('alert', safaridriver.inject.page.wrappedAlert_);
wrapDialogFunction('confirm', safaridriver.inject.page.wrappedConfirm_);
wrapDialogFunction('prompt', safaridriver.inject.page.wrappedPrompt_);
safaridriver.dom.call(window, 'addEventListener', 'beforeunload',
safaridriver.inject.page.onBeforeUnload_, true);
function wrapDialogFunction(name, newFn) {
var oldFn = window[name];
window[name] = newFn;
window.constructor.prototype[name] = newFn;
window[name].toString = function() {
return oldFn.toString();
};
}
};
goog.exportSymbol('init', safaridriver.inject.page.init);
/**
* The native dialog functions.
* @enum {{name: string, fn: !Function}}
* @private
*/
safaridriver.inject.page.NativeDialog_ = {
ALERT: {name: 'alert', fn: window.alert},
BEFOREUNLOAD: {name: 'beforeunload', fn: goog.nullFunction},
CONFIRM: {name: 'confirm', fn: window.confirm},
PROMPT: {name: 'prompt', fn: window.prompt}
};
/**
* Wraps window.alert.
* @param {...*} var_args The alert arguments.
* @this {Window}
* @private
*/
safaridriver.inject.page.wrappedAlert_ = function(var_args) {
safaridriver.inject.page.sendAlert_(
safaridriver.inject.page.NativeDialog_.ALERT,
// Closure's extern definition for window.alert says it takes var_args,
// but Safari's only accepts a single argument.
arguments[0]);
};
/**
* Wraps window.confirm.
* @param {*} arg The confirm argument.
* @return {boolean} The confirmation response.
* @this {Window}
* @private
*/
safaridriver.inject.page.wrappedConfirm_ = function(arg) {
return /** @type {boolean} */ (safaridriver.inject.page.sendAlert_(
safaridriver.inject.page.NativeDialog_.CONFIRM, arg));
};
/**
* Wraps window.prompt.
* @param {*} arg The prompt argument.
* @return {?string} The prompt response.
* @this {Window}
* @private
*/
safaridriver.inject.page.wrappedPrompt_ = function(arg) {
return /** @type {?string} */ (safaridriver.inject.page.sendAlert_(
safaridriver.inject.page.NativeDialog_.PROMPT, arg));
};
/**
* Window beforeunload event listener that intercepts calls to user defined
* window.onbeforeunload functions.
* @param {Event} event The beforeunload event.
* @private
*/
safaridriver.inject.page.onBeforeUnload_ = function(event) {
safaridriver.inject.page.sendAlert_(
safaridriver.inject.page.NativeDialog_.BEFOREUNLOAD, event);
};
/**
* @param {!safaridriver.inject.page.NativeDialog_} dialog The dialog
* descriptor.
* @param {...*} var_args The alert function dialogs.
* @return {?(boolean|string|undefined)} The alert response.
* @private
*/
safaridriver.inject.page.sendAlert_ = function(dialog, var_args) {
var args = goog.array.slice(arguments, 1);
var alertText = args[0] + '';
var blocksUiThread = true;
var nativeFn = dialog.fn;
if (dialog === safaridriver.inject.page.NativeDialog_.BEFOREUNLOAD) {
// The user onbeforeunload has not actually been called, so we're not at
// risk of blocking the UI thread yet. We just need to query if it's
// possible for it to block.
blocksUiThread = false;
nativeFn = window.onbeforeunload;
if (!nativeFn) {
// window.onbeforeunload not set, nothing more for us to do.
return null;
}
}
safaridriver.inject.page.LOG_.info('Sending alert notification; ' +
'type: ' + dialog.name + ', text: ' + alertText);
var message = new safaridriver.message.Alert(alertText, blocksUiThread);
var ignoreAlert = message.sendSync(window);
if (ignoreAlert == '1') {
if (dialog !== safaridriver.inject.page.NativeDialog_.BEFOREUNLOAD) {
safaridriver.inject.page.LOG_.info('Invoking native alert');
return nativeFn.apply(window, args);
}
return null; // Return and let onbeforeunload be called as usual.
}
safaridriver.inject.page.LOG_.info('Dismissing unexpected alert');
var response;
switch (dialog.name) {
case safaridriver.inject.page.NativeDialog_.BEFOREUNLOAD.name:
if (nativeFn) {
// Call the onbeforeunload handler so user logic executes, but clear
// the real deal so the dialog does not popup and hang the UI thread.
var ret = nativeFn();
window.onbeforeunload = null;
if (goog.isDefAndNotNull(ret)) {
// Ok, the user's onbeforeunload would block the UI thread so we
// need to let the extension know about it.
blocksUiThread = true;
new safaridriver.message.Alert(ret + '', blocksUiThread).
sendSync(window);
}
}
break;
case safaridriver.inject.page.NativeDialog_.CONFIRM.name:
response = false;
break;
case safaridriver.inject.page.NativeDialog_.PROMPT.name:
response = null;
break;
}
return response;
};
/**
* Handles command messages from the injected script.
* @param {!safaridriver.message.Command} message The command message.
* @param {!MessageEvent} e The original message event.
* @throws {Error} If the command is not supported by this script.
* @private
*/
safaridriver.inject.page.onCommand_ = function(message, e) {
if (message.isSameOrigin() || !safaridriver.inject.message.isFromSelf(e)) {
return;
}
var command = message.getCommand();
var response = new webdriver.promise.Deferred();
// When the response is resolved, we want to wrap it up in a message and
// send it back to the injected script. This does all that.
response.then(function(value) {
var encodedValue = safaridriver.inject.page.encoder_.encode(value);
// If the command result contains any DOM elements from another
// document, the encoded value will contain promises that will resolve
// once the owner documents have encoded the elements. Therefore, we
// must wait for those to resolve.
return webdriver.promise.fullyResolved(encodedValue);
}).then(bot.response.createResponse, bot.response.createErrorResponse).
then(function(response) {
var responseMessage = new safaridriver.message.Response(
command.getId(), response);
safaridriver.inject.page.LOG_.info(
'Sending ' + command.getName() + ' response: ' + responseMessage);
responseMessage.send(window);
});
var handlerFn;
switch (command.getName()) {
case webdriver.CommandName.EXECUTE_ASYNC_SCRIPT:
handlerFn = safaridriver.inject.page.executeAsyncScript_;
break;
case webdriver.CommandName.EXECUTE_SCRIPT:
handlerFn = safaridriver.inject.page.executeScript_;
break;
case webdriver.CommandName.SEND_KEYS_TO_ELEMENT:
handlerFn = safaridriver.inject.page.sendKeysToElement_;
break;
}
if (!handlerFn) {
response.reject(Error('Unknown command: ' + command.getName()));
return;
}
try {
webdriver.promise.asap(handlerFn(command),
response.resolve, response.reject);
} catch (ex) {
response.reject(ex);
}
};
/**
* @param {!Function} fn The function to execute.
* @param {!Array.<*>} args Function arguments.
* @return {*} The function result.
* @throws {Error} If unable to decode the function arguments.
* @private
*/
safaridriver.inject.page.execute_ = function(fn, args) {
args = /** @type {!Array} */ (safaridriver.inject.page.encoder_.decode(args));
return fn.apply(window, args);
};
/**
* @param {!safaridriver.Command} command The command to execute.
* @private
*/
safaridriver.inject.page.sendKeysToElement_ = function(command) {
safaridriver.inject.page.execute_(webdriver.atoms.element.type, [
command.getParameter('id'),
command.getParameter('value')
]);
};
/**
* Handles an executeScript command.
* @param {!safaridriver.Command} command The command to execute.
* @return {*} The script result.
* @private
*/
safaridriver.inject.page.executeScript_ = function(command) {
// TODO: clean-up bot.inject.executeScript so it doesn't pull in so many
// extra dependencies.
var fn = new Function(command.getParameter('script'));
var args = (/** @type {!Array.<*>} */command.getParameter('args'));
return safaridriver.inject.page.execute_(fn, args);
};
/**
* Handles an executeAsyncScript command.
* @param {!safaridriver.Command} command The command to execute.
* @return {!webdriver.promise.Promise} A promise that will be resolved with
* the script result.
* @private
*/
safaridriver.inject.page.executeAsyncScript_ = function(command) {
var response = new webdriver.promise.Deferred();
var script = /** @type {string} */ (command.getParameter('script'));
var scriptFn = new Function(script);
var args = command.getParameter('args');
args = /** @type {!Array} */ (safaridriver.inject.page.encoder_.decode(args));
// The last argument for an async script is the callback that triggers the
// response.
args.push(function(value) {
safaridriver.dom.call(window, 'clearTimeout', timeoutId);
if (response.isPending()) {
response.resolve(value);
}
});
var startTime = goog.now();
scriptFn.apply(window, 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:
// var scriptFn = function(callback) {
// setTimeout(callback, 0);
// };
var timeout = /** @type {number} */ (command.getParameter('timeout'));
var timeoutId = safaridriver.dom.call(window, 'setTimeout', function() {
if (response.isPending()) {
response.reject(new bot.Error(bot.ErrorCode.SCRIPT_TIMEOUT,
'Timed out waiting for an asynchronous script result after ' +
(goog.now() - startTime) + ' ms'));
}
}, Math.max(0, timeout));
return response.promise;
};