From 027aa93b18a2d4835c5d0e7de18c9a04874fbe9e Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Sat, 15 Dec 2012 19:00:23 +0000 Subject: [PATCH] Importing GhostDriver version `1.0.1`. Changes are documented here: https://github.com/detro/ghostdriver/issues?labels=1.0.1&state=closed. https://github.com/detro/ghostdriver/issues/130 --- src/ghostdriver/lastupdate | 11 +- src/ghostdriver/main.js | 2 +- .../session_request_handler.js | 42 ++++-- .../shutdown_request_handler.js | 6 +- .../webelement_request_handler.js | 75 ++++++++++- src/ghostdriver/session.js | 125 +++++++++++++++--- src/ghostdriver/webelementlocator.js | 4 +- 7 files changed, 219 insertions(+), 46 deletions(-) diff --git a/src/ghostdriver/lastupdate b/src/ghostdriver/lastupdate index 14305590..129e2a46 100644 --- a/src/ghostdriver/lastupdate +++ b/src/ghostdriver/lastupdate @@ -1,10 +1,7 @@ -2012-11-25 20:54:35 +2012-12-15 18:59:39 -commit 54876580d0a3886e42850804b03184adbd899d65 (HEAD, tag: refs/tags/1.0.0, refs/remotes/origin/master, refs/remotes/origin/HEAD, refs/heads/master) +commit 2676fd4e6410202ec439a235316451ea2c633681 (HEAD, tag: refs/tags/1.0.1, refs/heads/master) Author: Ivan De Marino -Date: Sun Nov 25 19:05:15 2012 +0000 +Date: Sat Dec 15 18:58:34 2012 +0000 - Updated export script. - - 1. Creates the `README.md` to inform PhantomJS developers that the code is automatically exported from GhostDriver - 2. Adds `--decorate=full` to the line where we populate the `lastupdate` file + Preparing GhostDriver version `1.0.1`. diff --git a/src/ghostdriver/main.js b/src/ghostdriver/main.js index a7e49659..142a17ad 100644 --- a/src/ghostdriver/main.js +++ b/src/ghostdriver/main.js @@ -30,7 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. var ghostdriver = { system : require('system'), hub : require('./hub_register'), - version : "1.0.0" + version : "1.0.1" }, server = require('webserver').create(), router, diff --git a/src/ghostdriver/request_handlers/session_request_handler.js b/src/ghostdriver/request_handlers/session_request_handler.js index 13bb1606..a251c2b3 100644 --- a/src/ghostdriver/request_handlers/session_request_handler.js +++ b/src/ghostdriver/request_handlers/session_request_handler.js @@ -345,7 +345,7 @@ ghostdriver.SessionReqHand = function(session) { var postObj = JSON.parse(req.post), result, timer, - scriptTimeout = _session.getTimeout(_session.timeoutNames().SCRIPT), + scriptTimeout = _session.getScriptTimeout(), timedOut = false; if (typeof(postObj) === "object" && postObj.script && postObj.args) { @@ -355,7 +355,7 @@ ghostdriver.SessionReqHand = function(session) { timedOut = true; _errors.handleFailedCommandEH( _errors.FAILED_CMD_STATUS.TIMEOUT, - "Script didn't return within "+scriptTimeout+"ms", + "Script didn't return within " + scriptTimeout + "ms", req, res, _session, @@ -399,14 +399,21 @@ ghostdriver.SessionReqHand = function(session) { "}", postObj.script, postObj.args, - _session.getTimeout(_session.timeoutNames().ASYNC_SCRIPT)); + _session.getAsyncScriptTimeout()); } else { throw _errors.createInvalidReqMissingCommandParameterEH(req); } }, _getWindowHandle = function (req, res) { - var handle = _session.getCurrentWindowHandle(); + var handle; + + // Initialize the Current Window (we need at least that) + _session.initCurrentWindowIfNull(); + + // Get current window handle + handle = _session.getCurrentWindowHandle(); + if (handle !== null) { res.success(_session.getId(), handle); } else { @@ -420,6 +427,8 @@ ghostdriver.SessionReqHand = function(session) { }, _getWindowHandles = function(req, res) { + // Initialize the Current Window (we need at least that) + _session.initCurrentWindowIfNull(); res.success(_session.getId(), _session.getWindowHandles()); }, @@ -460,9 +469,7 @@ ghostdriver.SessionReqHand = function(session) { // Request timed out _errors.handleFailedCommandEH( _errors.FAILED_CMD_STATUS.TIMEOUT, - "URL '" + postObj.url + "' didn't load within " + - _session.getTimeout(_session.timeoutNames().PAGE_LOAD) + - "ms", + "URL '" + postObj.url + "' didn't load within " + _session.getPageLoadTimeout() + "ms", req, res, _session, @@ -478,13 +485,28 @@ ghostdriver.SessionReqHand = function(session) { // Normalize the call: the "type" is read from the URL, not a POST parameter if (req.urlParsed.file === _const.IMPLICIT_WAIT) { - postObj["type"] = _session.timeoutNames().IMPLICIT; + postObj["type"] = _session.timeoutNames.IMPLICIT; } else if (req.urlParsed.file === _const.ASYNC_SCRIPT) { - postObj["type"] = _session.timeoutNames().ASYNC_SCRIPT; + postObj["type"] = _session.timeoutNames.ASYNC_SCRIPT; } if (typeof(postObj["type"]) !== "undefined" && typeof(postObj["ms"]) !== "undefined") { - _session.setTimeout(postObj["type"], postObj["ms"]); + // Set the right timeout on the Session + switch(postObj["type"]) { + case _session.timeoutNames.SCRIPT: + _session.setScriptTimeout(postObj["ms"]); + break; + case _session.timeoutNames.ASYNC_SCRIPT: + _session.setAsyncScriptTimeout(postObj["ms"]); + break; + case _session.timeoutNames.IMPLICIT: + _session.setImplicitTimeout(postObj["ms"]); + break; + case _session.timeoutNames.PAGE_LOAD: + _session.setPageLoadTimeout(postObj["ms"]); + break; + } + res.success(_session.getId()); } else { throw _errors.createInvalidReqMissingCommandParameterEH(req); diff --git a/src/ghostdriver/request_handlers/shutdown_request_handler.js b/src/ghostdriver/request_handlers/shutdown_request_handler.js index 49d7625e..1f85292c 100644 --- a/src/ghostdriver/request_handlers/shutdown_request_handler.js +++ b/src/ghostdriver/request_handlers/shutdown_request_handler.js @@ -34,10 +34,10 @@ ghostdriver.ShutdownReqHand = function() { _protoParent = ghostdriver.ShutdownReqHand.prototype, _handle = function(req, res) { - _protoParent.handle.call(this, req, res); + _protoParent.handle.call(this, req, res); - if (req.method === "GET" && req.urlParsed.file === "shutdown") { - //res.success(null, null); + // Any HTTP Request Method will be accepted for this command. Some drivers like HEAD for example... + if (req.urlParsed.file === "shutdown") { res.statusCode = 200; res.setHeader("Content-Type", "text/html;charset=UTF-8"); res.setHeader("Content-Length", 36); diff --git a/src/ghostdriver/request_handlers/webelement_request_handler.js b/src/ghostdriver/request_handlers/webelement_request_handler.js index 5b7a1007..5f8e94fb 100644 --- a/src/ghostdriver/request_handlers/webelement_request_handler.js +++ b/src/ghostdriver/request_handlers/webelement_request_handler.js @@ -348,7 +348,8 @@ ghostdriver.WebElementReqHand = function(idOrElement, session) { require("./webdriver_atoms.js").get("execute_script"), "return arguments[0].isSameNode(arguments[1]);", [_getJSON(), _getJSON(req.urlParsed.file)]); - res.success(_session.getId(), result); + + res.respondBasedOnResult(_session, req, result); return; } @@ -378,7 +379,7 @@ ghostdriver.WebElementReqHand = function(idOrElement, session) { } else { _errors.handleFailedCommandEH( _errors.FAILED_CMD_STATUS.UNKNOWN_ERROR, - "Submit succeded but Load Failed", + "Submit succeeded but Load Failed", req, res, _session, @@ -401,12 +402,72 @@ ghostdriver.WebElementReqHand = function(idOrElement, session) { }); }, - _postClickCommand = function(req, res) { - var result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( - require("./webdriver_atoms.js").get("click"), - _getJSON()); + _canCausePageLoadOnClick = function(currWindow) { + var tagName = _getTagName(currWindow).toLowerCase(), + href = _getAttribute(currWindow, "href"), + type = _getAttribute(currWindow, "type").toLowerCase(); - res.respondBasedOnResult(_session, req, result); + // Return "true" if it's an element that "can cause a page load if clicked" + if (tagName === "a" && !!href && href.length > 0) { + return true; + } + if (tagName === "input" && type === "submit") { + return true; + } + return false; + }, + + _postClickCommand = function(req, res) { + var currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req), + clickRes, + abortCallback = false; + + if (_canCausePageLoadOnClick(currWindow)) { + // Clicking on Current Element can cause a page load, hence we need to wait for it to happen + currWindow.execFuncAndWaitForLoad(function() { + // do the click + clickRes = currWindow.evaluate(require("./webdriver_atoms.js").get("click"), _getJSON()); + + // If Click was NOT positive, status will be set to something else than '0' + clickRes = JSON.parse(clickRes); + if (clickRes && clickRes.status !== 0) { + abortCallback = true; //< handling the error here + res.respondBasedOnResult(_session, req, clickRes); + } + }, function(status) { //< onLoadFinished + // Report about the Load, only if it was not already handled + if (!abortCallback) { + if (status === "success") { + res.success(_session.getId()); + } else { + _errors.handleFailedCommandEH( + _errors.FAILED_CMD_STATUS.UNKNOWN_ERROR, + "Click succeeded but Load Failed", + req, + res, + _session, + "WebElementReqHand"); + } + } + }, function() { + if (arguments.length === 0) { //< onTimeout + // onclick didn't bubble up, but we should still return success + res.success(_session.getId()); + } else { //< onError + _errors.handleFailedCommandEH( + _errors.FAILED_CMD_STATUS.UNKNOWN_ERROR, + "Click failed: " + arguments[0], + req, + res, + _session, + "WebElementReqHand"); + } + }); + } else { + // By default, clicking on this element can't cause a Page Load: we are done after having clicked + clickRes = currWindow.evaluate(require("./webdriver_atoms.js").get("click"), _getJSON()); + res.respondBasedOnResult(_session, req, clickRes); + } }, _getSelectedCommand = function(req, res) { diff --git a/src/ghostdriver/session.js b/src/ghostdriver/session.js index bcfe846f..fc460b35 100644 --- a/src/ghostdriver/session.js +++ b/src/ghostdriver/session.js @@ -76,11 +76,15 @@ ghostdriver.Session = function(desiredCapabilities) { _defaultCapabilities.proxy : desiredCapabilities.proxy }, + // NOTE: This value is needed for Timeouts Upper-bound limit. + // "setTimeout/setInterval" accept only 32 bit integers, even though Number are all Doubles (go figure!) + // Interesting details here: {@link http://stackoverflow.com/a/4995054}. + _max32bitInt = Math.pow(2, 31) -1, //< Max 32bit Int _timeouts = { - "script" : 500, //< 0.5s - "async script" : 5000, //< 5s + "script" : _max32bitInt, + "async script" : _max32bitInt, "implicit" : 0, //< 0s - "page load" : 10000 //< 10s + "page load" : _max32bitInt, }, _const = { TIMEOUT_NAMES : { @@ -95,7 +99,23 @@ ghostdriver.Session = function(desiredCapabilities) { _currentWindowHandle = null, _id = require("./third_party/uuid.js").v1(), _inputs = ghostdriver.Inputs(), + _capsPageSettingsPref = "phantomjs.page.settings.", + _pageSettings = {}, + k, settingKey; + // Searching for `phantomjs.settings.*` in the Desired Capabilities and merging with the Negotiated Capabilities + // Possible values: @see https://github.com/ariya/phantomjs/wiki/API-Reference#wiki-webpage-settings. + for (k in desiredCapabilities) { + if (k.indexOf(_capsPageSettingsPref) === 0) { + settingKey = k.substring(_capsPageSettingsPref.length); + if (settingKey.length > 0) { + _negotiatedCapabilities[k] = desiredCapabilities[k]; + _pageSettings[settingKey] = desiredCapabilities[k]; + } + } + } + + var /** * Executes a function and waits for Load to happen. * @@ -110,6 +130,7 @@ ghostdriver.Session = function(desiredCapabilities) { var args = Array.prototype.splice.call(arguments, 0), timer, loadingNewPage = false, + pageLoadNotTriggered = true, thisPage = this; // Normalize "execTypeOpt" value @@ -139,10 +160,12 @@ ghostdriver.Session = function(desiredCapabilities) { // callback. this.setOneShotCallback("onLoadStarted", function () { // console.log("onLoadStarted"); + pageLoadNotTriggered = false; loadingNewPage = true; }); this.setOneShotCallback("onUrlChanged", function () { // console.log("onUrlChanged"); + pageLoadNotTriggered = false; // If "not loading a new page" it's just a fragment change // and we should call "onLoadFunc()" @@ -168,6 +191,7 @@ ghostdriver.Session = function(desiredCapabilities) { // message += " in " + item["function"]; // console.log(" " + message); // }); + pageLoadNotTriggered = false; thisPage.stop(); //< stop the page from loading clearTimeout(timer); @@ -177,12 +201,13 @@ ghostdriver.Session = function(desiredCapabilities) { }); // Starting timer + // console.log("Setting timer to: " + _getPageLoadTimeout()); timer = setTimeout(function() { thisPage.stop(); //< stop the page from loading thisPage.resetOneShotCallbacks(); onErrorFunc.apply(thisPage, arguments); - }, _getTimeout(_const.TIMEOUT_NAMES.PAGE_LOAD)); + }, _getPageLoadTimeout()); // We are ready to execute if (execTypeOpt === "eval") { @@ -192,6 +217,15 @@ ghostdriver.Session = function(desiredCapabilities) { // "Apply" the provided function func.apply(this, args); } + + // In case a Page Load is not triggered at all (within 0.5s), we assume it's done and move on + setTimeout(function() { + if (pageLoadNotTriggered) { + clearTimeout(timer); + thisPage.resetOneShotCallbacks(); + onLoadFunc.call(thisPage, "success"); + } + }, 500); }, _oneShotCallbackFactory = function(page, callbackName) { @@ -239,6 +273,8 @@ ghostdriver.Session = function(desiredCapabilities) { }, _decorateNewWindow = function(page) { + var k; + // Decorating: // 0. Pages lifetime will be managed by Driver, not the pages page.ownsPages = false; @@ -258,8 +294,16 @@ ghostdriver.Session = function(desiredCapabilities) { page.onPageCreated = _addNewPage; // 5. Remove every closing page page.onClosing = _deleteClosingPage; + // 6. Applying Page settings received via capabilities + for (k in _pageSettings) { + // Apply setting only if really supported by PhantomJS + if (p.settings.hasOwnProperty(k)) { + page.settings[k] = _pageSettings[k]; + } + } // page.onConsoleMessage = function(msg) { console.log(msg); }; + // console.log("New Window/Page settings: " + JSON.stringify(page.settings, null, " ")); return page; }, @@ -284,14 +328,21 @@ ghostdriver.Session = function(desiredCapabilities) { return page; }, - _getCurrentWindow = function() { - var page = null; + _initCurrentWindowIfNull = function() { + // Ensure a Current Window is available, if it's found to be `null` if (_currentWindowHandle === null) { // First call to get the current window: need to create one page = _decorateNewWindow(require("webpage").create()); _currentWindowHandle = page.windowHandle; _windows[_currentWindowHandle] = page; - } else if (_windows.hasOwnProperty(_currentWindowHandle)) { + } + }, + + _getCurrentWindow = function() { + var page = null; + + _initCurrentWindowIfNull(); + if (_windows.hasOwnProperty(_currentWindowHandle)) { page = _windows[_currentWindowHandle]; } @@ -306,6 +357,8 @@ ghostdriver.Session = function(desiredCapabilities) { if (page !== null) { // Switch current window and return "true" _currentWindowHandle = page.windowHandle; + // Switch to the Main Frame of that window + page.switchToMainFrame(); return true; } @@ -353,15 +406,48 @@ ghostdriver.Session = function(desiredCapabilities) { }, _setTimeout = function(type, ms) { - _timeouts[type] = ms; + // In case the chosen timeout is less than 0, we reset it to `_max32bitInt` + if (ms < 0) { + _timeouts[type] = _max32bitInt; + } else { + _timeouts[type] = ms; + } }, _getTimeout = function(type) { return _timeouts[type]; }, - _timeoutNames = function() { - return _const.TIMEOUT_NAMES; + _getScriptTimeout = function() { + return _getTimeout(_const.TIMEOUT_NAMES.SCRIPT); + }, + + _getAsyncScriptTimeout = function() { + return _getTimeout(_const.TIMEOUT_NAMES.ASYNC_SCRIPT); + }, + + _getImplicitTimeout = function() { + return _getTimeout(_const.TIMEOUT_NAMES.IMPLICIT); + }, + + _getPageLoadTimeout = function() { + return _getTimeout(_const.TIMEOUT_NAMES.PAGE_LOAD); + }, + + _setScriptTimeout = function(ms) { + _setTimeout(_const.TIMEOUT_NAMES.SCRIPT, ms); + }, + + _setAsyncScriptTimeout = function(ms) { + _setTimeout(_const.TIMEOUT_NAMES.ASYNC_SCRIPT, ms); + }, + + _setImplicitTimeout = function(ms) { + _setTimeout(_const.TIMEOUT_NAMES.IMPLICIT, ms); + }, + + _setPageLoadTimeout = function(ms) { + _setTimeout(_const.TIMEOUT_NAMES.PAGE_LOAD, ms); }, _aboutToDelete = function() { @@ -389,14 +475,21 @@ ghostdriver.Session = function(desiredCapabilities) { getWindow : _getWindow, closeWindow : _closeWindow, getWindowsCount : _getWindowsCount, + initCurrentWindowIfNull : _initCurrentWindowIfNull, getCurrentWindowHandle : _getCurrentWindowHandle, - getWindowHandles: _getWindowHandles, - isValidWindowHandle: _isValidWindowHandle, + getWindowHandles : _getWindowHandles, + isValidWindowHandle : _isValidWindowHandle, aboutToDelete : _aboutToDelete, - setTimeout : _setTimeout, - getTimeout : _getTimeout, - timeoutNames : _timeoutNames, - inputs: _inputs + inputs : _inputs, + setScriptTimeout : _setScriptTimeout, + setAsyncScriptTimeout : _setAsyncScriptTimeout, + setImplicitTimeout : _setImplicitTimeout, + setPageLoadTimeout : _setPageLoadTimeout, + getScriptTimeout : _getScriptTimeout, + getAsyncScriptTimeout : _getAsyncScriptTimeout, + getImplicitTimeout : _getImplicitTimeout, + getPageLoadTimeout : _getPageLoadTimeout, + timeoutNames : _const.TIMEOUT_NAMES }; }; diff --git a/src/ghostdriver/webelementlocator.js b/src/ghostdriver/webelementlocator.js index a897008c..60a94685 100644 --- a/src/ghostdriver/webelementlocator.js +++ b/src/ghostdriver/webelementlocator.js @@ -207,7 +207,7 @@ ghostdriver.WebElementLocator = function(session) { elementOrElements.hasOwnProperty("value")) { // return if elements found OR we passed the "stopSearchByTime" - stopSearchByTime = searchStartTime + _session.getTimeout(_session.timeoutNames().IMPLICIT); + stopSearchByTime = searchStartTime + _session.getImplicitTimeout(); if (elementOrElements.value.length !== 0 || new Date().getTime() > stopSearchByTime) { res.success(_session.getId(), elementOrElements.value); return; @@ -215,7 +215,7 @@ ghostdriver.WebElementLocator = function(session) { } // retry if we haven't passed "stopSearchByTime" - stopSearchByTime = searchStartTime + _session.getTimeout(_session.timeoutNames().IMPLICIT); + stopSearchByTime = searchStartTime + _session.getImplicitTimeout(); if (stopSearchByTime >= new Date().getTime()) { // Recursive call in 50ms setTimeout(function(){