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
117 var json = goog.json.serialize({
118 'name': command.getName(),
119 'sessionId': {
120 'value': parameters['sessionId']
121 },
122 'parameters': parameters
123 });
124 this.docElement_.setAttribute(
125 webdriver.FirefoxDomExecutor.Attribute_.COMMAND, json);
126
127 var event = this.doc_.createEvent('Event');
128 event.initEvent(webdriver.FirefoxDomExecutor.EventType_.COMMAND,
129 /*canBubble=*/true, /*cancelable=*/true);
130
131 this.docElement_.dispatchEvent(event);
132};
133
134
135/** @private */
136webdriver.FirefoxDomExecutor.prototype.onResponse_ = function() {
137 if (!this.pendingCommand_) {
138 return; // Not expecting a response.
139 }
140
141 var command = this.pendingCommand_;
142 this.pendingCommand_ = null;
143
144 var json = this.docElement_.getAttribute(
145 webdriver.FirefoxDomExecutor.Attribute_.RESPONSE);
146 if (!json) {
147 command.callback(Error('Empty command response!'));
148 return;
149 }
150
151 this.docElement_.removeAttribute(
152 webdriver.FirefoxDomExecutor.Attribute_.COMMAND);
153 this.docElement_.removeAttribute(
154 webdriver.FirefoxDomExecutor.Attribute_.RESPONSE);
155
156 try {
157 var response = bot.response.checkResponse(
158 /** @type {!bot.response.ResponseObject} */ (goog.json.parse(json)));
159 } catch (ex) {
160 command.callback(ex);
161 return;
162 }
163
164 // Prior to Selenium 2.35.0, two commands are required to fully create a
165 // session: one to allocate the session, and another to fetch the
166 // capabilities.
167 if (command.name == webdriver.CommandName.NEW_SESSION &&
168 goog.isString(response['value'])) {
169 var cmd = new webdriver.Command(webdriver.CommandName.DESCRIBE_SESSION).
170 setParameter('sessionId', response['value']);
171 this.execute(cmd, command.callback);
172 } else {
173 command.callback(null, response);
174 }
175};