| // 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. |
| |
| goog.provide('safaridriver.extension.Server'); |
| |
| goog.require('bot.ErrorCode'); |
| goog.require('bot.response'); |
| goog.require('goog.Disposable'); |
| goog.require('goog.debug.Logger'); |
| goog.require('goog.object'); |
| goog.require('goog.string'); |
| goog.require('safaridriver.Command'); |
| goog.require('safaridriver.CommandRegistry'); |
| goog.require('safaridriver.alert'); |
| goog.require('safaridriver.extension.commands'); |
| goog.require('safaridriver.message.Command'); |
| goog.require('safaridriver.message.Response'); |
| goog.require('webdriver.CommandName'); |
| goog.require('webdriver.promise'); |
| |
| |
| |
| /** |
| * Creates a new WebSocket server that may be used to communicate with a |
| * SafariDriver client. |
| * |
| * <p>Note the name of this class is a bit misleading as it uses a WebSocket |
| * to communicate with the client (i.e., the actual HTTP server is run by the |
| * client). |
| * |
| * @param {!safaridriver.extension.Session} session The session associated with |
| * this server. |
| * @constructor |
| * @implements {webdriver.CommandExecutor} |
| * @extends {goog.Disposable} |
| */ |
| safaridriver.extension.Server = function(session) { |
| goog.base(this); |
| |
| /** |
| * @type {!goog.debug.Logger} |
| * @private |
| */ |
| this.log_ = goog.debug.Logger.getLogger('safaridriver.extension.Server'); |
| |
| /** |
| * @type {!safaridriver.extension.Session} |
| * @private |
| */ |
| this.session_ = session; |
| |
| /** |
| * @type {!webdriver.promise.Deferred} |
| * @private |
| */ |
| this.ready_ = new webdriver.promise.Deferred(); |
| |
| /** |
| * @type {!Array.<function()>} |
| * @private |
| */ |
| this.disposeCallbacks_ = []; |
| }; |
| goog.inherits(safaridriver.extension.Server, goog.Disposable); |
| |
| |
| goog.scope(function() { |
| var CommandName = webdriver.CommandName; |
| var commands = safaridriver.extension.commands; |
| |
| safaridriver.CommandRegistry.getInstance() |
| // By the time a server is accepting commands, it has already allocated a |
| // session, so we can treat NEW_SESSION the same as we do DESCRIBE_SESSION. |
| .defineCommand(CommandName.NEW_SESSION, commands.describeSession) |
| .defineCommand(CommandName.DESCRIBE_SESSION, commands.describeSession) |
| |
| // We can't shutdown Safari from an extension, but we can quietly handle |
| // the command so we don't return an unknown command error. |
| .defineCommand(CommandName.QUIT, goog.nullFunction) |
| |
| .defineCommand(CommandName.CLOSE, commands.closeTab) |
| .defineCommand(CommandName.GET_CURRENT_WINDOW_HANDLE, |
| commands.getWindowHandle) |
| .defineCommand(CommandName.GET_WINDOW_HANDLES, commands.getWindowHandles) |
| .defineCommand(CommandName.GET_CURRENT_URL, commands.sendCommand) |
| .defineCommand(CommandName.GET_TITLE, commands.sendCommand) |
| .defineCommand(CommandName.GET_PAGE_SOURCE, commands.sendCommand) |
| |
| .defineCommand(CommandName.GET, commands.loadUrl) |
| .defineCommand(CommandName.REFRESH, commands.refresh) |
| .defineCommand(CommandName.GO_BACK, commands.sendCommand) |
| .defineCommand(CommandName.GO_FORWARD, commands.sendCommand) |
| |
| .defineCommand(CommandName.ADD_COOKIE, commands.sendCommand) |
| .defineCommand(CommandName.GET_ALL_COOKIES, commands.sendCommand) |
| .defineCommand(CommandName.DELETE_ALL_COOKIES, commands.sendCommand) |
| .defineCommand(CommandName.DELETE_COOKIE, commands.sendCommand) |
| |
| .defineCommand(CommandName.IMPLICITLY_WAIT, commands.implicitlyWait) |
| .defineCommand(CommandName.FIND_ELEMENT, commands.findElement) |
| .defineCommand(CommandName.FIND_ELEMENTS, commands.findElement) |
| .defineCommand(CommandName.FIND_CHILD_ELEMENT, commands.findElement) |
| .defineCommand(CommandName.FIND_CHILD_ELEMENTS, commands.findElement) |
| .defineCommand(CommandName.GET_ACTIVE_ELEMENT, commands.sendCommand) |
| |
| .defineCommand(CommandName.CLEAR_ELEMENT, commands.sendCommand) |
| .defineCommand(CommandName.CLICK_ELEMENT, commands.sendCommand) |
| .defineCommand(CommandName.SUBMIT_ELEMENT, commands.sendCommand) |
| .defineCommand(CommandName.GET_ELEMENT_TEXT, commands.sendCommand) |
| .defineCommand(CommandName.GET_ELEMENT_TAG_NAME, commands.sendCommand) |
| .defineCommand(CommandName.IS_ELEMENT_SELECTED, commands.sendCommand) |
| .defineCommand(CommandName.IS_ELEMENT_ENABLED, commands.sendCommand) |
| .defineCommand(CommandName.IS_ELEMENT_DISPLAYED, commands.sendCommand) |
| .defineCommand(CommandName.GET_ELEMENT_LOCATION, commands.sendCommand) |
| .defineCommand(CommandName.GET_ELEMENT_LOCATION_IN_VIEW, |
| commands.sendCommand) |
| .defineCommand(CommandName.GET_ELEMENT_SIZE, commands.sendCommand) |
| .defineCommand(CommandName.GET_ELEMENT_ATTRIBUTE, commands.sendCommand) |
| .defineCommand(CommandName.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, |
| commands.sendCommand) |
| .defineCommand(CommandName.ELEMENT_EQUALS, commands.sendCommand) |
| .defineCommand(CommandName.SEND_KEYS_TO_ELEMENT, commands.sendCommand) |
| |
| .defineCommand(CommandName.CLICK, commands.sendCommand) |
| .defineCommand(CommandName.DOUBLE_CLICK, commands.sendCommand) |
| .defineCommand(CommandName.MOUSE_DOWN, commands.sendCommand) |
| .defineCommand(CommandName.MOUSE_UP, commands.sendCommand) |
| .defineCommand(CommandName.MOVE_TO, commands.sendCommand) |
| .defineCommand(CommandName.SEND_KEYS_TO_ACTIVE_ELEMENT, |
| commands.sendCommand) |
| |
| .defineCommand(CommandName.SWITCH_TO_FRAME, commands.sendCommand) |
| .defineCommand(CommandName.SWITCH_TO_WINDOW, commands.switchToWindow) |
| .defineCommand(CommandName.SET_WINDOW_SIZE, commands.sendWindowCommand) |
| .defineCommand(CommandName.SET_WINDOW_POSITION, commands.sendWindowCommand) |
| .defineCommand(CommandName.GET_WINDOW_SIZE, commands.sendWindowCommand) |
| .defineCommand(CommandName.GET_WINDOW_POSITION, commands.sendWindowCommand) |
| .defineCommand(CommandName.MAXIMIZE_WINDOW, commands.sendWindowCommand) |
| |
| .defineCommand(CommandName.EXECUTE_SCRIPT, commands.sendCommand) |
| .defineCommand(CommandName.EXECUTE_ASYNC_SCRIPT, |
| commands.executeAsyncScript) |
| .defineCommand(CommandName.SET_SCRIPT_TIMEOUT, commands.setScriptTimeout) |
| |
| .defineCommand(CommandName.SCREENSHOT, commands.takeScreenshot) |
| |
| .defineCommand(CommandName.ACCEPT_ALERT, commands.handleNoAlertsPresent) |
| .defineCommand(CommandName.DISMISS_ALERT, commands.handleNoAlertsPresent) |
| .defineCommand(CommandName.GET_ALERT_TEXT, commands.handleNoAlertsPresent) |
| .defineCommand(CommandName.SET_ALERT_TEXT, commands.handleNoAlertsPresent) |
| ; |
| }); // goog.scope |
| |
| |
| /** |
| * The WebSocket used by this instance, lazily initialized in {@link #connect}. |
| * @type {WebSocket} |
| * @private |
| */ |
| safaridriver.extension.Server.prototype.webSocket_ = null; |
| |
| |
| /** @override */ |
| safaridriver.extension.Server.prototype.disposeInternal = function() { |
| this.logMessage_('Disposing of server', goog.debug.Logger.Level.FINE); |
| |
| if (this.webSocket_) { |
| if (this.ready_.isPending()) { |
| this.ready_.cancel(Error('Server has been disposed')); |
| } |
| this.webSocket_.close(); |
| } |
| |
| while (this.disposeCallbacks_.length) { |
| var callback = this.disposeCallbacks_.shift(); |
| callback(); |
| } |
| |
| delete this.disposeCallbacks_; |
| delete this.log_; |
| delete this.session_; |
| delete this.ready_; |
| delete this.webSocket_; |
| |
| goog.base(this, 'disposeInternal'); |
| }; |
| |
| |
| /** @return {!safaridriver.extension.Session} The session for this server. */ |
| safaridriver.extension.Server.prototype.getSession = function() { |
| return this.session_; |
| }; |
| |
| |
| /** |
| * Registers a callback to be called when this server is disposed. If the server |
| * has already been disposed, the callback will be invoked immediately. |
| * @param {function()} fn The callback function. |
| */ |
| safaridriver.extension.Server.prototype.onDispose = function(fn) { |
| if (this.isDisposed()) { |
| fn(); |
| } else { |
| this.disposeCallbacks_.push(fn); |
| } |
| }; |
| |
| |
| /** |
| * Connects to a server. |
| * @param {string} url URL to connect to. |
| * @return {!webdriver.promise.Promise} A promise that will be resolved when |
| * this server has connected. |
| * @throws {Error} If this server has already connected to a server. |
| */ |
| safaridriver.extension.Server.prototype.connect = function(url) { |
| if (this.isDisposed()) { |
| throw Error('This server has been disposed!'); |
| } |
| |
| if (this.webSocket_) { |
| throw Error('This server has already connected!'); |
| } |
| |
| this.logMessage_('Connecting to ' + url); |
| this.webSocket_ = new WebSocket(url); |
| |
| // Register the event handlers. Note that it is not possible for these |
| // callbacks to be missed because it is registered after the web socket is |
| // instantiated. Because of the synchronous nature of JavaScript, this code |
| // will execute before the browser creates the resource and makes any calls |
| // to these callbacks. |
| this.webSocket_.onopen = goog.bind(this.onOpen_, this); |
| this.webSocket_.onclose = goog.bind(this.onClose_, this); |
| this.webSocket_.onmessage = goog.bind(this.onMessage_, this); |
| this.webSocket_.onerror = goog.bind(this.onError_, this); |
| |
| return this.ready_.promise; |
| }; |
| |
| |
| /** |
| * Executes a single command once all those received before it have completed. |
| * @param {!webdriver.Command} command The command to execute. |
| * @param {function(Error, !bot.response.ResponseObject=)=} opt_callback A |
| * callback function for adherence to the {@link webdriver.CommandExecutor} |
| * interface. |
| * @return {!webdriver.promise.Promise} A promise that will be resolved with a |
| * {@link bot.response.ResponseObject} object once the command has |
| * completed. |
| */ |
| safaridriver.extension.Server.prototype.execute = function( |
| command, opt_callback) { |
| // Normally command will be an instanceof safaridriver.Command, but it will be |
| // a standard webdriver.Command if it came from |
| // safaridriver.extension.driver (via the extension builder REPL). |
| command = new safaridriver.Command(goog.string.getRandomString(), |
| command.getName(), command.getParameters()); |
| |
| this.logMessage_('Scheduling command: ' + command.getName(), |
| goog.debug.Logger.Level.FINER); |
| var description = this.session_.getId() + '::' + command.getName(); |
| var fn = goog.bind(this.executeCommand_, this, command); |
| var result = webdriver.promise.Application.getInstance(). |
| schedule(description, fn). |
| then(bot.response.createResponse, bot.response.createErrorResponse). |
| addBoth(function(response) { |
| this.session_.setCurrentCommand(null); |
| return response; |
| }, this); |
| |
| // If we were given a callback, massage the result to fit the |
| // webdriver.CommandExecutor contract. |
| if (opt_callback) { |
| result.then(bot.response.checkResponse). |
| then(goog.partial(opt_callback, null), opt_callback); |
| } |
| |
| return result; |
| }; |
| |
| |
| /** |
| * @param {!safaridriver.Command} command The command to execute. |
| * @return {*} The command result. |
| * @private |
| */ |
| safaridriver.extension.Server.prototype.executeCommand_ = function(command) { |
| this.logMessage_('Executing command: ' + command.getName()); |
| |
| var alertText = this.session_.getUnhandledAlertText(); |
| if (!goog.isNull(alertText)) { |
| this.session_.setUnhandledAlertText(null); |
| return safaridriver.alert.createResponse(alertText); |
| } |
| |
| this.session_.setCurrentCommand(command); |
| |
| var registry = safaridriver.CommandRegistry.getInstance(); |
| return registry.execute(command, this.session_); |
| }; |
| |
| |
| /** |
| * @param {string} message The message to log. |
| * @param {goog.debug.Logger.Level=} opt_level The level to log the message at; |
| * Defaults to INFO. |
| * @private |
| */ |
| safaridriver.extension.Server.prototype.logMessage_ = function(message, |
| opt_level) { |
| this.log_.log(opt_level || goog.debug.Logger.Level.INFO, |
| '[' + this.session_.getId() + '] ' + message); |
| }; |
| |
| |
| /** |
| * Called when the WebSocket connection is opened. |
| * @private |
| */ |
| safaridriver.extension.Server.prototype.onOpen_ = function() { |
| this.logMessage_('WebSocket connection established.'); |
| if (!this.isDisposed() && this.ready_.isPending()) { |
| this.ready_.resolve(); |
| } |
| }; |
| |
| |
| /** |
| * Called when an attempt to open the WebSocket fails or there is a connection |
| * failure after a successful connection has been established. Triggers the |
| * disposable of this server. |
| * @private |
| */ |
| safaridriver.extension.Server.prototype.onClose_ = function() { |
| this.logMessage_('WebSocket connection was closed.', |
| goog.debug.Logger.Level.WARNING); |
| if (!this.isDisposed()) { |
| if (this.ready_.isPending()) { |
| this.ready_.reject(Error('Failed to connect')); |
| } |
| this.dispose(); |
| } |
| }; |
| |
| |
| /** |
| * Called when there is a communication error with the WebSocket. |
| * @param {!MessageEvent} event The error event. |
| * @private |
| */ |
| safaridriver.extension.Server.prototype.onError_ = function(event) { |
| this.logMessage_('There was an error in the WebSocket: ' + event.data, |
| goog.debug.Logger.Level.SEVERE); |
| }; |
| |
| |
| /** |
| * Called when the WebSocket receives a message. |
| * @param {!MessageEvent} event The message event. |
| * @private |
| */ |
| safaridriver.extension.Server.prototype.onMessage_ = function(event) { |
| this.logMessage_('Received a message: ' + event.data); |
| |
| try { |
| var message = safaridriver.message.fromEvent(event); |
| if (!message.isType(safaridriver.message.Command.TYPE)) { |
| throw Error('Not a command message: ' + message); |
| } |
| } catch (ex) { |
| this.send_(null, bot.response.createErrorResponse(ex)); |
| return; |
| } |
| |
| var command = message.getCommand(); |
| |
| this.execute(command). |
| addErrback(bot.response.createErrorResponse). |
| addCallback(function(response) { |
| this.send_(command, response); |
| }, this); |
| }; |
| |
| |
| /** |
| * Sends a response to the client. |
| * @param {safaridriver.Command} command The command this is a response to, or |
| * {@code null} if the response indicates a parse error with the command. |
| * @param {!bot.response.ResponseObject} response The response to send. |
| * @private |
| */ |
| safaridriver.extension.Server.prototype.send_ = function(command, response) { |
| var id = command ? command.id : ''; |
| var message = new safaridriver.message.Response(id, response); |
| |
| var str = message.toString(); |
| |
| this.logMessage_('Sending response: ' + str); |
| if (!command && response['status'] === bot.ErrorCode.SUCCESS) { |
| this.logMessage_('Sending success response with a null command: ' + str, |
| goog.debug.Logger.Level.WARNING); |
| } |
| |
| this.webSocket_.send(str); |
| }; |