diff --git a/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSCommandExecutor.java b/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSCommandExecutor.java index 85f323140..e8a3662ff 100644 --- a/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSCommandExecutor.java +++ b/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSCommandExecutor.java @@ -59,7 +59,7 @@ class PhantomJSCommandExecutor extends HttpCommandExecutor { * @param service The PhantomJSDriverService to send commands to. */ public PhantomJSCommandExecutor(PhantomJSDriverService service) { - super(service.getUrl()); + super(PhantomJSDriver.getCustomCommands(), service.getUrl()); this.service = service; } diff --git a/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSDriver.java b/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSDriver.java index e70b1dc3a..96381858f 100644 --- a/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSDriver.java +++ b/binding/java/src/main/java/org/openqa/selenium/phantomjs/PhantomJSDriver.java @@ -27,15 +27,18 @@ package org.openqa.selenium.phantomjs; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import org.openqa.selenium.Capabilities; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.remote.DesiredCapabilities; -import org.openqa.selenium.remote.DriverCommand; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.openqa.selenium.remote.service.DriverCommandExecutor; -import org.openqa.selenium.remote.service.DriverService; +import org.openqa.selenium.remote.*; +import org.openqa.selenium.remote.internal.WebElementToJsonConverter; + +import java.util.HashMap; +import java.util.Map; /** * A {@link org.openqa.selenium.WebDriver} implementation that controls a PhantomJS running in Remote WebDriver mode. @@ -103,8 +106,8 @@ public PhantomJSDriver(Capabilities desiredCapabilities) { * @param service The service to use. * @param desiredCapabilities The capabilities required from PhantomJS/GhostDriver. */ - public PhantomJSDriver(DriverService service, Capabilities desiredCapabilities) { - super(new DriverCommandExecutor(service), desiredCapabilities); + public PhantomJSDriver(PhantomJSDriverService service, Capabilities desiredCapabilities) { + super(new PhantomJSCommandExecutor(service), desiredCapabilities); } /** @@ -120,4 +123,49 @@ public X getScreenshotAs(OutputType target) throws WebDriverException { String base64 = (String) execute(DriverCommand.SCREENSHOT).getValue(); return target.convertFromBase64Png(base64); } + + /** + * Execute a PhantomJS fragment. Provides extra functionality not found in WebDriver + * but available in PhantomJS. + *

+ * See the PhantomJS API< + * for details on what is available. + *

+ * A 'page' variable pointing to currently selected page is available for use. + * If there is no page yet, one is created. + *

+ * When overriding any callbacks be sure to wrap in a try/catch block, as failures + * may cause future WebDriver calls to fail. + *

+ * Certain callbacks are used by GhostDriver (the PhantomJS WebDriver implementation) + * already. Overriding these may cause the script to fail. It's a good idea to check + * for existing callbacks before overriding. + * + * @param script The fragment of PhantomJS JavaScript to execute. + * @param args List of arguments to pass to the function that the script is wrapped in. + * These can accessed in the script as 'arguments[0]', 'arguments[1]', + * 'arguments[2]', etc + * @return The result of the evaluation. + */ + public Object executePhantomJS(String script, Object... args) { + script = script.replaceAll("\"", "\\\""); + + Iterable convertedArgs = Iterables.transform( + Lists.newArrayList(args), new WebElementToJsonConverter()); + Map params = ImmutableMap.of( + "script", script, "args", Lists.newArrayList(convertedArgs)); + + return execute(COMMAND_EXECUTE_PHANTOM_SCRIPT, params).getValue(); + } + + private static final String COMMAND_EXECUTE_PHANTOM_SCRIPT = "executePhantomScript"; + + protected static Map getCustomCommands() { + Map customCommands = new HashMap(); + + customCommands.put(COMMAND_EXECUTE_PHANTOM_SCRIPT, + new CommandInfo("/session/:sessionId/phantom/execute", HttpVerb.POST)); + + return customCommands; + } } diff --git a/src/request_handlers/session_request_handler.js b/src/request_handlers/session_request_handler.js index e14268290..5c61d775d 100644 --- a/src/request_handlers/session_request_handler.js +++ b/src/request_handlers/session_request_handler.js @@ -63,7 +63,9 @@ ghostdriver.SessionReqHand = function(session) { CLICK : "click", BUTTON_DOWN : "buttondown", BUTTON_UP : "buttonup", - DOUBLE_CLICK : "doubleclick" + DOUBLE_CLICK : "doubleclick", + PHANTOM_DIR : "/phantom/", + PHANTOM_EXEC : "execute", }; var @@ -133,7 +135,7 @@ ghostdriver.SessionReqHand = function(session) { } else if (req.urlParsed.file === _const.REFRESH && req.method === "POST") { _refreshCommand(req, res); return; - } else if (req.urlParsed.file === _const.EXECUTE && req.method === "POST") { + } else if (req.urlParsed.file === _const.EXECUTE && req.urlParsed.directory === "/" && req.method == "POST") { _executeCommand(req, res); return; } else if (req.urlParsed.file === _const.EXECUTE_ASYNC && req.method === "POST") { @@ -157,6 +159,9 @@ ghostdriver.SessionReqHand = function(session) { } else if (req.urlParsed.file === _const.MOVE_TO && req.method === "POST") { _postMouseMoveToCommand(req, res); return; + } else if (req.urlParsed.file === _const.PHANTOM_EXEC && req.urlParsed.directory === _const.PHANTOM_DIR && req.method === "POST") { + _executePhantomJS(req, res); + return; } else if (req.urlParsed.file === _const.CLICK && req.method === "POST") { _postMouseClickCommand(req, res, "click"); return; @@ -837,6 +842,15 @@ ghostdriver.SessionReqHand = function(session) { res.success(_session.getId(), _protoParent.getSessionCurrWindow.call(this, _session, req).title); }; + _executePhantomJS = function(req, res) { + var params = JSON.parse(req.post); + if (typeof(params) === "object" && params.script && params.args) { + res.success(_session.getId(), _session.executePhantomJS(_protoParent.getSessionCurrWindow.call(this, _session, req), params.script, params.args)); + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }; + // public: return { handle : _handle, diff --git a/src/session.js b/src/session.js index db4e79e7c..d436f95a9 100644 --- a/src/session.js +++ b/src/session.js @@ -477,6 +477,15 @@ ghostdriver.Session = function(desiredCapabilities) { _setTimeout(_const.TIMEOUT_NAMES.PAGE_LOAD, ms); }, + _executePhantomJS = function(page, script, args) { + try { + var code = new Function(script); + return code.apply(page, args); + } catch (e) { + return e; + } + }, + _aboutToDelete = function() { var k; @@ -519,6 +528,7 @@ ghostdriver.Session = function(desiredCapabilities) { getAsyncScriptTimeout : _getAsyncScriptTimeout, getImplicitTimeout : _getImplicitTimeout, getPageLoadTimeout : _getPageLoadTimeout, + executePhantomJS : _executePhantomJS, timeoutNames : _const.TIMEOUT_NAMES, isLoading : _isLoading }; diff --git a/test/java/src/test/java/ghostdriver/PhantomJSCommandTest.java b/test/java/src/test/java/ghostdriver/PhantomJSCommandTest.java new file mode 100755 index 000000000..56c1eeecc --- /dev/null +++ b/test/java/src/test/java/ghostdriver/PhantomJSCommandTest.java @@ -0,0 +1,49 @@ +package ghostdriver; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.phantomjs.PhantomJSDriver; + +import static junit.framework.TestCase.assertEquals; + +public class PhantomJSCommandTest extends BaseTest { + @Test + public void executePhantomJS() { + WebDriver d = getDriver(); + if (!(d instanceof PhantomJSDriver)) { + // Skip this test if not using PhantomJS. + // The command under test is only available when using PhantomJS + return; + } + + PhantomJSDriver phantom = (PhantomJSDriver)d; + + // Do we get results back? + Object result = phantom.executePhantomJS("return 1 + 1"); + assertEquals(new Long(2), (Long)result); + + // Can we read arguments? + result = phantom.executePhantomJS("return arguments[0] + arguments[0]", new Long(1)); + assertEquals(new Long(2), (Long)result); + + // Can we override some browser JavaScript functions in the page context? + result = phantom.executePhantomJS("var page = this;" + + "page.onInitialized = function () { " + + "page.evaluate(function () { " + + "Math.random = function() { return 42 / 100 } " + + "})" + + "}"); + + phantom.get("http://ariya.github.com/js/random/"); + + WebElement numbers = phantom.findElement(By.id("numbers")); + boolean foundAtLeastOne = false; + for(String number : numbers.getText().split(" ")) { + foundAtLeastOne = true; + assertEquals("42", number); + } + assert(foundAtLeastOne); + } +}