lib/webdriver/firefoxdomexecutor.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
15goog.provide('webdriver.FirefoxDomExecutor');
16
17goog.require('bot.response');
18goog.require('goog.json');
19goog.require('goog.userAgent.product');
20goog.require('webdriver.Command');
21goog.require('webdriver.CommandName');
22
23
24
25/**
26 * @constructor
27 * @implements {webdriver.CommandExecutor}
28 */
29webdriver.FirefoxDomExecutor = function() {
30 if (!webdriver.FirefoxDomExecutor.isAvailable()) {
31 throw Error(
32 'The current environment does not support the FirefoxDomExecutor');
33 }
34
35 /** @private {!Document} */
36 this.doc_ = document;
37
38 /** @private {!Element} */
39 this.docElement_ = document.documentElement;
40
41 this.docElement_.addEventListener(
42 webdriver.FirefoxDomExecutor.EventType_.RESPONSE,
43 goog.bind(this.onResponse_, this), false);
44};
45
46
47/**
48 * @return {boolean} Whether the current environment supports the
49 * FirefoxDomExecutor.
50 */
51webdriver.FirefoxDomExecutor.isAvailable = function() {
52 return goog.userAgent.product.FIREFOX &&
53 typeof document !== 'undefined' &&
54 document.documentElement &&
55 goog.isFunction(document.documentElement.hasAttribute) &&
56 document.documentElement.hasAttribute('webdriver');
57};
58
59
60/**
61 * Attributes used to communicate with the FirefoxDriver extension.
62 * @enum {string}
63 * @private
64 */
65webdriver.FirefoxDomExecutor.Attribute_ = {
66 COMMAND: 'command',
67 RESPONSE: 'response'
68};
69
70
71/**
72 * Events used to communicate with the FirefoxDriver extension.
73 * @enum {string}
74 * @private
75 */
76webdriver.FirefoxDomExecutor.EventType_ = {
77 COMMAND: 'webdriverCommand',
78 RESPONSE: 'webdriverResponse'
79};
80
81
82/**
83 * The pending command, if any.
84 * @private {?{name:string, callback:!Function}}
85 */
86webdriver.FirefoxDomExecutor.prototype.pendingCommand_ = null;
87
88
89/** @override */
90webdriver.FirefoxDomExecutor.prototype.execute = function(command, callback) {
91 if (this.pendingCommand_) {
92 throw Error('Currently awaiting a command response!');
93 }
94
95 this.pendingCommand_ = {
96 name: command.getName(),
97 callback: callback
98 };
99
100 var parameters = command.getParameters();
101
102 // There are two means for communicating with the FirefoxDriver: via
103 // HTTP using WebDriver's wire protocol and over the DOM using a custom
104 // JSON protocol. This class uses the latter. When the FirefoxDriver receives
105 // commands over HTTP, it builds a parameters object from the URL parameters.
106 // When an element ID is sent in the URL, it'll be decoded as just id:string
107 // instead of id:{ELEMENT:string}. When switching to a frame by element,
108 // however, the element ID is not sent through the URL, so we must make sure
109 // to encode that parameter properly here. It would be nice if we unified
110 // the two protocols used by the FirefoxDriver...
111 if (parameters['id'] &&
112 parameters['id']['ELEMENT'] &&
113 command.getName() != webdriver.CommandName.SWITCH_TO_FRAME) {
114 parameters['id'] = parameters['id']['ELEMENT'];
115 }
116 var json = goog.json.serialize({
117 'name': command.getName(),
118 'sessionId': parameters['sessionId'],
119 'parameters': parameters
120 });
121 this.docElement_.setAttribute(
122 webdriver.FirefoxDomExecutor.Attribute_.COMMAND, json);
123
124 var event = this.doc_.createEvent('Event');
125 event.initEvent(webdriver.FirefoxDomExecutor.EventType_.COMMAND,
126 /*canBubble=*/true, /*cancelable=*/true);
127
128 this.docElement_.dispatchEvent(event);
129};
130
131
132/** @private */
133webdriver.FirefoxDomExecutor.prototype.onResponse_ = function() {
134 if (!this.pendingCommand_) {
135 return; // Not expecting a response.
136 }
137
138 var command = this.pendingCommand_;
139 this.pendingCommand_ = null;
140
141 var json = this.docElement_.getAttribute(
142 webdriver.FirefoxDomExecutor.Attribute_.RESPONSE);
143 if (!json) {
144 command.callback(Error('Empty command response!'));
145 return;
146 }
147
148 this.docElement_.removeAttribute(
149 webdriver.FirefoxDomExecutor.Attribute_.COMMAND);
150 this.docElement_.removeAttribute(
151 webdriver.FirefoxDomExecutor.Attribute_.RESPONSE);
152
153 try {
154 var response = bot.response.checkResponse(
155 /** @type {!bot.response.ResponseObject} */ (goog.json.parse(json)));
156 } catch (ex) {
157 command.callback(ex);
158 return;
159 }
160
161 // Prior to Selenium 2.35.0, two commands are required to fully create a
162 // session: one to allocate the session, and another to fetch the
163 // capabilities.
164 if (command.name == webdriver.CommandName.NEW_SESSION &&
165 goog.isString(response['value'])) {
166 var cmd = new webdriver.Command(webdriver.CommandName.DESCRIBE_SESSION).
167 setParameter('sessionId', response['value']);
168 this.execute(cmd, command.callback);
169 } else {
170 command.callback(null, response);
171 }
172};