lib/webdriver/http/http.js

1// Copyright 2011 Software Freedom Conservancy. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/**
16 * @fileoverview Defines a {@code webdriver.CommandExecutor} that communicates
17 * with a server over HTTP.
18 */
19
20goog.provide('webdriver.http.Client');
21goog.provide('webdriver.http.Executor');
22goog.provide('webdriver.http.Request');
23goog.provide('webdriver.http.Response');
24
25goog.require('bot.ErrorCode');
26goog.require('goog.array');
27goog.require('goog.json');
28goog.require('webdriver.CommandExecutor');
29goog.require('webdriver.CommandName');
30goog.require('webdriver.promise.Deferred');
31
32
33
34/**
35 * Interface used for sending individual HTTP requests to the server.
36 * @interface
37 */
38webdriver.http.Client = function() {
39};
40
41
42/**
43 * Sends a request to the server. If an error occurs while sending the request,
44 * such as a failure to connect to the server, the provided callback will be
45 * invoked with a non-null {@link Error} describing the error. Otherwise, when
46 * the server's response has been received, the callback will be invoked with a
47 * null Error and non-null {@link webdriver.http.Response} object.
48 *
49 * @param {!webdriver.http.Request} request The request to send.
50 * @param {function(Error, !webdriver.http.Response=)} callback the function to
51 * invoke when the server's response is ready.
52 */
53webdriver.http.Client.prototype.send = function(request, callback) {
54};
55
56
57
58/**
59 * A command executor that communicates with a server using the WebDriver
60 * command protocol.
61 * @param {!webdriver.http.Client} client The client to use when sending
62 * requests to the server.
63 * @constructor
64 * @implements {webdriver.CommandExecutor}
65 */
66webdriver.http.Executor = function(client) {
67
68 /**
69 * Client used to communicate with the server.
70 * @private {!webdriver.http.Client}
71 */
72 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} path 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 */
94webdriver.http.Executor.prototype.defineCommand = function(
95 name, method, path) {
96 this.customCommands_[name] = {method: method, path: path};
97};
98
99
100/** @override */
101webdriver.http.Executor.prototype.execute = function(command, callback) {
102 var resource =
103 this.customCommands_[command.getName()] ||
104 webdriver.http.Executor.COMMAND_MAP_[command.getName()];
105 if (!resource) {
106 throw new Error('Unrecognized command: ' + command.getName());
107 }
108
109 var parameters = command.getParameters();
110 var path = webdriver.http.Executor.buildPath_(resource.path, parameters);
111 var request = new webdriver.http.Request(resource.method, path, parameters);
112
113 this.client_.send(request, function(e, response) {
114 var responseObj;
115 if (!e) {
116 try {
117 responseObj = webdriver.http.Executor.parseHttpResponse_(
118 /** @type {!webdriver.http.Response} */ (response));
119 } catch (ex) {
120 e = ex;
121 }
122 }
123 callback(e, responseObj);
124 });
125};
126
127
128/**
129 * Builds a fully qualified path using the given set of command parameters. Each
130 * path segment prefixed with ':' will be replaced by the value of the
131 * corresponding parameter. All parameters spliced into the path will be
132 * removed from the parameter map.
133 * @param {string} path The original resource path.
134 * @param {!Object.<*>} parameters The parameters object to splice into
135 * the path.
136 * @return {string} The modified path.
137 * @private
138 */
139webdriver.http.Executor.buildPath_ = function(path, parameters) {
140 var pathParameters = path.match(/\/:(\w+)\b/g);
141 if (pathParameters) {
142 for (var i = 0; i < pathParameters.length; ++i) {
143 var key = pathParameters[i].substring(2); // Trim the /:
144 if (key in parameters) {
145 var value = parameters[key];
146 // TODO: move webdriver.WebElement.ELEMENT definition to a
147 // common file so we can reference it here without pulling in all of
148 // webdriver.WebElement's dependencies.
149 if (value && value['ELEMENT']) {
150 // When inserting a WebElement into the URL, only use its ID value,
151 // not the full JSON.
152 value = value['ELEMENT'];
153 }
154 path = path.replace(pathParameters[i], '/' + value);
155 delete parameters[key];
156 } else {
157 throw new Error('Missing required parameter: ' + key);
158 }
159 }
160 }
161 return path;
162};
163
164
165/**
166 * Callback used to parse {@link webdriver.http.Response} objects from a
167 * {@link webdriver.http.Client}.
168 * @param {!webdriver.http.Response} httpResponse The HTTP response to parse.
169 * @return {!bot.response.ResponseObject} The parsed response.
170 * @private
171 */
172webdriver.http.Executor.parseHttpResponse_ = function(httpResponse) {
173 try {
174 return /** @type {!bot.response.ResponseObject} */ (goog.json.parse(
175 httpResponse.body));
176 } catch (ex) {
177 // Whoops, looks like the server sent us a malformed response. We'll need
178 // to manually build a response object based on the response code.
179 }
180
181 var response = {
182 'status': bot.ErrorCode.SUCCESS,
183 'value': httpResponse.body.replace(/\r\n/g, '\n')
184 };
185
186 if (!(httpResponse.status > 199 && httpResponse.status < 300)) {
187 // 404 represents an unknown command; anything else is a generic unknown
188 // error.
189 response['status'] = httpResponse.status == 404 ?
190 bot.ErrorCode.UNKNOWN_COMMAND :
191 bot.ErrorCode.UNKNOWN_ERROR;
192 }
193
194 return response;
195};
196
197
198/**
199 * Maps command names to resource locator.
200 * @private {!Object.<{method:string, path:string}>}
201 * @const
202 */
203webdriver.http.Executor.COMMAND_MAP_ = (function() {
204 return new Builder().
205 put(webdriver.CommandName.GET_SERVER_STATUS, get('/status')).
206 put(webdriver.CommandName.NEW_SESSION, post('/session')).
207 put(webdriver.CommandName.GET_SESSIONS, get('/sessions')).
208 put(webdriver.CommandName.DESCRIBE_SESSION, get('/session/:sessionId')).
209 put(webdriver.CommandName.QUIT, del('/session/:sessionId')).
210 put(webdriver.CommandName.CLOSE, del('/session/:sessionId/window')).
211 put(webdriver.CommandName.GET_CURRENT_WINDOW_HANDLE,
212 get('/session/:sessionId/window_handle')).
213 put(webdriver.CommandName.GET_WINDOW_HANDLES,
214 get('/session/:sessionId/window_handles')).
215 put(webdriver.CommandName.GET_CURRENT_URL,
216 get('/session/:sessionId/url')).
217 put(webdriver.CommandName.GET, post('/session/:sessionId/url')).
218 put(webdriver.CommandName.GO_BACK, post('/session/:sessionId/back')).
219 put(webdriver.CommandName.GO_FORWARD,
220 post('/session/:sessionId/forward')).
221 put(webdriver.CommandName.REFRESH,
222 post('/session/:sessionId/refresh')).
223 put(webdriver.CommandName.ADD_COOKIE,
224 post('/session/:sessionId/cookie')).
225 put(webdriver.CommandName.GET_ALL_COOKIES,
226 get('/session/:sessionId/cookie')).
227 put(webdriver.CommandName.DELETE_ALL_COOKIES,
228 del('/session/:sessionId/cookie')).
229 put(webdriver.CommandName.DELETE_COOKIE,
230 del('/session/:sessionId/cookie/:name')).
231 put(webdriver.CommandName.FIND_ELEMENT,
232 post('/session/:sessionId/element')).
233 put(webdriver.CommandName.FIND_ELEMENTS,
234 post('/session/:sessionId/elements')).
235 put(webdriver.CommandName.GET_ACTIVE_ELEMENT,
236 post('/session/:sessionId/element/active')).
237 put(webdriver.CommandName.FIND_CHILD_ELEMENT,
238 post('/session/:sessionId/element/:id/element')).
239 put(webdriver.CommandName.FIND_CHILD_ELEMENTS,
240 post('/session/:sessionId/element/:id/elements')).
241 put(webdriver.CommandName.CLEAR_ELEMENT,
242 post('/session/:sessionId/element/:id/clear')).
243 put(webdriver.CommandName.CLICK_ELEMENT,
244 post('/session/:sessionId/element/:id/click')).
245 put(webdriver.CommandName.SEND_KEYS_TO_ELEMENT,
246 post('/session/:sessionId/element/:id/value')).
247 put(webdriver.CommandName.SUBMIT_ELEMENT,
248 post('/session/:sessionId/element/:id/submit')).
249 put(webdriver.CommandName.GET_ELEMENT_TEXT,
250 get('/session/:sessionId/element/:id/text')).
251 put(webdriver.CommandName.GET_ELEMENT_TAG_NAME,
252 get('/session/:sessionId/element/:id/name')).
253 put(webdriver.CommandName.IS_ELEMENT_SELECTED,
254 get('/session/:sessionId/element/:id/selected')).
255 put(webdriver.CommandName.IS_ELEMENT_ENABLED,
256 get('/session/:sessionId/element/:id/enabled')).
257 put(webdriver.CommandName.IS_ELEMENT_DISPLAYED,
258 get('/session/:sessionId/element/:id/displayed')).
259 put(webdriver.CommandName.GET_ELEMENT_LOCATION,
260 get('/session/:sessionId/element/:id/location')).
261 put(webdriver.CommandName.GET_ELEMENT_SIZE,
262 get('/session/:sessionId/element/:id/size')).
263 put(webdriver.CommandName.GET_ELEMENT_ATTRIBUTE,
264 get('/session/:sessionId/element/:id/attribute/:name')).
265 put(webdriver.CommandName.GET_ELEMENT_VALUE_OF_CSS_PROPERTY,
266 get('/session/:sessionId/element/:id/css/:propertyName')).
267 put(webdriver.CommandName.ELEMENT_EQUALS,
268 get('/session/:sessionId/element/:id/equals/:other')).
269 put(webdriver.CommandName.SWITCH_TO_WINDOW,
270 post('/session/:sessionId/window')).
271 put(webdriver.CommandName.MAXIMIZE_WINDOW,
272 post('/session/:sessionId/window/:windowHandle/maximize')).
273 put(webdriver.CommandName.GET_WINDOW_POSITION,
274 get('/session/:sessionId/window/:windowHandle/position')).
275 put(webdriver.CommandName.SET_WINDOW_POSITION,
276 post('/session/:sessionId/window/:windowHandle/position')).
277 put(webdriver.CommandName.GET_WINDOW_SIZE,
278 get('/session/:sessionId/window/:windowHandle/size')).
279 put(webdriver.CommandName.SET_WINDOW_SIZE,
280 post('/session/:sessionId/window/:windowHandle/size')).
281 put(webdriver.CommandName.SWITCH_TO_FRAME,
282 post('/session/:sessionId/frame')).
283 put(webdriver.CommandName.GET_PAGE_SOURCE,
284 get('/session/:sessionId/source')).
285 put(webdriver.CommandName.GET_TITLE,
286 get('/session/:sessionId/title')).
287 put(webdriver.CommandName.EXECUTE_SCRIPT,
288 post('/session/:sessionId/execute')).
289 put(webdriver.CommandName.EXECUTE_ASYNC_SCRIPT,
290 post('/session/:sessionId/execute_async')).
291 put(webdriver.CommandName.SCREENSHOT,
292 get('/session/:sessionId/screenshot')).
293 put(webdriver.CommandName.SET_TIMEOUT,
294 post('/session/:sessionId/timeouts')).
295 put(webdriver.CommandName.SET_SCRIPT_TIMEOUT,
296 post('/session/:sessionId/timeouts/async_script')).
297 put(webdriver.CommandName.IMPLICITLY_WAIT,
298 post('/session/:sessionId/timeouts/implicit_wait')).
299 put(webdriver.CommandName.MOVE_TO, post('/session/:sessionId/moveto')).
300 put(webdriver.CommandName.CLICK, post('/session/:sessionId/click')).
301 put(webdriver.CommandName.DOUBLE_CLICK,
302 post('/session/:sessionId/doubleclick')).
303 put(webdriver.CommandName.MOUSE_DOWN,
304 post('/session/:sessionId/buttondown')).
305 put(webdriver.CommandName.MOUSE_UP, post('/session/:sessionId/buttonup')).
306 put(webdriver.CommandName.MOVE_TO, post('/session/:sessionId/moveto')).
307 put(webdriver.CommandName.SEND_KEYS_TO_ACTIVE_ELEMENT,
308 post('/session/:sessionId/keys')).
309 put(webdriver.CommandName.TOUCH_SINGLE_TAP,
310 post('/session/:sessionId/touch/click')).
311 put(webdriver.CommandName.TOUCH_DOUBLE_TAP,
312 post('/session/:sessionId/touch/doubleclick')).
313 put(webdriver.CommandName.TOUCH_DOWN,
314 post('/session/:sessionId/touch/down')).
315 put(webdriver.CommandName.TOUCH_UP,
316 post('/session/:sessionId/touch/up')).
317 put(webdriver.CommandName.TOUCH_MOVE,
318 post('/session/:sessionId/touch/move')).
319 put(webdriver.CommandName.TOUCH_SCROLL,
320 post('/session/:sessionId/touch/scroll')).
321 put(webdriver.CommandName.TOUCH_LONG_PRESS,
322 post('/session/:sessionId/touch/longclick')).
323 put(webdriver.CommandName.TOUCH_FLICK,
324 post('/session/:sessionId/touch/flick')).
325 put(webdriver.CommandName.ACCEPT_ALERT,
326 post('/session/:sessionId/accept_alert')).
327 put(webdriver.CommandName.DISMISS_ALERT,
328 post('/session/:sessionId/dismiss_alert')).
329 put(webdriver.CommandName.GET_ALERT_TEXT,
330 get('/session/:sessionId/alert_text')).
331 put(webdriver.CommandName.SET_ALERT_TEXT,
332 post('/session/:sessionId/alert_text')).
333 put(webdriver.CommandName.GET_LOG, post('/session/:sessionId/log')).
334 put(webdriver.CommandName.GET_AVAILABLE_LOG_TYPES,
335 get('/session/:sessionId/log/types')).
336 put(webdriver.CommandName.GET_SESSION_LOGS, post('/logs')).
337 put(webdriver.CommandName.UPLOAD_FILE, post('/session/:sessionId/file')).
338 build();
339
340 /** @constructor */
341 function Builder() {
342 var map = {};
343
344 this.put = function(name, resource) {
345 map[name] = resource;
346 return this;
347 };
348
349 this.build = function() {
350 return map;
351 };
352 }
353
354 function post(path) { return resource('POST', path); }
355 function del(path) { return resource('DELETE', path); }
356 function get(path) { return resource('GET', path); }
357 function resource(method, path) { return {method: method, path: path}; }
358})();
359
360
361/**
362 * Converts a headers object to a HTTP header block string.
363 * @param {!Object.<string>} headers The headers object to convert.
364 * @return {string} The headers as a string.
365 * @private
366 */
367webdriver.http.headersToString_ = function(headers) {
368 var ret = [];
369 for (var key in headers) {
370 ret.push(key + ': ' + headers[key]);
371 }
372 return ret.join('\n');
373};
374
375
376
377/**
378 * Describes a partial HTTP request. This class is a "partial" request and only
379 * defines the path on the server to send a request to. It is each
380 * {@link webdriver.http.Client}'s responsibility to build the full URL for the
381 * final request.
382 * @param {string} method The HTTP method to use for the request.
383 * @param {string} path Path on the server to send the request to.
384 * @param {Object=} opt_data This request's JSON data.
385 * @constructor
386 */
387webdriver.http.Request = function(method, path, opt_data) {
388
389 /**
390 * The HTTP method to use for the request.
391 * @type {string}
392 */
393 this.method = method;
394
395 /**
396 * The path on the server to send the request to.
397 * @type {string}
398 */
399 this.path = path;
400
401 /**
402 * This request's body.
403 * @type {!Object}
404 */
405 this.data = opt_data || {};
406
407 /**
408 * The headers to send with the request.
409 * @type {!Object.<(string|number)>}
410 */
411 this.headers = {'Accept': 'application/json; charset=utf-8'};
412};
413
414
415/** @override */
416webdriver.http.Request.prototype.toString = function() {
417 return [
418 this.method + ' ' + this.path + ' HTTP/1.1',
419 webdriver.http.headersToString_(this.headers),
420 '',
421 goog.json.serialize(this.data)
422 ].join('\n');
423};
424
425
426
427/**
428 * Represents a HTTP response.
429 * @param {number} status The response code.
430 * @param {!Object.<string>} headers The response headers. All header
431 * names will be converted to lowercase strings for consistent lookups.
432 * @param {string} body The response body.
433 * @constructor
434 */
435webdriver.http.Response = function(status, headers, body) {
436
437 /**
438 * The HTTP response code.
439 * @type {number}
440 */
441 this.status = status;
442
443 /**
444 * The response body.
445 * @type {string}
446 */
447 this.body = body;
448
449 /**
450 * The response body.
451 * @type {!Object.<string>}
452 */
453 this.headers = {};
454 for (var header in headers) {
455 this.headers[header.toLowerCase()] = headers[header];
456 }
457};
458
459
460/**
461 * Builds a {@link webdriver.http.Response} from a {@link XMLHttpRequest} or
462 * {@link XDomainRequest} response object.
463 * @param {!(XDomainRequest|XMLHttpRequest)} xhr The request to parse.
464 * @return {!webdriver.http.Response} The parsed response.
465 */
466webdriver.http.Response.fromXmlHttpRequest = function(xhr) {
467 var headers = {};
468
469 // getAllResponseHeaders is only available on XMLHttpRequest objects.
470 if (xhr.getAllResponseHeaders) {
471 var tmp = xhr.getAllResponseHeaders();
472 if (tmp) {
473 tmp = tmp.replace(/\r\n/g, '\n').split('\n');
474 goog.array.forEach(tmp, function(header) {
475 var parts = header.split(/\s*:\s*/, 2);
476 if (parts[0]) {
477 headers[parts[0]] = parts[1] || '';
478 }
479 });
480 }
481 }
482
483 // If xhr is a XDomainRequest object, it will not have a status.
484 // However, if we're parsing the response from a XDomainRequest, then
485 // that request must have been a success, so we can assume status == 200.
486 var status = xhr.status || 200;
487 return new webdriver.http.Response(status, headers,
488 xhr.responseText.replace(/\0/g, ''));
489};
490
491
492/** @override */
493webdriver.http.Response.prototype.toString = function() {
494 var headers = webdriver.http.headersToString_(this.headers);
495 var ret = ['HTTP/1.1 ' + this.status, headers];
496
497 if (headers) {
498 ret.push('');
499 }
500
501 if (this.body) {
502 ret.push(this.body);
503 }
504
505 return ret.join('\n');
506};