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