From ffa9fab316a64fb7410fb7429db644c3a38ed595 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Sun, 18 Nov 2012 23:58:21 +0000 Subject: [PATCH] Embedding GhostDriver into PhantomJS(!!!) Finally. After so much work, this is finally a reality. To launch PhantomJS in "Remote WebDriver mode": ```bash $ phantomjs --webdriver=OPTIONAL_IP:OPTIONAL_PORT ``` Also, GhostDriver brings along support for Selenium Grid: now PhantomJS can register itself to a Selenium Grid HUB. Just launch it in Webdriver Mode with the following extra options: ```bash $ phantomjs --webdriver=OPTIONAL_IP:OPTIONAL_PORT --webdriver-selenium-grid-hub=http://url.to.selenium.grid.hub:port ``` http://code.google.com/p/phantomjs/issues/detail?id=49 --- src/bootstrap.js | 9 +++-- src/config.cpp | 68 ++++++++++++++++++++++++++++++++- src/config.h | 13 ++++++- src/filesystem.cpp | 2 +- src/phantom.cpp | 50 +++++++++++++++++++++--- src/phantom.h | 3 ++ src/qcommandline/qcommandline.h | 2 +- 7 files changed, 135 insertions(+), 12 deletions(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index 14ea83b6..b3edb2a6 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -193,7 +193,7 @@ phantom.onError = phantom.defaultErrorHandler; var paths = [], dir; if (request[0] === '.') { - paths.push(fs.absolute(joinPath(this.dirname, request))); + paths.push(fs.absolute(joinPath(phantom.webdriverMode ? ":/ghostdriver" : this.dirname, request))); } else if (request[0] === '/') { paths.push(fs.absolute(request)); } else { @@ -284,8 +284,11 @@ phantom.onError = phantom.defaultErrorHandler; cwd = fs.absolute(phantom.libraryPath); mainFilename = joinPath(cwd, basename(require('system').args[0]) || 'repl'); mainModule._setFilename(mainFilename); - // include CoffeeScript which takes care of adding .coffee extension - require('_coffee-script'); + + // include CoffeeScript which takes care of adding .coffee extension (only if not in Webdriver mode) + if (!phantom.webdriverMode) { + require('_coffee-script'); + } }()); }()); diff --git a/src/config.cpp b/src/config.cpp index be04687b..c0b56134 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -63,6 +63,8 @@ static const struct QCommandLineConfigEntry flags[] = { QCommandLine::Option, '\0', "script-encoding", "Sets the encoding used for the starting script, default is 'utf8'", QCommandLine::Optional }, { QCommandLine::Option, '\0', "web-security", "Enables web security, 'yes' (default) or 'no'", QCommandLine::Optional }, { QCommandLine::Option, '\0', "ssl-protocol", "Sets the SSL protocol (supported protocols: 'SSLv3' (default), 'SSLv2', 'TLSv1', 'any')", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "webdriver", "Starts in 'Remote WebDriver mode' (embedded GhostDriver): '[[:]]' (default '127.0.0.1:8910') ", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "webdriver-selenium-grid-hub", "URL to the Selenium Grid HUB: 'URL_TO_HUB' (default 'none') (NOTE: works only together with '--webdriver') ", QCommandLine::Optional }, { QCommandLine::Param, '\0', "script", "Script", QCommandLine::Flags(QCommandLine::Optional|QCommandLine::ParameterFence)}, { QCommandLine::Param, '\0', "argument", "Script argument", QCommandLine::OptionalMultiple }, { QCommandLine::Switch, 'h', "help", "Shows this message and quits", QCommandLine::Optional }, @@ -99,6 +101,20 @@ void Config::processArgs(const QStringList &args) m_cmdLine->setArguments(args); m_cmdLine->setConfig(flags); m_cmdLine->parse(); + + // Inject command line parameters to be picked up by GhostDriver + if (isWebdriverMode()) { + QStringList argsForGhostDriver; + + m_scriptFile = "main.js"; //< launch script + argsForGhostDriver << m_webdriver; //< ip:port + if (!m_seleniumGridHub.isEmpty()) { + argsForGhostDriver << m_seleniumGridHub; //< selenium grid url + } + + // Clear current args and override with those + setScriptArgs(argsForGhostDriver); + } } void Config::loadJsonFile(const QString &filePath) @@ -424,6 +440,48 @@ bool Config::javascriptCanCloseWindows() const return m_javascriptCanCloseWindows; } +void Config::setWebdriver(const QString &webdriverConfig) +{ + // This option can be provided empty: in that case we should use the default IP:PORT configuration + QString ip = "127.0.0.1"; + QString port = "8910"; + + // Parse and validate the configuration + bool isValidPort; + QStringList wdCfg = webdriverConfig.split(':'); + if (wdCfg.length() == 1 && wdCfg[0].toInt(&isValidPort) && isValidPort) { + // Only a PORT was provided + port = wdCfg[0]; + } else if(wdCfg.length() == 2 && !wdCfg[0].isEmpty() && wdCfg[1].toInt(&isValidPort) && isValidPort) { + // Both IP and PORT provided + ip = wdCfg[0]; + port = wdCfg[1]; + } + + // Setting the "webdriver" configuration + m_webdriver = QString("%1:%2").arg(ip).arg(port); +} + +QString Config::webdriver() const +{ + return m_webdriver; +} + +bool Config::isWebdriverMode() const +{ + return !m_webdriver.isEmpty(); +} + +void Config::setSeleniumGridHub(const QString &hubUrl) +{ + m_seleniumGridHub = hubUrl; +} + +QString Config::seleniumGridHub() const +{ + return m_seleniumGridHub; +} + // private: void Config::resetToDefaults() { @@ -455,6 +513,8 @@ void Config::resetToDefaults() m_helpFlag = false; m_printDebugMessages = false; m_sslProtocol = "sslv3"; + m_webdriver = QString(); + m_seleniumGridHub = QString(); } void Config::setProxyAuthPass(const QString &value) @@ -598,6 +658,12 @@ void Config::handleOption(const QString &option, const QVariant &value) if (option == "ssl-protocol") { setSslProtocol(value.toString()); } + if (option == "webdriver") { + setWebdriver(value.toString()); + } + if (option == "selenium-grid-hub") { + setSeleniumGridHub(value.toString()); + } } void Config::handleParam(const QString& param, const QVariant &value) @@ -623,4 +689,4 @@ QString Config::sslProtocol() const void Config::setSslProtocol(const QString& sslProtocolName) { m_sslProtocol = sslProtocolName.toLower(); -} \ No newline at end of file +} diff --git a/src/config.h b/src/config.h index 3268fb1e..15c50cce 100644 --- a/src/config.h +++ b/src/config.h @@ -38,7 +38,7 @@ class QCommandLine; -class Config: QObject +class Config: public QObject { Q_OBJECT Q_PROPERTY(QString cookiesFile READ cookiesFile WRITE setCookiesFile) @@ -58,6 +58,8 @@ class Config: QObject Q_PROPERTY(bool javascriptCanOpenWindows READ javascriptCanOpenWindows WRITE setJavascriptCanOpenWindows) Q_PROPERTY(bool javascriptCanCloseWindows READ javascriptCanCloseWindows WRITE setJavascriptCanCloseWindows) Q_PROPERTY(QString sslProtocol READ sslProtocol WRITE setSslProtocol) + Q_PROPERTY(QString webdriver READ webdriver WRITE setWebdriver) + Q_PROPERTY(QString seleniumGridHub READ seleniumGridHub WRITE setSeleniumGridHub) public: Config(QObject *parent = 0); @@ -152,6 +154,13 @@ public: void setSslProtocol(const QString& sslProtocolName); QString sslProtocol() const; + void setWebdriver(const QString& webdriverConfig); + QString webdriver() const; + bool isWebdriverMode() const; + + void setSeleniumGridHub(const QString& hubUrl); + QString seleniumGridHub() const; + public slots: void handleSwitch(const QString &sw); void handleOption(const QString &option, const QVariant &value); @@ -196,6 +205,8 @@ private: bool m_javascriptCanOpenWindows; bool m_javascriptCanCloseWindows; QString m_sslProtocol; + QString m_webdriver; + QString m_seleniumGridHub; }; #endif // CONFIG_H diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 294891e4..e199ce7e 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -354,7 +354,7 @@ bool FileSystem::changeWorkingDirectory(const QString &path) const QString FileSystem::absolute(const QString &relativePath) const { - return QFileInfo(relativePath).absoluteFilePath(); + return QFileInfo(relativePath).absoluteFilePath(); } // Files diff --git a/src/phantom.cpp b/src/phantom.cpp index f499d615..9614799a 100644 --- a/src/phantom.cpp +++ b/src/phantom.cpp @@ -35,6 +35,9 @@ #include #include #include +#include +#include +#include #include "consts.h" #include "terminal.h" @@ -179,12 +182,37 @@ bool Phantom::execute() if (m_terminated) return false; - if (m_config.scriptFile().isEmpty()) { - // REPL mode requested +#ifndef QT_NO_DEBUG_OUTPUT + qDebug() << "Phantom - execute: Configuration"; + const QMetaObject* configMetaObj = m_config.metaObject(); + for (int i = 0, ilen = configMetaObj->propertyCount(); i < ilen; ++i) { + qDebug() << " " << i << configMetaObj->property(i).name() << ":" << m_config.property(configMetaObj->property(i).name()).toString(); + } + + qDebug() << "Phantom - execute: Script & Arguments"; + qDebug() << " " << "script:" << m_config.scriptFile(); + QStringList args = m_config.scriptArgs(); + for (int i = 0, ilen = args.length(); i < ilen; ++i) { + qDebug() << " " << i << "arg:" << args.at(i); + } +#endif + + if (m_config.isWebdriverMode()) { // Remote WebDriver mode requested + qDebug() << "Phantom - execute: Starting Remote WebDriver mode"; + + Terminal::instance()->cout("PhantomJS is launching GhostDriver..."); + if (!Utils::injectJsInFrame(":/ghostdriver/main.js", m_scriptFileEnc, QDir::currentPath(), m_page->mainFrame(), true)) { + m_returnValue = -1; + return false; + } + } else if (m_config.scriptFile().isEmpty()) { // REPL mode requested + qDebug() << "Phantom - execute: Starting REPL mode"; + // Create the REPL: it will launch itself, no need to store this variable. REPL::getInstance(m_page->mainFrame(), this); - } else { - // Load the User Script + } else { // Load the User Script + qDebug() << "Phantom - execute: Starting normal mode"; + if (m_config.debug()) { // Debug enabled if (!Utils::loadJSForDebug(m_config.scriptFile(), m_scriptFileEnc, QDir::currentPath(), m_page->mainFrame(), m_config.remoteDebugAutorun())) { @@ -261,6 +289,11 @@ void Phantom::setCookiesEnabled(const bool value) } } +bool Phantom::webdriverMode() const +{ + return m_config.isWebdriverMode(); +} + // public slots: QObject *Phantom::createWebPage() { @@ -331,12 +364,19 @@ void Phantom::loadModule(const QString &moduleSource, const QString &filename) bool Phantom::injectJs(const QString &jsFilePath) { + QString pre = ""; qDebug() << "Phantom - injectJs:" << jsFilePath; + // If in Remote Webdriver Mode, we need to manipulate the PATH, to point it to a resource in `ghostdriver.qrc` + if (webdriverMode()) { + pre = ":/ghostdriver/"; + qDebug() << "Phantom - injectJs: prepending" << pre; + } + if (m_terminated) return false; - return Utils::injectJsInFrame(jsFilePath, libraryPath(), m_page->mainFrame()); + return Utils::injectJsInFrame(pre + jsFilePath, libraryPath(), m_page->mainFrame()); } void Phantom::exit(int code) diff --git a/src/phantom.h b/src/phantom.h index 0fb1f577..e922f77c 100644 --- a/src/phantom.h +++ b/src/phantom.h @@ -56,6 +56,7 @@ class Phantom: public REPLCompletable Q_PROPERTY(QObject *page READ page) Q_PROPERTY(bool cookiesEnabled READ areCookiesEnabled WRITE setCookiesEnabled) Q_PROPERTY(QVariantList cookies READ cookies WRITE setCookies) + Q_PROPERTY(bool webdriverMode READ webdriverMode) private: // Private constructor: the Phantom class is a singleton @@ -99,6 +100,8 @@ public: bool areCookiesEnabled() const; void setCookiesEnabled(const bool value); + bool webdriverMode() const; + public slots: QObject *createWebPage(); QObject *createWebServer(); diff --git a/src/qcommandline/qcommandline.h b/src/qcommandline/qcommandline.h index 23949e05..734e6c6d 100644 --- a/src/qcommandline/qcommandline.h +++ b/src/qcommandline/qcommandline.h @@ -85,7 +85,7 @@ public: Multiple = 0x04, /**< argument can be used multiple time and will produce multiple signals. */ ParameterFence = 0x08, //**< all arguments after this point are considered parameters, not options. */ MandatoryMultiple = Mandatory|Multiple, - OptionalMultiple = Optional|Multiple, + OptionalMultiple = Optional|Multiple } Flags; /**