selenium-webdriver 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/chrome/prebuilt/Win32/Release/npchromedriver.dll +0 -0
- data/chrome/prebuilt/x64/Release/npchromedriver.dll +0 -0
- data/chrome/src/extension/background.html +9 -0
- data/chrome/src/extension/background.js +933 -0
- data/chrome/src/extension/content_script.js +1286 -0
- data/chrome/src/extension/manifest-nonwin.json +15 -0
- data/chrome/src/extension/manifest-win.json +16 -0
- data/chrome/src/extension/toolstrip.html +28 -0
- data/chrome/src/extension/utils.js +196 -0
- data/chrome/src/rb/lib/selenium/webdriver/chrome.rb +8 -0
- data/chrome/src/rb/lib/selenium/webdriver/chrome/bridge.rb +324 -0
- data/chrome/src/rb/lib/selenium/webdriver/chrome/command_executor.rb +70 -0
- data/chrome/src/rb/lib/selenium/webdriver/chrome/launcher.rb +119 -0
- data/common/src/js/abstractcommandprocessor.js +161 -0
- data/common/src/js/asserts.js +296 -0
- data/common/src/js/by.js +147 -0
- data/common/src/js/command.js +274 -0
- data/common/src/js/context.js +58 -0
- data/common/src/js/extension/README +2 -0
- data/common/src/js/extension/dommessenger.js +152 -0
- data/common/src/js/factory.js +55 -0
- data/common/src/js/future.js +118 -0
- data/common/src/js/key.js +117 -0
- data/common/src/js/localcommandprocessor.js +181 -0
- data/common/src/js/logging.js +249 -0
- data/common/src/js/testrunner.js +605 -0
- data/common/src/js/timing.js +89 -0
- data/common/src/js/wait.js +199 -0
- data/common/src/js/webdriver.js +853 -0
- data/common/src/js/webelement.js +683 -0
- data/common/src/rb/lib/selenium-webdriver.rb +1 -0
- data/common/src/rb/lib/selenium/webdriver.rb +52 -0
- data/common/src/rb/lib/selenium/webdriver/bridge_helper.rb +88 -0
- data/common/src/rb/lib/selenium/webdriver/child_process.rb +85 -0
- data/common/src/rb/lib/selenium/webdriver/core_ext/dir.rb +41 -0
- data/common/src/rb/lib/selenium/webdriver/driver.rb +128 -0
- data/common/src/rb/lib/selenium/webdriver/element.rb +126 -0
- data/common/src/rb/lib/selenium/webdriver/error.rb +68 -0
- data/common/src/rb/lib/selenium/webdriver/find.rb +69 -0
- data/common/src/rb/lib/selenium/webdriver/navigation.rb +23 -0
- data/common/src/rb/lib/selenium/webdriver/options.rb +50 -0
- data/common/src/rb/lib/selenium/webdriver/platform.rb +82 -0
- data/common/src/rb/lib/selenium/webdriver/target_locator.rb +23 -0
- data/firefox/prebuilt/nsICommandProcessor.xpt +0 -0
- data/firefox/prebuilt/nsINativeEvents.xpt +0 -0
- data/firefox/prebuilt/nsIResponseHandler.xpt +0 -0
- data/firefox/src/extension/chrome.manifest +3 -0
- data/firefox/src/extension/components/context.js +37 -0
- data/firefox/src/extension/components/driver-component.js +127 -0
- data/firefox/src/extension/components/firefoxDriver.js +706 -0
- data/firefox/src/extension/components/json2.js +273 -0
- data/firefox/src/extension/components/keytest.html +554 -0
- data/firefox/src/extension/components/nsCommandProcessor.js +586 -0
- data/firefox/src/extension/components/screenshooter.js +70 -0
- data/firefox/src/extension/components/socketListener.js +185 -0
- data/firefox/src/extension/components/utils.js +1200 -0
- data/firefox/src/extension/components/webLoadingListener.js +57 -0
- data/firefox/src/extension/components/webdriverserver.js +101 -0
- data/firefox/src/extension/components/wrappedElement.js +609 -0
- data/firefox/src/extension/content/fxdriver.xul +30 -0
- data/firefox/src/extension/content/server.js +95 -0
- data/firefox/src/extension/idl/nsICommandProcessor.idl +38 -0
- data/firefox/src/extension/idl/nsIResponseHandler.idl +34 -0
- data/firefox/src/extension/install.rdf +29 -0
- data/firefox/src/rb/lib/selenium/webdriver/firefox.rb +21 -0
- data/firefox/src/rb/lib/selenium/webdriver/firefox/binary.rb +86 -0
- data/firefox/src/rb/lib/selenium/webdriver/firefox/bridge.rb +426 -0
- data/firefox/src/rb/lib/selenium/webdriver/firefox/extension_connection.rb +82 -0
- data/firefox/src/rb/lib/selenium/webdriver/firefox/launcher.rb +132 -0
- data/firefox/src/rb/lib/selenium/webdriver/firefox/profile.rb +174 -0
- data/firefox/src/rb/lib/selenium/webdriver/firefox/profiles_ini.rb +60 -0
- data/firefox/src/rb/lib/selenium/webdriver/firefox/util.rb +23 -0
- data/jobbie/prebuilt/Win32/Release/InternetExplorerDriver.dll +0 -0
- data/jobbie/prebuilt/x64/Release/InternetExplorerDriver.dll +0 -0
- data/jobbie/src/rb/lib/selenium/webdriver/ie.rb +14 -0
- data/jobbie/src/rb/lib/selenium/webdriver/ie/bridge.rb +552 -0
- data/jobbie/src/rb/lib/selenium/webdriver/ie/lib.rb +94 -0
- data/jobbie/src/rb/lib/selenium/webdriver/ie/util.rb +147 -0
- data/remote/client/src/rb/lib/selenium/webdriver/remote.rb +16 -0
- data/remote/client/src/rb/lib/selenium/webdriver/remote/bridge.rb +374 -0
- data/remote/client/src/rb/lib/selenium/webdriver/remote/capabilities.rb +105 -0
- data/remote/client/src/rb/lib/selenium/webdriver/remote/commands.rb +53 -0
- data/remote/client/src/rb/lib/selenium/webdriver/remote/default_http_client.rb +71 -0
- data/remote/client/src/rb/lib/selenium/webdriver/remote/response.rb +43 -0
- data/remote/client/src/rb/lib/selenium/webdriver/remote/server_error.rb +32 -0
- metadata +182 -0
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All functions which take elements assume that they are not null,
|
|
3
|
+
* and are present as passed on the DOM.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
ChromeDriverContentScript = {};
|
|
7
|
+
|
|
8
|
+
ChromeDriverContentScript.internalElementArray = [];
|
|
9
|
+
ChromeDriverContentScript.port = null;
|
|
10
|
+
ChromeDriverContentScript.currentDocument = window.document;
|
|
11
|
+
ChromeDriverContentScript.injectedEmbedElement = null;
|
|
12
|
+
//Record this for async calls (execute), so returner knows what to return
|
|
13
|
+
//(Also so that we can not re-start commands we have already started executing)
|
|
14
|
+
ChromeDriverContentScript.currentSequenceNumber = -1;
|
|
15
|
+
|
|
16
|
+
if (ChromeDriverContentScript.currentDocument.location != "about:blank") {
|
|
17
|
+
//If loading windows using window.open, the port is opened
|
|
18
|
+
//while we are on about:blank (which always reports window.name as ''),
|
|
19
|
+
//and we use port-per-tab semantics, so don't open the port if
|
|
20
|
+
//we're on about:blank
|
|
21
|
+
ChromeDriverContentScript.port = chrome.extension.connect({name: window.name});
|
|
22
|
+
ChromeDriverContentScript.port.onMessage.addListener(parsePortMessage);
|
|
23
|
+
var isFrameset = (ChromeDriverContentScript.currentDocument.getElementsByTagName("frameset").length > 0);
|
|
24
|
+
ChromeDriverContentScript.port.postMessage({response: {response: "newTabInformation",
|
|
25
|
+
value: {statusCode: "no-op", isFrameset: isFrameset, frameCount: window.frames.length,
|
|
26
|
+
portName: ChromeDriverContentScript.port.name}}, sequenceNumber: -1});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse messages coming in on the port.
|
|
31
|
+
* Sends relevant response back down the port
|
|
32
|
+
* @param message JSON message of format:
|
|
33
|
+
* {request: {request: "some command"
|
|
34
|
+
* [, optional params]},
|
|
35
|
+
* sequenceNumber: some sequence number
|
|
36
|
+
* [, followup: message']}
|
|
37
|
+
* where message' is a message to parse after this one
|
|
38
|
+
*/
|
|
39
|
+
function parsePortMessage(message) {
|
|
40
|
+
if (message == null || message.request == null) {
|
|
41
|
+
console.log("Received bad request: " + JSON.stringify(message));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (message.sequenceNumber <= ChromeDriverContentScript.currentSequenceNumber) {
|
|
45
|
+
console.log("Already process{ed,ing} request with sequence number: " + message.sequenceNumber + ", ignoring request: " + message);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ChromeDriverContentScript.currentSequenceNumber = message.sequenceNumber;
|
|
50
|
+
|
|
51
|
+
console.log("Received request: " + JSON.stringify(message) + " (" + window.name + ")");
|
|
52
|
+
//wait indicates whether this is a potentially page-changing change (see background.js's sendResponseByXHR)
|
|
53
|
+
var response = {response: message.request.request, value: null, wait: true};
|
|
54
|
+
if (message.request.elementId !== undefined && message.request.elementId != null) {
|
|
55
|
+
//If it seems an elementId has been passed, try to resolve that to an element
|
|
56
|
+
try {
|
|
57
|
+
var element = internalGetElement(message.request.elementId);
|
|
58
|
+
} catch(e) {
|
|
59
|
+
response.value = e;
|
|
60
|
+
ChromeDriverContentScript.port.postMessage({response: response, sequenceNumber: message.sequenceNumber});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
switch (message.request.request) {
|
|
65
|
+
case "addCookie":
|
|
66
|
+
response.value = setCookie(message.request.cookie);
|
|
67
|
+
response.wait = false;
|
|
68
|
+
break;
|
|
69
|
+
case "clearElement":
|
|
70
|
+
response.value = clearElement(element);
|
|
71
|
+
break;
|
|
72
|
+
case "clickElement":
|
|
73
|
+
response.value = clickElement(element, message.request.elementId);
|
|
74
|
+
break;
|
|
75
|
+
case "nonNativeClickElement":
|
|
76
|
+
//TODO(danielwh): Focus/blur events for non-native clicking
|
|
77
|
+
element.scrollIntoView(true);
|
|
78
|
+
//TODO: Work out a way of firing events,
|
|
79
|
+
//now that synthesising them gives appendMessage errors
|
|
80
|
+
Utils.fireMouseEventOn(element, "mousedown");
|
|
81
|
+
Utils.fireMouseEventOn(element, "mouseup");
|
|
82
|
+
Utils.fireMouseEventOn(element, "click");
|
|
83
|
+
if (element.click) {
|
|
84
|
+
element.click();
|
|
85
|
+
}
|
|
86
|
+
response.value = {statusCode: 0};
|
|
87
|
+
break;
|
|
88
|
+
case "deleteAllCookies":
|
|
89
|
+
response.value = deleteAllCookies();
|
|
90
|
+
response.wait = false;
|
|
91
|
+
break;
|
|
92
|
+
case "deleteCookie":
|
|
93
|
+
response.value = deleteCookie(message.request.name);
|
|
94
|
+
response.wait = false;
|
|
95
|
+
break;
|
|
96
|
+
case "executeScript":
|
|
97
|
+
execute(message.request.script, message.request.args);
|
|
98
|
+
//Sends port message back to background page from its own callback
|
|
99
|
+
break;
|
|
100
|
+
case "getCookies":
|
|
101
|
+
response.value = getCookies();
|
|
102
|
+
response.wait = false;
|
|
103
|
+
break;
|
|
104
|
+
case "getCookie":
|
|
105
|
+
response.value = getCookieNamed(message.request.name);
|
|
106
|
+
response.wait = false;
|
|
107
|
+
break;
|
|
108
|
+
case "findChildElement":
|
|
109
|
+
response.value = getElement(false, message.request.using, message.request.value, message.request.id);
|
|
110
|
+
response.wait = false;
|
|
111
|
+
break;
|
|
112
|
+
case "findChildElements":
|
|
113
|
+
response.value = getElement(true, message.request.using, message.request.value, message.request.id);
|
|
114
|
+
response.wait = false;
|
|
115
|
+
break;
|
|
116
|
+
case "findElement":
|
|
117
|
+
response.value = getElement(false, message.request.using, message.request.value);
|
|
118
|
+
response.wait = false;
|
|
119
|
+
break;
|
|
120
|
+
case "findElements":
|
|
121
|
+
response.value = getElement(true, message.request.using, message.request.value);
|
|
122
|
+
response.wait = false;
|
|
123
|
+
break;
|
|
124
|
+
case "getElementAttribute":
|
|
125
|
+
response.value = getElementAttribute(element, message.request.attribute);
|
|
126
|
+
response.wait = false;
|
|
127
|
+
break;
|
|
128
|
+
case "getElementValueOfCssProperty":
|
|
129
|
+
response.value = {statusCode: 0, value: getStyle(element, message.request.css)};
|
|
130
|
+
response.wait = false;
|
|
131
|
+
break;
|
|
132
|
+
case "getElementLocation":
|
|
133
|
+
var coords = getElementCoords(element);
|
|
134
|
+
response.value = {statusCode: 0, value: {type: "POINT", x: coords[0], y: coords[1]}};
|
|
135
|
+
response.wait = false;
|
|
136
|
+
break;
|
|
137
|
+
case "getElementLocationOnceScrolledIntoView":
|
|
138
|
+
element.scrollIntoView(true);
|
|
139
|
+
var coords = getElementCoords(element);
|
|
140
|
+
response.value = {statusCode: 0, value: {type: "POINT", x: coords[0], y: coords[1]}};
|
|
141
|
+
break;
|
|
142
|
+
case "getElementSize":
|
|
143
|
+
response.value = {statusCode: 0, value: getOffsetSizeFromSubElements(element)};
|
|
144
|
+
response.wait = false;
|
|
145
|
+
break;
|
|
146
|
+
case "getElementTagName":
|
|
147
|
+
response.value = {statusCode: 0, value: element.tagName.toLowerCase()};
|
|
148
|
+
response.wait = false;
|
|
149
|
+
break;
|
|
150
|
+
case "getElementText":
|
|
151
|
+
response.value = {statusCode: 0, value: Utils.getText(element)};
|
|
152
|
+
response.wait = false;
|
|
153
|
+
break;
|
|
154
|
+
case "getElementValue":
|
|
155
|
+
response.value = {statusCode: 0, value: element.value};
|
|
156
|
+
response.wait = false;
|
|
157
|
+
break;
|
|
158
|
+
case "getFrameNameFromIndex":
|
|
159
|
+
//TODO(danielwh): Do this by simply looking it up in window.frames when Chrome is fixed. Track: crbug.com 20773
|
|
160
|
+
getFrameNameFromIndex(message.request.index);
|
|
161
|
+
break;
|
|
162
|
+
case "getPageSource":
|
|
163
|
+
response.value = getSource();
|
|
164
|
+
response.wait = false;
|
|
165
|
+
break;
|
|
166
|
+
case "getTitle":
|
|
167
|
+
response.value = {statusCode: 0, value: ChromeDriverContentScript.currentDocument.title};
|
|
168
|
+
response.wait = false;
|
|
169
|
+
break;
|
|
170
|
+
case "getCurrentUrl":
|
|
171
|
+
response.value = {statusCode: 0, value: ChromeDriverContentScript.currentDocument.location.href};
|
|
172
|
+
response.wait = false;
|
|
173
|
+
break;
|
|
174
|
+
case "goBack":
|
|
175
|
+
history.back();
|
|
176
|
+
response.value = {statusCode: 0};
|
|
177
|
+
break;
|
|
178
|
+
case "goForward":
|
|
179
|
+
history.forward();
|
|
180
|
+
response.value = {statusCode: 0};
|
|
181
|
+
break;
|
|
182
|
+
case "hoverOverElement":
|
|
183
|
+
response.value = hoverElement(element, message.request.elementId);
|
|
184
|
+
break;
|
|
185
|
+
case "injectEmbed":
|
|
186
|
+
injectEmbed();
|
|
187
|
+
break;
|
|
188
|
+
case "isElementDisplayed":
|
|
189
|
+
response.value = {statusCode: 0, value: isElementDisplayed(element)};
|
|
190
|
+
response.wait = false;
|
|
191
|
+
break;
|
|
192
|
+
case "isElementEnabled":
|
|
193
|
+
response.value = {statusCode: 0, value: !element.disabled};
|
|
194
|
+
response.wait = false;
|
|
195
|
+
break;
|
|
196
|
+
case "isElementSelected":
|
|
197
|
+
response.value = {statusCode: 0, value: findWhetherElementIsSelected(element)};
|
|
198
|
+
response.wait = false;
|
|
199
|
+
break;
|
|
200
|
+
case "refresh":
|
|
201
|
+
ChromeDriverContentScript.currentDocument.location.reload(true);
|
|
202
|
+
response.value = {statusCode: 0};
|
|
203
|
+
break;
|
|
204
|
+
case "sendKeysToElement":
|
|
205
|
+
response.value = sendElementKeys(element, message.request.keys, message.request.elementId);
|
|
206
|
+
response.wait = false;
|
|
207
|
+
break;
|
|
208
|
+
case "sendElementNonNativeKeys":
|
|
209
|
+
response.value = sendElementNonNativeKeys(element, message.request.keys);
|
|
210
|
+
response.wait = false;
|
|
211
|
+
break;
|
|
212
|
+
case "setElementSelected":
|
|
213
|
+
response.value = selectElement(element);
|
|
214
|
+
break;
|
|
215
|
+
case "getActiveElement":
|
|
216
|
+
response.value = {statusCode: 0, value: [addElementToInternalArray(ChromeDriverContentScript.currentDocument.activeElement).toString()]};
|
|
217
|
+
response.wait = false;
|
|
218
|
+
break;
|
|
219
|
+
case "switchToNamedIFrameIfOneExists":
|
|
220
|
+
response.value = switchToNamedIFrameIfOneExists(message.request.name);
|
|
221
|
+
response.wait = false;
|
|
222
|
+
break;
|
|
223
|
+
case "submitElement":
|
|
224
|
+
response.value = submitElement(element);
|
|
225
|
+
break;
|
|
226
|
+
case "toggleElement":
|
|
227
|
+
response.value = toggleElement(element);
|
|
228
|
+
break;
|
|
229
|
+
case "sniffForMetaRedirects":
|
|
230
|
+
response.value = sniffForMetaRedirects();
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
response.value = {statusCode: 9, value: {message: message.request.request + " is unsupported"}};
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
if (response.value != null) {
|
|
237
|
+
ChromeDriverContentScript.port.postMessage({response: response, sequenceNumber: message.sequenceNumber})
|
|
238
|
+
console.log("Sent response: " + JSON.stringify(response) + " (seq:" + message.sequenceNumber + ")");
|
|
239
|
+
}
|
|
240
|
+
if (message.request.followup) {
|
|
241
|
+
setTimeout(parsePortMessage(message.request.followup), 100);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Deletes all cookies accessible from the current page.
|
|
247
|
+
*/
|
|
248
|
+
function deleteAllCookies() {
|
|
249
|
+
var cookies = getAllCookiesAsStrings();
|
|
250
|
+
for (var i = 0; i < cookies.length; ++i) {
|
|
251
|
+
var cookie = cookies[i].split("=");
|
|
252
|
+
deleteCookie(cookie[0]);
|
|
253
|
+
}
|
|
254
|
+
return {statusCode: 0};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Deletes the cookie with the passed name, accessible from the current page (i.e. with path of the current directory or above)
|
|
259
|
+
* @param cookieName name of the cookie to delete
|
|
260
|
+
*/
|
|
261
|
+
function deleteCookie(cookieName) {
|
|
262
|
+
//It's possible we're trying to delete cookies within iframes.
|
|
263
|
+
//iframe stuff is unsupported in Chrome at the moment (track crbug.com/20773)
|
|
264
|
+
//But for the iframe to be loaded and have cookies, it must be of same origin,
|
|
265
|
+
//so we'll try deleting the cookie as if it was on this page anyway...
|
|
266
|
+
//(Yes, this is a hack)
|
|
267
|
+
//TODO(danielwh): Remove the cookieDocument stuff when Chrome fix frame support
|
|
268
|
+
try {
|
|
269
|
+
var fullpath = ChromeDriverContentScript.currentDocument.location.pathname;
|
|
270
|
+
var cookieDocument = ChromeDriverContentScript.currentDocument;
|
|
271
|
+
} catch (e) {
|
|
272
|
+
console.log("Falling back on non-iframe document to delete cookies");
|
|
273
|
+
var cookieDocument = document;
|
|
274
|
+
var fullpath = cookieDocument.location.pathname;
|
|
275
|
+
}
|
|
276
|
+
var hostParts = cookieDocument.location.hostname.split(".");
|
|
277
|
+
|
|
278
|
+
fullpath = fullpath.split('/');
|
|
279
|
+
fullpath.pop(); //Get rid of the file
|
|
280
|
+
//TODO(danielwh): Tidy up these loops and this repeated code
|
|
281
|
+
for (var segment in fullpath) {
|
|
282
|
+
var path = '';
|
|
283
|
+
for (var i = 0; i < segment; ++i) {
|
|
284
|
+
path += fullpath[i + 1] + '/';
|
|
285
|
+
}
|
|
286
|
+
//Delete cookie with trailing /
|
|
287
|
+
cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path;
|
|
288
|
+
//Delete cookie without trailing /
|
|
289
|
+
cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path.substring(0, path.length - 1);
|
|
290
|
+
|
|
291
|
+
var domain = "";
|
|
292
|
+
for (var i = hostParts.length - 1; i >= 0; --i) {
|
|
293
|
+
domain = "." + hostParts[i] + domain;
|
|
294
|
+
//Delete cookie with trailing /
|
|
295
|
+
cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path + ";domain=" + domain;
|
|
296
|
+
|
|
297
|
+
cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path + ";domain=" + domain;
|
|
298
|
+
//Delete cookie without trailing /
|
|
299
|
+
cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path.substring(0, path.length - 1) + ";domain=" + domain;
|
|
300
|
+
|
|
301
|
+
cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path.substring(0, path.length - 1) + ";domain=" + domain.substring(1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {statusCode: 0};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get all cookies accessible from the current page as an array of
|
|
309
|
+
* {name: some name, value: some value, secure: false} values
|
|
310
|
+
*/
|
|
311
|
+
function getCookies() {
|
|
312
|
+
var cookies = [];
|
|
313
|
+
var cookieStrings = getAllCookiesAsStrings();
|
|
314
|
+
for (var i = 0; i < cookieStrings.length; ++i) {
|
|
315
|
+
var cookie = cookieStrings[i].split("=");
|
|
316
|
+
cookies.push({type: "COOKIE", name: cookie[0], value: cookie[1], secure: false});
|
|
317
|
+
}
|
|
318
|
+
return {statusCode: 0, value: cookies};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get the cookie accessible from the current page with the passed name
|
|
323
|
+
* @param name name of the cookie
|
|
324
|
+
*/
|
|
325
|
+
function getCookieNamed(name) {
|
|
326
|
+
var cookies = [];
|
|
327
|
+
var cookieStrings = getAllCookiesAsStrings();
|
|
328
|
+
for (var i = 0; i < cookieStrings.length; ++i) {
|
|
329
|
+
var cookie = cookieStrings[i].split("=");
|
|
330
|
+
if (cookie[0] == name) {
|
|
331
|
+
return {statusCode: 0, value: {type: "COOKIE", name: cookie[0], value: cookie[1], secure: false}};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {statusCode: 0, value: null};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Gets all cookies accessible from the current page as an array of
|
|
339
|
+
* key=value strings
|
|
340
|
+
*/
|
|
341
|
+
function getAllCookiesAsStrings() {
|
|
342
|
+
//It's possible we're trying to delete cookies within iframes.
|
|
343
|
+
//iframe stuff is unsupported in Chrome at the moment (track crbug.com/20773)
|
|
344
|
+
//But for the iframe to be loaded and have cookies, it must be of same origin,
|
|
345
|
+
//so we'll try deleting the cookie as if it was on this page anyway...
|
|
346
|
+
//(Yes, this is a hack)
|
|
347
|
+
//TODO(danielwh): Remove the cookieDocument stuff when Chrome fix frame support
|
|
348
|
+
var cookieDocument = ChromeDriverContentScript.currentDocument;
|
|
349
|
+
try {
|
|
350
|
+
var tempFullpath = ChromeDriverContentScript.currentDocument.location.pathname;
|
|
351
|
+
} catch (e) {
|
|
352
|
+
cookieDocument = document;
|
|
353
|
+
}
|
|
354
|
+
var cookieStrings = cookieDocument.cookie.split('; ');
|
|
355
|
+
var cookies = [];
|
|
356
|
+
for (var i = 0; i < cookieStrings.length; ++i) {
|
|
357
|
+
if (cookieStrings[i] == '') {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
cookies.push(cookieStrings[i]);
|
|
361
|
+
}
|
|
362
|
+
return cookies;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Add the passed cookie to the page's cookies
|
|
367
|
+
* @param cookie org.openqa.selenium.Cookie to add
|
|
368
|
+
*/
|
|
369
|
+
function setCookie(cookie) {
|
|
370
|
+
//It's possible we're trying to delete cookies within iframes.
|
|
371
|
+
//iframe stuff is unsupported in Chrome at the moment (track crbug.com/20773)
|
|
372
|
+
//But for the iframe to be loaded and have cookies, it must be of same origin,
|
|
373
|
+
//so we'll try deleting the cookie as if it was on this page anyway...
|
|
374
|
+
//(Yes, this is a hack)
|
|
375
|
+
//TODO(danielwh): Remove the cookieDocument stuff when Chrome fix frame support
|
|
376
|
+
try {
|
|
377
|
+
var currLocation = ChromeDriverContentScript.currentDocument.location;
|
|
378
|
+
var currDomain = currLocation.host;
|
|
379
|
+
var cookieDocument = ChromeDriverContentScript.currentDocument;
|
|
380
|
+
} catch (e) {
|
|
381
|
+
cookieDocument = document;
|
|
382
|
+
var currLocation = ChromeDriverContentScript.currentDocument.location;
|
|
383
|
+
var currDomain = currLocation.host;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (currLocation.port != 80) { currDomain += ":" + currLocation.port; }
|
|
387
|
+
if (cookie.domain != null && cookie.domain !== undefined &&
|
|
388
|
+
currDomain.indexOf(cookie.domain) == -1) {
|
|
389
|
+
// Not quite right, but close enough. (See r783)
|
|
390
|
+
return {statusCode: 2, value: {
|
|
391
|
+
message: "You may only set cookies for the current domain"}};
|
|
392
|
+
} else if (guessPageType() != "html") {
|
|
393
|
+
return {statusCode: 2, value: {
|
|
394
|
+
message: "You may only set cookies on html documents"}};
|
|
395
|
+
} else {
|
|
396
|
+
cookieDocument.cookie = cookie.name + '=' + escape(cookie.value) +
|
|
397
|
+
((cookie.expiry == null || cookie.expiry === undefined) ?
|
|
398
|
+
'' : ';expires=' + (new Date(cookie.expiry.time)).toGMTString()) +
|
|
399
|
+
((cookie.path == null || cookie.path === undefined) ?
|
|
400
|
+
'' : ';path=' + cookie.path);
|
|
401
|
+
return {statusCode: 0};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get an element, or a set of elements, by some lookup
|
|
407
|
+
* Called by both findElement and findElements
|
|
408
|
+
* @param plural true if want array of all elements, false if singular element
|
|
409
|
+
* @param parsed array showing how to look up, e.g. ["id", "cheese"] or
|
|
410
|
+
* [{"id": 0, using: "id", value: "cheese"}]
|
|
411
|
+
*/
|
|
412
|
+
function getElement(plural, lookupBy, lookupValue, id) {
|
|
413
|
+
var root = "";
|
|
414
|
+
var parent = null;
|
|
415
|
+
if (id !== undefined && id != null) {
|
|
416
|
+
try {
|
|
417
|
+
parent = internalGetElement(id);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
return e;
|
|
420
|
+
}
|
|
421
|
+
//Looking for children
|
|
422
|
+
root = getXPathOfElement(parent);
|
|
423
|
+
} else {
|
|
424
|
+
parent = ChromeDriverContentScript.currentDocument;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
var elements = [];
|
|
428
|
+
var attribute = '';
|
|
429
|
+
switch (lookupBy) {
|
|
430
|
+
case "class name":
|
|
431
|
+
elements = getElementsByXPath(root +
|
|
432
|
+
"//*[contains(concat(' ',normalize-space(@class),' '),' " +
|
|
433
|
+
lookupValue + " ')]");
|
|
434
|
+
break;
|
|
435
|
+
case "name":
|
|
436
|
+
attribute = 'name';
|
|
437
|
+
break;
|
|
438
|
+
case "id":
|
|
439
|
+
attribute = 'id';
|
|
440
|
+
break;
|
|
441
|
+
case "link text":
|
|
442
|
+
elements = getElementsByLinkText(parent, lookupValue);
|
|
443
|
+
break;
|
|
444
|
+
case "partial link text":
|
|
445
|
+
elements = getElementsByPartialLinkText(parent, lookupValue);
|
|
446
|
+
break;
|
|
447
|
+
case "tag name":
|
|
448
|
+
elements = getElementsByXPath(root + "//" + lookupValue);
|
|
449
|
+
break;
|
|
450
|
+
case "xpath":
|
|
451
|
+
if (root != "") {
|
|
452
|
+
//Because xpath is relative to the parent, if there is a parent, we add a /
|
|
453
|
+
root = root + "/";
|
|
454
|
+
}
|
|
455
|
+
var xpath = root + lookupValue;
|
|
456
|
+
try {
|
|
457
|
+
elements = getElementsByXPath(xpath);
|
|
458
|
+
} catch (e) {
|
|
459
|
+
return {statusCode: 19, value: {message: "Could not look up element by xpath " + xpath}};
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
if (attribute != '') {
|
|
464
|
+
elements = getElementsByXPath(root + "//*[@" + attribute + "='" + lookupValue + "']");
|
|
465
|
+
}
|
|
466
|
+
if (elements == null || elements.length == 0) {
|
|
467
|
+
if (plural) {
|
|
468
|
+
//Fine, no elements matched
|
|
469
|
+
return {statusCode: 0, value: []};
|
|
470
|
+
} else {
|
|
471
|
+
//Problem - we were expecting an element
|
|
472
|
+
return {statusCode: 7, value: {
|
|
473
|
+
message: "Unable to find element with " + lookupBy + " " + lookupValue}};
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
var elementsToReturnArray = [];
|
|
477
|
+
if (plural) {
|
|
478
|
+
//Add all found elements to the page's elements, and push each to the array to return
|
|
479
|
+
var addedElements = addElementsToInternalArray(elements);
|
|
480
|
+
for (var addedElement in addedElements) {
|
|
481
|
+
elementsToReturnArray.push(addedElements[addedElement].toString());
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
if (!elements[0]) {
|
|
485
|
+
return {statusCode: 7, value: {
|
|
486
|
+
message: "Unable to find element with " + lookupBy + " " + lookupValue}};
|
|
487
|
+
}
|
|
488
|
+
//Add the first found elements to the page's elements, and push it to the array to return
|
|
489
|
+
elementsToReturnArray.push(addElementToInternalArray(elements[0]).toString());
|
|
490
|
+
}
|
|
491
|
+
return {statusCode: 0, value: elementsToReturnArray};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function addElementToInternalArray(element) {
|
|
496
|
+
for (var existingElement in ChromeDriverContentScript.internalElementArray) {
|
|
497
|
+
if (element == ChromeDriverContentScript.internalElementArray[existingElement]) {
|
|
498
|
+
return existingElement;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
ChromeDriverContentScript.internalElementArray.push(element);
|
|
502
|
+
return (ChromeDriverContentScript.internalElementArray.length - 1);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function addElementsToInternalArray(elements) {
|
|
506
|
+
var toReturn = [];
|
|
507
|
+
for (var element in elements) {
|
|
508
|
+
toReturn.push(addElementToInternalArray(elements[element]));
|
|
509
|
+
}
|
|
510
|
+
return toReturn;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Gets an element which we have previously looked up by its internal ID
|
|
515
|
+
* @param elementIdAsString the element's internal ID
|
|
516
|
+
* @return element with the passed internal ID
|
|
517
|
+
* @throws if element ID was stale: wrapped up
|
|
518
|
+
* org.openqa.selenium.StaleElementReferenceException ready to send
|
|
519
|
+
*/
|
|
520
|
+
function internalGetElement(elementIdAsString) {
|
|
521
|
+
var elementId = parseInt(elementIdAsString);
|
|
522
|
+
if (elementId != null && elementId >= 0 &&
|
|
523
|
+
ChromeDriverContentScript.internalElementArray.length >= elementId + 1) {
|
|
524
|
+
var element = ChromeDriverContentScript.internalElementArray[elementId];
|
|
525
|
+
var parent = element;
|
|
526
|
+
while (parent && parent != element.ownerDocument.documentElement) {
|
|
527
|
+
parent = parent.parentNode;
|
|
528
|
+
}
|
|
529
|
+
if (parent !== element.ownerDocument.documentElement) {
|
|
530
|
+
throw {statusCode: 10, value: {message: "Element is obsolete"}};
|
|
531
|
+
}
|
|
532
|
+
return element;
|
|
533
|
+
} else {
|
|
534
|
+
throw {statusCode: 10, value: {message: "Element is obsolete"}};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Ensures the passed element is in view, so that the native click event can be sent
|
|
540
|
+
* @return object to send back to background page to trigger a native click
|
|
541
|
+
*/
|
|
542
|
+
function clickElement(element, elementId) {
|
|
543
|
+
try {
|
|
544
|
+
checkElementIsDisplayed(element)
|
|
545
|
+
} catch (e) {
|
|
546
|
+
return e;
|
|
547
|
+
}
|
|
548
|
+
element.scrollIntoView(true);
|
|
549
|
+
var size = getOffsetSizeFromSubElements(element);
|
|
550
|
+
var coords = getElementCoords(element);
|
|
551
|
+
return {statusCode: "no-op", elementId: elementId,
|
|
552
|
+
x: parseInt(coords[0] - ChromeDriverContentScript.currentDocument.body.scrollLeft + (size.width ? size.width / 2 : 0)),
|
|
553
|
+
y: parseInt(coords[1] - ChromeDriverContentScript.currentDocument.body.scrollTop + (size.height ? size.height / 2 : 0))};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Ensures the passed element is in view, so that the native click event can be sent
|
|
558
|
+
* @return object to send back to background page to trigger a native click
|
|
559
|
+
*/
|
|
560
|
+
function hoverElement(element, elementId) {
|
|
561
|
+
try {
|
|
562
|
+
checkElementIsDisplayed(element)
|
|
563
|
+
} catch (e) {
|
|
564
|
+
return e;
|
|
565
|
+
}
|
|
566
|
+
element.scrollIntoView(true);
|
|
567
|
+
var size = getOffsetSizeFromSubElements(element)
|
|
568
|
+
var coords = getElementCoords(element);
|
|
569
|
+
console.log("element.clientX: " + element.clientX);
|
|
570
|
+
return {statusCode: "no-op", elementId: elementId,
|
|
571
|
+
oldX: 0,
|
|
572
|
+
oldY: 0,
|
|
573
|
+
newX: coords[0] - ChromeDriverContentScript.currentDocument.body.scrollLeft + (size.width ? size.width / 2 : 0),
|
|
574
|
+
newY: coords[1] - ChromeDriverContentScript.currentDocument.body.scrollTop + (size.height ? size.height / 2 : 0)};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Clears the passed element
|
|
579
|
+
*/
|
|
580
|
+
function clearElement(element) {
|
|
581
|
+
var oldValue = element.value;
|
|
582
|
+
element.value = '';
|
|
583
|
+
if (oldValue != '') {
|
|
584
|
+
//TODO: Work out a way of firing events,
|
|
585
|
+
//now that synthesising them gives appendMessage errors
|
|
586
|
+
Utils.fireHtmlEvent(element, "change");
|
|
587
|
+
}
|
|
588
|
+
return {statusCode: 0};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Gets the attribute of the element
|
|
593
|
+
* If the attribute is {disabled, selected, checked, index}, always returns
|
|
594
|
+
* the sensible default (i.e. never null)
|
|
595
|
+
*/
|
|
596
|
+
function getElementAttribute(element, attribute) {
|
|
597
|
+
var value = null;
|
|
598
|
+
switch (attribute.toLowerCase()) {
|
|
599
|
+
case "disabled":
|
|
600
|
+
value = (element.disabled ? element.disabled : "false");
|
|
601
|
+
break;
|
|
602
|
+
case "selected":
|
|
603
|
+
value = findWhetherElementIsSelected(element);
|
|
604
|
+
break;
|
|
605
|
+
case "checked":
|
|
606
|
+
value = (element.checked ? element.checked : "false");
|
|
607
|
+
break;
|
|
608
|
+
case "index":
|
|
609
|
+
value = element.index;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
if (value == null) {
|
|
613
|
+
value = element.getAttribute(attribute);
|
|
614
|
+
}
|
|
615
|
+
return {statusCode: 0, value: value};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Gets the source of the current document
|
|
620
|
+
*/
|
|
621
|
+
function getSource() {
|
|
622
|
+
if (guessPageType() == "html") {
|
|
623
|
+
return {statusCode: 0, value: ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].outerHTML};
|
|
624
|
+
} else if (guessPageType() == "text") {
|
|
625
|
+
return {statusCode: 0, value: ChromeDriverContentScript.currentDocument.getElementsByTagName("pre")[0].innerHTML};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Get whether the element is currently displayed (i.e. can be seen in the browser)
|
|
631
|
+
*/
|
|
632
|
+
function isElementDisplayed(element) {
|
|
633
|
+
try {
|
|
634
|
+
checkElementIsDisplayed(element)
|
|
635
|
+
} catch (e) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Selects the element (i.e. sets its selected/checked value to true)
|
|
643
|
+
* @param element An option element or input element with type checkbox or radio
|
|
644
|
+
*/
|
|
645
|
+
function selectElement(element) {
|
|
646
|
+
var oldValue = true;
|
|
647
|
+
try {
|
|
648
|
+
checkElementIsDisplayed(element);
|
|
649
|
+
checkElementNotDisabled(element);
|
|
650
|
+
var tagName = element.tagName.toLowerCase();
|
|
651
|
+
if (tagName == "option") {
|
|
652
|
+
oldValue = element.selected;
|
|
653
|
+
element.selected = true;
|
|
654
|
+
} else if (tagName == "input") {
|
|
655
|
+
var type = element.getAttribute("type").toLowerCase();
|
|
656
|
+
if (type == "checkbox") {
|
|
657
|
+
oldValue = element.checked;
|
|
658
|
+
element.checked = true;
|
|
659
|
+
} else if (type == "radio") {
|
|
660
|
+
oldValue = element.checked;
|
|
661
|
+
element.checked = true;
|
|
662
|
+
} else {
|
|
663
|
+
throw {statusCode: 12, value: {message: "Cannot select an input." + type}};
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
throw {statusCode: 12, value: {message: "Cannot select a " + tagName}};
|
|
667
|
+
}
|
|
668
|
+
} catch(e) {
|
|
669
|
+
return e;
|
|
670
|
+
}
|
|
671
|
+
if (!oldValue) {
|
|
672
|
+
//TODO: Work out a way of firing events,
|
|
673
|
+
//now that synthesising them gives appendMessage errors
|
|
674
|
+
Utils.fireHtmlEvent(element, "change");
|
|
675
|
+
}
|
|
676
|
+
return {statusCode: 0};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Focus the element so that native code can type to it
|
|
681
|
+
* @param keys the string to type. Must not be an array.
|
|
682
|
+
*/
|
|
683
|
+
function sendElementKeys(element, keys, elementId) {
|
|
684
|
+
try {
|
|
685
|
+
checkElementIsDisplayed(element)
|
|
686
|
+
} catch (e) {
|
|
687
|
+
return e;
|
|
688
|
+
}
|
|
689
|
+
var oldFocusedElement = ChromeDriverContentScript.currentDocument.activeElement;
|
|
690
|
+
if (oldFocusedElement != element) {
|
|
691
|
+
//TODO: Work out a way of firing events,
|
|
692
|
+
//now that synthesising them gives appendMessage errors
|
|
693
|
+
oldFocusedElement.blur();
|
|
694
|
+
Utils.fireHtmlEvent(oldFocusedElement, "blur");
|
|
695
|
+
element.focus();
|
|
696
|
+
Utils.fireHtmlEvent(element, "focus");
|
|
697
|
+
}
|
|
698
|
+
return {statusCode: "no-op", keys: keys, elementId: elementId};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @param keys the string to type. Must not be an array.
|
|
703
|
+
*/
|
|
704
|
+
function sendElementNonNativeKeys(element, keys) {
|
|
705
|
+
//TODO(danielwh): Any kind of actually support for non-native keys
|
|
706
|
+
for (var i = 0; i < keys.length; i++) {
|
|
707
|
+
element.value += keys.charAt(i);
|
|
708
|
+
}
|
|
709
|
+
return {statusCode: 0};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Submits the element if it is a form, or the closest enclosing form otherwise
|
|
714
|
+
*/
|
|
715
|
+
function submitElement(element) {
|
|
716
|
+
while (element != null) {
|
|
717
|
+
if (element.tagName.toLowerCase() == "form") {
|
|
718
|
+
element.submit();
|
|
719
|
+
return {statusCode: 0};
|
|
720
|
+
}
|
|
721
|
+
element = element.parentElement;
|
|
722
|
+
}
|
|
723
|
+
return {statusCode: 12, value: {message: "Cannot submit an element not in a form"}};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Toggles the element if it is an input element of type checkbox,
|
|
728
|
+
* or option element in a multi-select select element
|
|
729
|
+
*/
|
|
730
|
+
function toggleElement(element) {
|
|
731
|
+
var changed = false;
|
|
732
|
+
var oldValue = null;
|
|
733
|
+
var newValue = null;
|
|
734
|
+
try {
|
|
735
|
+
checkElementIsDisplayed(element);
|
|
736
|
+
checkElementNotDisabled(element);
|
|
737
|
+
var tagName = element.tagName.toLowerCase();
|
|
738
|
+
if (tagName == "option") {
|
|
739
|
+
var parent = element;
|
|
740
|
+
while (parent != null && parent.tagName.toLowerCase() != "select") {
|
|
741
|
+
parent = parent.parentElement;
|
|
742
|
+
}
|
|
743
|
+
if (parent == null) {
|
|
744
|
+
throw {statusCode: 12, value: {message: "option tag had no select tag parent"}};
|
|
745
|
+
}
|
|
746
|
+
oldValue = element.selected;
|
|
747
|
+
if (oldValue && !parent.multiple) {
|
|
748
|
+
throw {statusCode: 12, value: {message: "Cannot unselect a single element select"}};
|
|
749
|
+
}
|
|
750
|
+
newValue = element.selected = !oldValue;
|
|
751
|
+
} else if (tagName == "input") {
|
|
752
|
+
var type = element.getAttribute("type").toLowerCase();
|
|
753
|
+
if (type == "checkbox") {
|
|
754
|
+
oldValue = element.checked;
|
|
755
|
+
newValue = element.checked = !oldValue;
|
|
756
|
+
changed = true;
|
|
757
|
+
} else {
|
|
758
|
+
throw {statusCode: 12, value: {message: "Cannot toggle an input." + type}};
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
throw {statusCode: 12, value: {message: "Cannot toggle a " + tagName}};
|
|
762
|
+
}
|
|
763
|
+
} catch (e) {
|
|
764
|
+
return e;
|
|
765
|
+
}
|
|
766
|
+
if (changed) {
|
|
767
|
+
//TODO: Work out a way of firing events,
|
|
768
|
+
//now that synthesising them gives appendMessage errors
|
|
769
|
+
Utils.fireHtmlEvent(element, "change");
|
|
770
|
+
}
|
|
771
|
+
return {statusCode: 0, value: newValue};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Gets the CSS property asked for
|
|
776
|
+
* @param style CSS property to get
|
|
777
|
+
*/
|
|
778
|
+
function getStyle(element, style) {
|
|
779
|
+
var value = ChromeDriverContentScript.currentDocument.defaultView.getComputedStyle(element, null).getPropertyValue(style);
|
|
780
|
+
return rgbToRRGGBB(value);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function switchToNamedIFrameIfOneExists(name) {
|
|
784
|
+
var iframes = ChromeDriverContentScript.currentDocument.getElementsByTagName("iframe");
|
|
785
|
+
for (var i = 0; i < iframes.length; ++i) {
|
|
786
|
+
if (iframes[i].name == name || iframes[i].id == name) {
|
|
787
|
+
ChromeDriverContentScript.currentDocument = iframes[i].contentDocument;
|
|
788
|
+
return {statusCode: 0};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return {statusCode: 8, value: {message: 'Could not find iframe to switch to by name:' + name}};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function sniffForMetaRedirects() {
|
|
795
|
+
for (var i = 0; i < ChromeDriverContentScript.currentDocument.getElementsByTagName("meta").length; ++i) {
|
|
796
|
+
if (ChromeDriverContentScript.currentDocument.getElementsByTagName("meta")[i].hasAttribute("http-equiv") &&
|
|
797
|
+
ChromeDriverContentScript.currentDocument.getElementsByTagName("meta")[i].getAttribute("http-equiv").toLowerCase == "refresh") {
|
|
798
|
+
return {statusCode: "no-op", value: true};
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return {statusCode: "no-op", value: false};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function parseWrappedArguments(argument) {
|
|
805
|
+
//Parse the arguments into actual values (which are wrapped up in JSON)
|
|
806
|
+
if (argument.length !== undefined) {
|
|
807
|
+
var array = [];
|
|
808
|
+
for (var i = 0; i < argument.length; ++i) {
|
|
809
|
+
array.push(parseWrappedArguments(argument[i]));
|
|
810
|
+
}
|
|
811
|
+
return {success: true, value: array};
|
|
812
|
+
}
|
|
813
|
+
switch (argument.type) {
|
|
814
|
+
case "ELEMENT":
|
|
815
|
+
//Wrap up as a special object with the element's canonical xpath, which the page can work out
|
|
816
|
+
var element_id = argument.value;
|
|
817
|
+
var element = null;
|
|
818
|
+
try {
|
|
819
|
+
element = internalGetElement(element_id);
|
|
820
|
+
} catch (e) {
|
|
821
|
+
return {success: false, value: {response: "execute", value:
|
|
822
|
+
{statusCode: 10,
|
|
823
|
+
message: "Tried use obsolete element as argument when executing javascript."}}};
|
|
824
|
+
}
|
|
825
|
+
return {success: true, value: {webdriverElementXPath: getXPathOfElement(element)}};
|
|
826
|
+
break;
|
|
827
|
+
//Intentional falling through because Javascript will parse things properly
|
|
828
|
+
case "STRING":
|
|
829
|
+
case "BOOLEAN":
|
|
830
|
+
case "NUMBER":
|
|
831
|
+
return {success: true, value: argument.value};
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
return {success: false, value: {statusCode: 17,
|
|
835
|
+
message: "Bad argument to javascript."}}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Execute arbitrary javascript in the page.
|
|
840
|
+
* Returns by callback to returnFromJavascriptInPage.
|
|
841
|
+
* Yes, this is *horribly* hacky.
|
|
842
|
+
* We can't share objects between content script and page, so have to wrap up arguments as JSON
|
|
843
|
+
* @param script script to execute as a string
|
|
844
|
+
* @param passedArgs array of arguments to pass to the script
|
|
845
|
+
*/
|
|
846
|
+
function execute(script, passedArgs) {
|
|
847
|
+
console.log("execing " + script + ", args: " + JSON.stringify(passedArgs));
|
|
848
|
+
var func = "function(){" + script + "}";
|
|
849
|
+
var args = [];
|
|
850
|
+
for (var i = 0; i < passedArgs.length; ++i) {
|
|
851
|
+
console.log("Parsing: " + passedArgs[i]);
|
|
852
|
+
var value = parseWrappedArguments(passedArgs[i]);
|
|
853
|
+
if (value.success) {
|
|
854
|
+
args.push(value.value);
|
|
855
|
+
} else {
|
|
856
|
+
ChromeDriverContentScript.port.postMessage(value.value);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
//Add a script tag to the page, containing the script we wish to execute
|
|
861
|
+
var scriptTag = ChromeDriverContentScript.currentDocument.createElement('script');
|
|
862
|
+
var argsString = JSON.stringify(args).replace(/"/g, "\\\"");
|
|
863
|
+
scriptTag.innerText = 'var e = document.createEvent("MutationEvent");' +
|
|
864
|
+
//Dump our arguments in an array
|
|
865
|
+
'var args = JSON.parse("' + argsString + '");' +
|
|
866
|
+
'var element = null;' +
|
|
867
|
+
'for (var i = 0; i < args.length; ++i) {' +
|
|
868
|
+
'if (args[i] && typeof(args[i]) == "object" && args[i].webdriverElementXPath) {' +
|
|
869
|
+
//If this is an element (because it has the proper xpath), turn it into an actual element object
|
|
870
|
+
'args[i] = document.evaluate(args[i].webdriverElementXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;' +
|
|
871
|
+
'}' +
|
|
872
|
+
'}' +
|
|
873
|
+
'try {' +
|
|
874
|
+
'var val = eval(' + func + ').apply(window, args);' +
|
|
875
|
+
'if (typeof(val) == "string") { val = JSON.stringify(val); }' +
|
|
876
|
+
//TODO(danielwh): Deal with the undefined != null case better
|
|
877
|
+
'else if (val === undefined) { val = null; }' +
|
|
878
|
+
'else if (typeof(val) == "object" && val && val.nodeType == 1) {' +
|
|
879
|
+
//If we're returning an element, turn it into a special xpath-containing object
|
|
880
|
+
'var path = "";' +
|
|
881
|
+
'for (; val && val.nodeType == 1; val = val.parentNode) {' +
|
|
882
|
+
'var index = 1;' +
|
|
883
|
+
'for (var sibling = val.previousSibling; sibling; sibling = sibling.previousSibling) {' +
|
|
884
|
+
'if (sibling.nodeType == 1 && sibling.tagName && sibling.tagName == val.tagName) {' +
|
|
885
|
+
'index++;' +
|
|
886
|
+
'}' +
|
|
887
|
+
'}' +
|
|
888
|
+
'path = "/" + val.tagName + "[" + index + "]" + path;' +
|
|
889
|
+
'}' +
|
|
890
|
+
'val = JSON.stringify({webdriverElementXPath: path});' +
|
|
891
|
+
'} else {' +
|
|
892
|
+
'val = JSON.stringify(val);' +
|
|
893
|
+
'}' +
|
|
894
|
+
//Fire mutation event with newValue set to the JSON of our return value
|
|
895
|
+
'e.initMutationEvent("DOMAttrModified", true, false, null, null, "{value: " + val + "}", null, 0);' +
|
|
896
|
+
'} catch (exn) {' +
|
|
897
|
+
//Fire mutation event with prevValue set to EXCEPTION to indicate an error in the script
|
|
898
|
+
'e.initMutationEvent("DOMAttrModified", true, false, null, "EXCEPTION", null, null, 0);' +
|
|
899
|
+
'}' +
|
|
900
|
+
'document.getElementsByTagName("script")[document.getElementsByTagName("script").length - 1].dispatchEvent(e);' +
|
|
901
|
+
'document.getElementsByTagName("html")[0].removeChild(document.getElementsByTagName("script")[document.getElementsByTagName("script").length - 1]);';
|
|
902
|
+
scriptTag.addEventListener('DOMAttrModified', returnFromJavascriptInPage, false);
|
|
903
|
+
console.log("Injecting script element");
|
|
904
|
+
ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].appendChild(scriptTag);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function parseReturnValueFromScript(result) {
|
|
908
|
+
console.log("Parsing: " + JSON.stringify(result));
|
|
909
|
+
var value = {"type":"NULL"};
|
|
910
|
+
if (result !== undefined && result != null && typeof(result) == "object") {
|
|
911
|
+
if (result.webdriverElementXPath) {
|
|
912
|
+
//If we're returning an element, turn it into an actual element object
|
|
913
|
+
value = {value: addElementToInternalArray(getElementsByXPath(result.webdriverElementXPath)[0]).toString(), type:"ELEMENT"};
|
|
914
|
+
} else if (result.length !== undefined) {
|
|
915
|
+
value = [];
|
|
916
|
+
for (var i = 0; i < result.length; ++i) {
|
|
917
|
+
value.push(parseReturnValueFromScript(result[i]));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
} else if (result !== undefined && result != null) {
|
|
921
|
+
switch (typeof(result)) {
|
|
922
|
+
//Intentional falling through because we treat all "VALUE"s the same
|
|
923
|
+
case "string":
|
|
924
|
+
case "number":
|
|
925
|
+
case "boolean":
|
|
926
|
+
value = {value: result, type: "VALUE"};
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return value;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Callback from execute
|
|
935
|
+
*/
|
|
936
|
+
function returnFromJavascriptInPage(e) {
|
|
937
|
+
if (e.prevValue == "EXCEPTION") {
|
|
938
|
+
ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber,
|
|
939
|
+
response: {response: "execute", value: {statusCode: 17,
|
|
940
|
+
message: "Tried to execute bad javascript."}}});
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
console.log("Got result");
|
|
944
|
+
console.log("Result was: " + e.newValue.value);
|
|
945
|
+
var result = JSON.parse(e.newValue).value;
|
|
946
|
+
var value = parseReturnValueFromScript(result);
|
|
947
|
+
console.log("reutrn value: " + JSON.stringify(value));
|
|
948
|
+
ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber, response: {response: "execute", value: {statusCode: 0, value: value}}});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function getFrameNameFromIndex(index) {
|
|
952
|
+
var scriptTag = ChromeDriverContentScript.currentDocument.createElement('script');
|
|
953
|
+
scriptTag.innerText = 'var e = document.createEvent("MutationEvent");' +
|
|
954
|
+
'try {' +
|
|
955
|
+
'var val = window.frames[' + index + '].name;' +
|
|
956
|
+
'e.initMutationEvent("DOMAttrModified", true, false, null, null, val, null, 0);' +
|
|
957
|
+
'} catch (exn) {' +
|
|
958
|
+
//Fire mutation event with prevValue set to EXCEPTION to indicate an error in the script
|
|
959
|
+
'e.initMutationEvent("DOMAttrModified", true, false, null, "EXCEPTION", null, null, 0);' +
|
|
960
|
+
'}' +
|
|
961
|
+
'document.getElementsByTagName("script")[document.getElementsByTagName("script").length - 1].dispatchEvent(e);' +
|
|
962
|
+
'document.getElementsByTagName("html")[0].removeChild(document.getElementsByTagName("script")[document.getElementsByTagName("script").length - 1]);';
|
|
963
|
+
scriptTag.addEventListener('DOMAttrModified', returnFromGetFrameNameFromIndexJavascriptInPage, false);
|
|
964
|
+
try {
|
|
965
|
+
if (ChromeDriverContentScript.currentDocument.getElementsByTagName("frameset").length > 0) {
|
|
966
|
+
ChromeDriverContentScript.currentDocument.getElementsByTagName("frameset")[0].appendChild(scriptTag);
|
|
967
|
+
} else {
|
|
968
|
+
ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].appendChild(scriptTag);
|
|
969
|
+
}
|
|
970
|
+
} catch (e) {
|
|
971
|
+
ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber,
|
|
972
|
+
response: {response: "getFrameNameFromIndex", value: {statusCode: 8,
|
|
973
|
+
message: "Page seemed not to be a frameset. Couldn't find frame"}}});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function returnFromGetFrameNameFromIndexJavascriptInPage(e) {
|
|
978
|
+
if (e.prevValue == "EXCEPTION") {
|
|
979
|
+
ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber,
|
|
980
|
+
response: {response: "getFrameNameFromIndex", value: {statusCode: 8,
|
|
981
|
+
message: "No such frame"}}});
|
|
982
|
+
} else {
|
|
983
|
+
ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber,
|
|
984
|
+
response: {response: "getFrameNameFromIndex", value: {statusCode: "no-op",
|
|
985
|
+
name: e.newValue}}});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Inject an embed tag so the native code can grab the HWND
|
|
991
|
+
*/
|
|
992
|
+
function injectEmbed() {
|
|
993
|
+
ChromeDriverContentScript.injectedEmbedElement = ChromeDriverContentScript.currentDocument.createElement('embed');
|
|
994
|
+
ChromeDriverContentScript.injectedEmbedElement.setAttribute("type", "application/x-chromedriver-reporter");
|
|
995
|
+
ChromeDriverContentScript.injectedEmbedElement.setAttribute("style", "width: 0; height: 0;");
|
|
996
|
+
ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].appendChild(ChromeDriverContentScript.injectedEmbedElement);
|
|
997
|
+
//Give the embed time to render. Hope that the followup doesn't count embeds or anything
|
|
998
|
+
setTimeout(removeInjectedEmbed, 100);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function removeInjectedEmbed() {
|
|
1002
|
+
if (ChromeDriverContentScript.injectedEmbedElement != null) {
|
|
1003
|
+
ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].removeChild(ChromeDriverContentScript.injectedEmbedElement);
|
|
1004
|
+
ChromeDriverContentScript.injectedEmbedElement = null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Guesses whether we have an HTML document or a text file
|
|
1010
|
+
*/
|
|
1011
|
+
function guessPageType() {
|
|
1012
|
+
var source = ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].outerHTML;
|
|
1013
|
+
var textSourceBegins = '<html><body><pre style="word-wrap: break-word; white-space: pre-wrap;">';
|
|
1014
|
+
var textSourceEnds = '</pre></body></html>';
|
|
1015
|
+
|
|
1016
|
+
if (source.substr(0, textSourceBegins.length) == textSourceBegins &&
|
|
1017
|
+
source.substr(0 - textSourceEnds.length) == textSourceEnds) {
|
|
1018
|
+
return "text";
|
|
1019
|
+
} else {
|
|
1020
|
+
return "html";
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Gets an array of elements which match the passed xpath string
|
|
1026
|
+
*/
|
|
1027
|
+
function getElementsByXPath(xpath) {
|
|
1028
|
+
var elements = [];
|
|
1029
|
+
var foundElements = ChromeDriverContentScript.currentDocument.evaluate(xpath, ChromeDriverContentScript.currentDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
|
|
1030
|
+
var this_element = foundElements.iterateNext();
|
|
1031
|
+
while (this_element) {
|
|
1032
|
+
elements.push(this_element);
|
|
1033
|
+
this_element = foundElements.iterateNext();
|
|
1034
|
+
}
|
|
1035
|
+
return elements;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Gets canonical xpath of the passed element, e.g. /HTML/BODY/P[1]
|
|
1040
|
+
*/
|
|
1041
|
+
function getXPathOfElement(element) {
|
|
1042
|
+
var path = "";
|
|
1043
|
+
for (; element && element.nodeType == 1; element = element.parentNode) {
|
|
1044
|
+
index = getElementIndexForXPath(element);
|
|
1045
|
+
path = "/" + element.tagName + "[" + index + "]" + path;
|
|
1046
|
+
}
|
|
1047
|
+
return path;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Returns n for the nth element of type element.tagName in the page
|
|
1052
|
+
*/
|
|
1053
|
+
function getElementIndexForXPath(element) {
|
|
1054
|
+
var index = 1;
|
|
1055
|
+
for (var sibling = element.previousSibling; sibling ; sibling = sibling.previousSibling) {
|
|
1056
|
+
if (sibling.nodeType == 1 && sibling.tagName == element.tagName) {
|
|
1057
|
+
index++;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return index;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Gets an array of link elements whose displayed text is linkText
|
|
1065
|
+
*/
|
|
1066
|
+
function getElementsByLinkText(parent, linkText) {
|
|
1067
|
+
var links = parent.getElementsByTagName("a");
|
|
1068
|
+
var matchingLinks = [];
|
|
1069
|
+
for (var i = 0; i < links.length; i++) {
|
|
1070
|
+
if (Utils.getText(links[i]) == linkText) {
|
|
1071
|
+
matchingLinks.push(links[i]);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return matchingLinks;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Gets an array of link elements whose displayed text includes linkText
|
|
1079
|
+
*/
|
|
1080
|
+
function getElementsByPartialLinkText(parent, partialLinkText) {
|
|
1081
|
+
var links = parent.getElementsByTagName("a");
|
|
1082
|
+
var matchingLinks = [];
|
|
1083
|
+
for (var i = 0; i < links.length; i++) {
|
|
1084
|
+
if (Utils.getText(links[i]).indexOf(partialLinkText) > -1) {
|
|
1085
|
+
matchingLinks.push(links[i]);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return matchingLinks;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Throws exception if element is not displayed
|
|
1093
|
+
* @return nothing if element is displayed
|
|
1094
|
+
* @throws ElementNotVisibleException object ready to be sent if element is not displayed
|
|
1095
|
+
*/
|
|
1096
|
+
function checkElementIsDisplayed(element) {
|
|
1097
|
+
if (element.tagName.toLowerCase() == "title") {
|
|
1098
|
+
//Always visible
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (!Utils.isDisplayed(element)) {
|
|
1102
|
+
throw {statusCode: 11, value: {message: "Element was not visible"}};
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Throws exception if element is disabled
|
|
1108
|
+
* @return nothing if element is enabled
|
|
1109
|
+
* @throws UnsupoprtedOperationException object ready to be sent if element is disabled
|
|
1110
|
+
*/
|
|
1111
|
+
function checkElementNotDisabled(element) {
|
|
1112
|
+
if (element.disabled) {
|
|
1113
|
+
throw {statusCode: 12, value: {message: "Cannot operate on disabled element"}};
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Checks whether element is selected/checked
|
|
1119
|
+
* @return true if element is {selectable and selected, checkable and checked},
|
|
1120
|
+
* false otherwise
|
|
1121
|
+
*/
|
|
1122
|
+
function findWhetherElementIsSelected(element) {
|
|
1123
|
+
var selected = false;
|
|
1124
|
+
try {
|
|
1125
|
+
var tagName = element.tagName.toLowerCase();
|
|
1126
|
+
if (tagName == "option") {
|
|
1127
|
+
selected = element.selected;
|
|
1128
|
+
} else if (tagName == "input") {
|
|
1129
|
+
var type = element.getAttribute("type").toLowerCase();
|
|
1130
|
+
if (type == "checkbox" || type == "radio") {
|
|
1131
|
+
selected = element.checked;
|
|
1132
|
+
}
|
|
1133
|
+
} else {
|
|
1134
|
+
selected = element.getAttribute("selected");
|
|
1135
|
+
}
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
selected = false;
|
|
1138
|
+
}
|
|
1139
|
+
return selected;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Gets the coordinates of the top-left corner of the element on the browser window
|
|
1144
|
+
* (NOT the displayed portion, the WHOLE page)
|
|
1145
|
+
* Heavily influenced by com.google.gwt.dom.client.DOMImplSafari,
|
|
1146
|
+
* which is released under Apache 2
|
|
1147
|
+
* It's not actually correct...
|
|
1148
|
+
* @return array: [x, y]
|
|
1149
|
+
*/
|
|
1150
|
+
function getElementCoords(elem) {
|
|
1151
|
+
var left = 0;
|
|
1152
|
+
var top = 0;
|
|
1153
|
+
if (frameElement) {
|
|
1154
|
+
left += frameElement.offsetLeft;
|
|
1155
|
+
top += frameElement.offsetTop;
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
if (elem.getBoundingClientRect) {
|
|
1159
|
+
var rect = elem.getBoundingClientRect();
|
|
1160
|
+
left += rect.left + ChromeDriverContentScript.currentDocument.body.scrollLeft;
|
|
1161
|
+
top += rect.top + ChromeDriverContentScript.currentDocument.body.scrollTop;
|
|
1162
|
+
return [left, top];
|
|
1163
|
+
}
|
|
1164
|
+
} catch(e) {
|
|
1165
|
+
var left = 0;
|
|
1166
|
+
var top = 0;
|
|
1167
|
+
if (frameElement) {
|
|
1168
|
+
left += frameElement.offsetLeft;
|
|
1169
|
+
top += frameElement.offsetTop;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
//The below is ugly and NOT ACTUALLY RIGHT
|
|
1174
|
+
|
|
1175
|
+
// Unattached elements and elements (or their ancestors) with style
|
|
1176
|
+
// 'display: none' have no offset{Top,Left}.
|
|
1177
|
+
if (elem.offsetTop == null || elem.offsetLeft == null) {
|
|
1178
|
+
return [left, top];
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
var doc = elem.ownerDocument;
|
|
1182
|
+
var curr = elem.parentNode;
|
|
1183
|
+
if (curr) {
|
|
1184
|
+
// This intentionally excludes body which has a null offsetParent.
|
|
1185
|
+
while (curr.offsetParent) {
|
|
1186
|
+
top -= curr.scrollTop;
|
|
1187
|
+
left -= curr.scrollLeft;
|
|
1188
|
+
|
|
1189
|
+
// In RTL mode, offsetLeft is relative to the left edge of the
|
|
1190
|
+
// scrollable area when scrolled all the way to the right, so we need
|
|
1191
|
+
// to add back that difference.
|
|
1192
|
+
if (getStyle(curr, 'direction') == 'rtl') {
|
|
1193
|
+
left += (curr.scrollWidth - curr.clientWidth);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
curr = curr.parentNode;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
while (elem) {
|
|
1201
|
+
top += elem.offsetTop;
|
|
1202
|
+
left += elem.offsetLeft;
|
|
1203
|
+
|
|
1204
|
+
if (getStyle(elem, 'position') == 'fixed') {
|
|
1205
|
+
top += doc.body.scrollTop;
|
|
1206
|
+
left += doc.body.scrollLeft;
|
|
1207
|
+
return [left, top];
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
// Webkit bug: a top-level absolutely positioned element includes the
|
|
1212
|
+
// body's offset position already.
|
|
1213
|
+
var parent = elem.offsetParent;
|
|
1214
|
+
if (parent && (parent.tagName == 'BODY') &&
|
|
1215
|
+
(getStyle(elem, 'position') == 'absolute')) {
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
elem = parent;
|
|
1220
|
+
}
|
|
1221
|
+
return [left, top];
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Gets the maximum offsetHeight and offsetWidth of an element or those of its sub-elements
|
|
1226
|
+
* In place because element.offset{Height,Width} returns incorrectly in WebKit (see bug 28810)
|
|
1227
|
+
* @param element element to get max dimensions of
|
|
1228
|
+
* @param width optional greatest width seen so far (omit when calling)
|
|
1229
|
+
* @param height optional greatest height seen so far (omit when calling)
|
|
1230
|
+
* @return an object of form: {type: "DIMENSION", width: maxOffsetWidth, height: maxOffsetHeight}
|
|
1231
|
+
*/
|
|
1232
|
+
function getOffsetSizeFromSubElements(element, maxWidth, maxHeight) {
|
|
1233
|
+
if (element.getBoundingClientRect) {
|
|
1234
|
+
var rect = element.getBoundingClientRect();
|
|
1235
|
+
return {type: "DIMENSION", width: rect.width, height: rect.height};
|
|
1236
|
+
}
|
|
1237
|
+
//The below isn't correct, but is a hack with a decent probability of being correct, if the element has no BoundingClientRect
|
|
1238
|
+
//TODO(danielwh): Fix this up a bit
|
|
1239
|
+
maxWidth = (maxWidth === undefined || element.offsetWidth > maxWidth) ? element.offsetWidth : maxWidth;
|
|
1240
|
+
maxHeight = (maxHeight === undefined || element.offsetHeight > maxHeight) ? element.offsetHeight : maxHeight;
|
|
1241
|
+
for (var child in element.children) {
|
|
1242
|
+
var childSize = getOffsetSizeFromSubElements(element.children[child], maxWidth, maxHeight);
|
|
1243
|
+
maxWidth = (childSize.width > maxWidth) ? childSize.width : maxWidth;
|
|
1244
|
+
maxHeight = (childSize.height > maxHeight) ? childSize.height : maxHeight;
|
|
1245
|
+
}
|
|
1246
|
+
return {type: "DIMENSION", width: maxWidth, height: maxHeight};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Converts rgb(x, y, z) colours to #RRGGBB colours
|
|
1251
|
+
* @param rgb string of form either rgb(x, y, z) or rgba(x, y, z, a) with x, y, z, a numbers
|
|
1252
|
+
* @return string of form #RRGGBB where RR, GG, BB are two-digit lower-case hex values
|
|
1253
|
+
*/
|
|
1254
|
+
function rgbToRRGGBB(rgb) {
|
|
1255
|
+
var r, g, b;
|
|
1256
|
+
var values = rgb.split(",");
|
|
1257
|
+
if (values.length == 3 && values[0].length > 4 && values[0].substr(0, 4) == "rgb(") {
|
|
1258
|
+
r = decimalToHex(values[0].substr(4));
|
|
1259
|
+
g = decimalToHex(values[1]);
|
|
1260
|
+
b = decimalToHex(values[2].substr(0, values[2].length - 1));
|
|
1261
|
+
if (r == null || g == null || b == null) {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
return "#" + r + g + b;
|
|
1265
|
+
} else if (rgb == "rgba(0, 0, 0, 0)") {
|
|
1266
|
+
return "transparent";
|
|
1267
|
+
} else {
|
|
1268
|
+
return rgb;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Convert a number from decimal to a hex string of at least two digits
|
|
1274
|
+
* @return null if value was not an int, two digit string representation
|
|
1275
|
+
* (with leading zero if needed) of value in base 16 otherwise
|
|
1276
|
+
*/
|
|
1277
|
+
function decimalToHex(value) {
|
|
1278
|
+
value = parseInt(value).toString(16);
|
|
1279
|
+
if (value == null) {
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
if (value.length == 1) {
|
|
1283
|
+
value = '0' + '' + value;
|
|
1284
|
+
}
|
|
1285
|
+
return value;
|
|
1286
|
+
}
|