Skip to content

Commit ea08463

Browse files
committed
Add support for custom command mappings in the node bindings.
Fixes issue 8221
1 parent ef9dc8d commit ea08463

File tree

6 files changed

+210
-2
lines changed

6 files changed

+210
-2
lines changed

javascript/node/selenium-webdriver/CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
* Promise rejections are now always coerced to Error-like objects (an object
44
with a string `message` property). We do not guarantee `instanceof Error`
55
since the rejection value may come from another context.
6+
* FIXED: 8221: Added support for defining custom command mappings. Includes
7+
support for PhantomJS's `executePhantomJS` (requires PhantomJS 1.9.7 or
8+
GhostDriver 1.1.0).
69
* FIXED: 8128: When the FirefoxDriver marshals an object to the page for
710
`executeScript`, it defines additional properties (required by the driver's
811
implementation). These properties will no longer be enumerable and should

javascript/node/selenium-webdriver/executors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ var DeferredExecutor = function(delegate) {
4545
// PUBLIC API
4646

4747

48+
exports.DeferredExecutor = DeferredExecutor;
49+
4850
/**
4951
* Creates a command executor that uses WebDriver's JSON wire protocol.
5052
* @param {(string|!webdriver.promise.Promise.<string>)} url The server's URL,

javascript/node/selenium-webdriver/phantomjs.js

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var fs = require('fs'),
2020

2121
var webdriver = require('./index'),
2222
executors = require('./executors'),
23+
http = require('./http'),
2324
io = require('./io'),
2425
portprober = require('./net/portprober'),
2526
remote = require('./remote');
@@ -58,6 +59,15 @@ var CLI_ARGS_CAPABILITY = 'phantomjs.cli.args';
5859
var DEFAULT_LOG_FILE = 'phantomjsdriver.log';
5960

6061

62+
/**
63+
* Custom command names supported by PhantomJS.
64+
* @enum {string}
65+
*/
66+
var Command = {
67+
EXECUTE_PHANTOM_SCRIPT: 'executePhantomScript'
68+
};
69+
70+
6171
/**
6272
* Finds the PhantomJS executable.
6373
* @param {string=} opt_exe Path to the executable to use.
@@ -111,6 +121,24 @@ function createDriver(opt_capabilities, opt_flow) {
111121
}
112122

113123

124+
/**
125+
* Creates a command executor with support for PhantomJS' custom commands.
126+
* @param {!webdriver.promise.Promise<string>} url The server's URL.
127+
* @return {!webdriver.CommandExecutor} The new command executor.
128+
*/
129+
function createExecutor(url) {
130+
return new executors.DeferredExecutor(url.then(function(url) {
131+
var client = new http.HttpClient(url);
132+
var executor = new http.Executor(client);
133+
134+
executor.defineCommand(
135+
Command.EXECUTE_PHANTOM_SCRIPT,
136+
'POST', '/session/:sessionId/phantom/execute');
137+
138+
return executor;
139+
}));
140+
}
141+
114142
/**
115143
* Creates a new WebDriver client for PhantomJS.
116144
*
@@ -169,7 +197,7 @@ var Driver = function(opt_capabilities, opt_flow) {
169197
})
170198
});
171199

172-
var executor = executors.createExecutor(service.start());
200+
var executor = createExecutor(service.start());
173201
var driver = webdriver.WebDriver.createSession(
174202
executor, capabilities, opt_flow);
175203

@@ -186,6 +214,54 @@ var Driver = function(opt_capabilities, opt_flow) {
186214
util.inherits(Driver, webdriver.WebDriver);
187215

188216

217+
/**
218+
* Executes a PhantomJS fragment. This method is similar to
219+
* {@link #executeScript}, except it exposes the
220+
* <a href="http://phantomjs.org/api/">PhantomJS API</a> to the injected
221+
* script.
222+
*
223+
* <p>The injected script will execute in the context of PhantomJS's
224+
* {@code page} variable. If a page has not been loaded before calling this
225+
* method, one will be created.</p>
226+
*
227+
* <p>Be sure to wrap callback definitions in a try/catch block, as failures
228+
* may cause future WebDriver calls to fail.</p>
229+
*
230+
* <p>Certain callbacks are used by GhostDriver (the PhantomJS WebDriver
231+
* implementation) and overriding these may cause the script to fail. It is
232+
* recommended that you check for existing callbacks before defining your own.
233+
* </p>
234+
*
235+
* As with {@link #executeScript}, the injected script may be defined as
236+
* a string for an anonymous function body (e.g. "return 123;"), or as a
237+
* function. If a function is provided, it will be decompiled to its original
238+
* source. Note that injecting functions is provided as a convenience to
239+
* simplify defining complex scripts. Care must be taken that the function
240+
* only references variables that will be defined in the page's scope and
241+
* that the function does not override {@code Function.prototype.toString}
242+
* (overriding toString() will interfere with how the function is
243+
* decompiled.
244+
*
245+
* @param {(string|!Function)} script The script to execute.
246+
* @param {...*} var_args The arguments to pass to the script.
247+
* @return {!webdriver.promise.Promise<T>} A promise that resolve to the
248+
* script's return value.
249+
* @template T
250+
*/
251+
Driver.prototype.executePhantomJS = function(script, args) {
252+
if (typeof script === 'function') {
253+
script = 'return (' + script + ').apply(this, arguments);';
254+
}
255+
var args = arguments.length > 1
256+
? Array.prototype.slice.call(arguments, 1) : [];
257+
return this.schedule(
258+
new webdriver.Command(Command.EXECUTE_PHANTOM_SCRIPT)
259+
.setParameter('script', script)
260+
.setParameter('args', args),
261+
'Driver.executePhantomJS()');
262+
};
263+
264+
189265
// PUBLIC API
190266

191267
exports.Driver = Driver;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2015 Selenium committers
2+
// Copyright 2015 Software Freedom Conservancy
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
'use strict';
17+
18+
var assert = require('assert');
19+
var path = require('path');
20+
var test = require('../../lib/test');
21+
22+
test.suite(function(env) {
23+
var driver;
24+
25+
test.before(function() {
26+
driver = env.builder().build();
27+
});
28+
29+
test.after(function() {
30+
driver.quit();
31+
});
32+
33+
var testPageUrl =
34+
'data:text/html,<html><h1>' + path.basename(__filename) + '</h1></html>';
35+
36+
test.beforeEach(function() {
37+
driver.get(testPageUrl);
38+
});
39+
40+
describe('phantomjs.Driver', function() {
41+
describe('#executePhantomJS()', function() {
42+
43+
test.it('can execute scripts using PhantomJS API', function() {
44+
return driver.executePhantomJS('return this.url;').then(function(url) {
45+
assert.equal(testPageUrl, url);
46+
});
47+
});
48+
49+
test.it('can execute scripts as functions', function() {
50+
driver.executePhantomJS(function(a, b) {
51+
return a + b;
52+
}, 1, 2).then(function(result) {
53+
assert.equal(3, result);
54+
});
55+
});
56+
57+
test.it('can manipulate the current page', function() {
58+
driver.manage().addCookie('foo', 'bar');
59+
driver.manage().getCookie('foo').then(function(cookie) {
60+
assert.equal('bar', cookie.value);
61+
});
62+
driver.executePhantomJS(function() {
63+
this.clearCookies();
64+
});
65+
driver.manage().getCookie('foo').then(function(cookie) {
66+
assert.equal(null, cookie);
67+
});
68+
});
69+
});
70+
});
71+
}, {browsers: ['phantomjs']});

javascript/webdriver/http/http.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,38 @@ webdriver.http.Executor = function(client) {
7070
* @private {!webdriver.http.Client}
7171
*/
7272
this.client_ = client;
73+
74+
/**
75+
* @private {!Object<{method:string, path:string}>}
76+
*/
77+
this.customCommands_ = {};
78+
};
79+
80+
81+
/**
82+
* Defines a new command for use with this executor. When a command is sent,
83+
* the {@code path} will be preprocessed using the command's parameters; any
84+
* path segments prefixed with ":" will be replaced by the parameter of the
85+
* same name. For example, given "/person/:name" and the parameters
86+
* "{name: 'Bob'}", the final command path will be "/person/Bob".
87+
*
88+
* @param {string} name The command name.
89+
* @param {string} method The HTTP method to use when sending this command.
90+
* @param {string} pathPattern The path to send the command to, relative to
91+
* the WebDriver server's command root and of the form
92+
* "/path/:variable/segment".
93+
*/
94+
webdriver.http.Executor.prototype.defineCommand = function(
95+
name, method, path) {
96+
this.customCommands_[name] = {method: method, path: path};
7397
};
7498

7599

76100
/** @override */
77101
webdriver.http.Executor.prototype.execute = function(command, callback) {
78-
var resource = webdriver.http.Executor.COMMAND_MAP_[command.getName()];
102+
var resource =
103+
this.customCommands_[command.getName()] ||
104+
webdriver.http.Executor.COMMAND_MAP_[command.getName()];
79105
if (!resource) {
80106
throw new Error('Unrecognized command: ' + command.getName());
81107
}

javascript/webdriver/test/http/http_test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,36 @@ function testExecute_attemptsToParseBodyWhenNoContentTypeSpecified() {
328328
});
329329
}
330330

331+
function testCanDefineNewCommands() {
332+
executor.defineCommand('greet', 'GET', '/person/:name');
333+
334+
var command = new webdriver.Command('greet').
335+
setParameter('name', 'Bob');
336+
337+
expectRequest('GET', '/person/Bob', {},
338+
{'Accept': 'application/json; charset=utf-8'}).
339+
$does(respondsWith(null, response(200, {}, '')));
340+
control.$replayAll();
341+
342+
assertSendsSuccessfully(command);
343+
}
344+
345+
function testCanRedefineStandardCommands() {
346+
executor.defineCommand(webdriver.CommandName.GO_BACK,
347+
'POST', '/custom/back');
348+
349+
var command = new webdriver.Command(webdriver.CommandName.GO_BACK).
350+
setParameter('times', 3);
351+
352+
expectRequest('POST', '/custom/back',
353+
{'times': 3},
354+
{'Accept': 'application/json; charset=utf-8'}).
355+
$does(respondsWith(null, response(200, {}, '')));
356+
control.$replayAll();
357+
358+
assertSendsSuccessfully(command);
359+
}
360+
331361
function FakeXmlHttpRequest(headers, status, responseText) {
332362
return {
333363
getAllResponseHeaders: function() { return headers; },

0 commit comments

Comments
 (0)