/** * All functions which take elements assume that they are not null, * and are present as passed on the DOM. */ ChromeDriverContentScript = {}; ChromeDriverContentScript.internalElementArray = []; ChromeDriverContentScript.port = null; ChromeDriverContentScript.currentDocument = window.document; ChromeDriverContentScript.injectedEmbedElement = null; //Record this for async calls (execute), so returner knows what to return //(Also so that we can not re-start commands we have already started executing) ChromeDriverContentScript.currentSequenceNumber = -1; if (ChromeDriverContentScript.currentDocument.location != "about:blank") { //If loading windows using window.open, the port is opened //while we are on about:blank (which always reports window.name as ''), //and we use port-per-tab semantics, so don't open the port if //we're on about:blank ChromeDriverContentScript.port = chrome.extension.connect({name: window.name}); ChromeDriverContentScript.port.onMessage.addListener(parsePortMessage); var isFrameset = (ChromeDriverContentScript.currentDocument.getElementsByTagName("frameset").length > 0); ChromeDriverContentScript.port.postMessage({response: {response: "newTabInformation", value: {statusCode: "no-op", isFrameset: isFrameset, frameCount: window.frames.length, portName: ChromeDriverContentScript.port.name}}, sequenceNumber: -1}); } /** * Parse messages coming in on the port. * Sends relevant response back down the port * @param message JSON message of format: * {request: {request: "some command" * [, optional params]}, * sequenceNumber: some sequence number * [, followup: message']} * where message' is a message to parse after this one */ function parsePortMessage(message) { if (message == null || message.request == null) { console.log("Received bad request: " + JSON.stringify(message)); return; } if (message.sequenceNumber <= ChromeDriverContentScript.currentSequenceNumber) { console.log("Already process{ed,ing} request with sequence number: " + message.sequenceNumber + ", ignoring request: " + message); return; } ChromeDriverContentScript.currentSequenceNumber = message.sequenceNumber; console.log("Received request: " + JSON.stringify(message) + " (" + window.name + ")"); //wait indicates whether this is a potentially page-changing change (see background.js's sendResponseByXHR) var response = {response: message.request.request, value: null, wait: true}; if (message.request.elementId !== undefined && message.request.elementId != null) { //If it seems an elementId has been passed, try to resolve that to an element try { var element = internalGetElement(message.request.elementId); } catch(e) { response.value = e; ChromeDriverContentScript.port.postMessage({response: response, sequenceNumber: message.sequenceNumber}); return; } } try { switch (message.request.request) { case "addCookie": response.value = setCookie(message.request.cookie); response.wait = false; break; case "clearElement": response.value = clearElement(element); break; case "clickElement": response.value = clickElement(element, message.request.elementId); break; case "nonNativeClickElement": //TODO(danielwh): Focus/blur events for non-native clicking element.scrollIntoView(true); //TODO: Work out a way of firing events, //now that synthesising them gives appendMessage errors console.log("mouse downing"); Utils.fireMouseEventOn(element, "mousedown"); console.log("mouse up"); Utils.fireMouseEventOn(element, "mouseup"); console.log("mouse click"); Utils.fireMouseEventOn(element, "click"); if (element.click) { console.log("click"); execute("try { arguments[0].click(); } catch(e){}", {type: "ELEMENT", value: addElementToInternalArray(element)}); } response.value = {statusCode: 0}; break; case "deleteAllCookies": response.value = deleteAllCookies(); response.wait = false; break; case "deleteCookie": response.value = deleteCookie(message.request.name); response.wait = false; break; case "executeScript": execute(message.request.script, message.request.args); //Sends port message back to background page from its own callback break; case "getCookies": response.value = getCookies(); response.wait = false; break; case "getCookie": response.value = getCookieNamed(message.request.name); response.wait = false; break; case "findChildElement": response.value = getElement(false, message.request.using, message.request.value, message.request.id); response.wait = false; break; case "findChildElements": response.value = getElement(true, message.request.using, message.request.value, message.request.id); response.wait = false; break; case "findElement": response.value = getElement(false, message.request.using, message.request.value); response.wait = false; break; case "findElements": response.value = getElement(true, message.request.using, message.request.value); response.wait = false; break; case "getElementAttribute": response.value = getElementAttribute(element, message.request.attribute); response.wait = false; break; case "getElementValueOfCssProperty": response.value = {statusCode: 0, value: getStyle(element, message.request.css)}; response.wait = false; break; case "getElementLocation": var coords = getElementCoords(element); response.value = {statusCode: 0, value: {type: "POINT", x: coords[0], y: coords[1]}}; response.wait = false; break; case "getElementLocationOnceScrolledIntoView": element.scrollIntoView(true); var coords = getElementCoords(element); response.value = {statusCode: 0, value: {type: "POINT", x: coords[0], y: coords[1]}}; break; case "getElementSize": response.value = {statusCode: 0, value: getOffsetSizeFromSubElements(element)}; response.wait = false; break; case "getElementTagName": response.value = {statusCode: 0, value: element.tagName.toLowerCase()}; response.wait = false; break; case "getElementText": response.value = {statusCode: 0, value: Utils.getText(element)}; response.wait = false; break; case "getElementValue": response.value = {statusCode: 0, value: element.value}; response.wait = false; break; case "getFrameNameFromIndex": //TODO(danielwh): Do this by simply looking it up in window.frames when Chrome is fixed. Track: crbug.com 20773 getFrameNameFromIndex(message.request.index); break; case "getPageSource": response.value = getSource(); response.wait = false; break; case "getTitle": response.value = {statusCode: 0, value: Utils.trim(ChromeDriverContentScript.currentDocument.title)}; response.wait = false; break; case "getCurrentUrl": response.value = {statusCode: 0, value: ChromeDriverContentScript.currentDocument.location.href}; response.wait = false; break; case "goBack": history.back(); response.value = {statusCode: 0}; break; case "goForward": history.forward(); response.value = {statusCode: 0}; break; case "hoverOverElement": response.value = hoverElement(element, message.request.elementId); break; case "injectEmbed": injectEmbed(); break; case "isElementDisplayed": response.value = {statusCode: 0, value: isElementDisplayed(element)}; response.wait = false; break; case "isElementEnabled": response.value = {statusCode: 0, value: !element.disabled}; response.wait = false; break; case "isElementSelected": response.value = {statusCode: 0, value: findWhetherElementIsSelected(element)}; response.wait = false; break; case "refresh": ChromeDriverContentScript.currentDocument.location.reload(true); response.value = {statusCode: 0}; break; case "sendKeysToElement": response.value = sendElementKeys(element, message.request.keys, message.request.elementId); response.wait = false; break; case "sendElementNonNativeKeys": response.value = sendElementNonNativeKeys(element, message.request.keys); response.wait = false; break; case "setElementSelected": response.value = selectElement(element); break; case "getActiveElement": response.value = {statusCode: 0, value: [addElementToInternalArray(ChromeDriverContentScript.currentDocument.activeElement).toString()]}; response.wait = false; break; case "switchToNamedIFrameIfOneExists": response.value = switchToNamedIFrameIfOneExists(message.request.name); response.wait = false; break; case "submitElement": response.value = submitElement(element); break; case "toggleElement": response.value = toggleElement(element); break; case "sniffForMetaRedirects": response.value = sniffForMetaRedirects(); break; default: response.value = {statusCode: 9, value: {message: message.request.request + " is unsupported"}}; break; } } catch (e) { console.log("Caught exception " + e + ", sending error response"); ChromeDriverContentScript.port.postMessage({response: {statusCode: 13, value: {message: "An unexpected error occured while executing " + message.request.request + ", exception dump: " + e}}, sequenceNumber: message.sequenceNumber}); } if (response.value != null) { ChromeDriverContentScript.port.postMessage({response: response, sequenceNumber: message.sequenceNumber}); console.log("Sent response: " + JSON.stringify(response) + " (seq:" + message.sequenceNumber + ")"); } if (message.request.followup) { setTimeout(parsePortMessage(message.request.followup), 100); } } /** * Deletes all cookies accessible from the current page. */ function deleteAllCookies() { var cookies = getAllCookiesAsStrings(); for (var i = 0; i < cookies.length; ++i) { var cookie = cookies[i].split("="); deleteCookie(cookie[0]); } return {statusCode: 0}; } /** * Deletes the cookie with the passed name, accessible from the current page (i.e. with path of the current directory or above) * @param cookieName name of the cookie to delete */ function deleteCookie(cookieName) { //It's possible we're trying to delete cookies within iframes. //iframe stuff is unsupported in Chrome at the moment (track crbug.com/20773) //But for the iframe to be loaded and have cookies, it must be of same origin, //so we'll try deleting the cookie as if it was on this page anyway... //(Yes, this is a hack) //TODO(danielwh): Remove the cookieDocument stuff when Chrome fix frame support try { var fullpath = ChromeDriverContentScript.currentDocument.location.pathname; var cookieDocument = ChromeDriverContentScript.currentDocument; } catch (e) { console.log("Falling back on non-iframe document to delete cookies"); var cookieDocument = document; var fullpath = cookieDocument.location.pathname; } var hostParts = cookieDocument.location.hostname.split("."); fullpath = fullpath.split('/'); fullpath.pop(); //Get rid of the file //TODO(danielwh): Tidy up these loops and this repeated code for (var segment in fullpath) { var path = ''; for (var i = 0; i < segment; ++i) { path += fullpath[i + 1] + '/'; } //Delete cookie with trailing / cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path; //Delete cookie without trailing / cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path.substring(0, path.length - 1); var domain = ""; for (var i = hostParts.length - 1; i >= 0; --i) { domain = "." + hostParts[i] + domain; //Delete cookie with trailing / cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path + ";domain=" + domain; cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path + ";domain=" + domain; //Delete cookie without trailing / cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path.substring(0, path.length - 1) + ";domain=" + domain; cookieDocument.cookie = cookieName + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' + path.substring(0, path.length - 1) + ";domain=" + domain.substring(1); } } return {statusCode: 0}; } /** * Get all cookies accessible from the current page as an array of * {name: some name, value: some value, secure: false} values */ function getCookies() { var cookies = []; var cookieStrings = getAllCookiesAsStrings(); for (var i = 0; i < cookieStrings.length; ++i) { var cookie = cookieStrings[i].split("="); cookies.push({type: "COOKIE", name: cookie[0], value: cookie[1], secure: false}); } return {statusCode: 0, value: cookies}; } /** * Get the cookie accessible from the current page with the passed name * @param name name of the cookie */ function getCookieNamed(name) { var cookies = []; var cookieStrings = getAllCookiesAsStrings(); for (var i = 0; i < cookieStrings.length; ++i) { var cookie = cookieStrings[i].split("="); if (cookie[0] == name) { return {statusCode: 0, value: {type: "COOKIE", name: cookie[0], value: cookie[1], secure: false}}; } } return {statusCode: 0, value: null}; } /** * Gets all cookies accessible from the current page as an array of * key=value strings */ function getAllCookiesAsStrings() { //It's possible we're trying to delete cookies within iframes. //iframe stuff is unsupported in Chrome at the moment (track crbug.com/20773) //But for the iframe to be loaded and have cookies, it must be of same origin, //so we'll try deleting the cookie as if it was on this page anyway... //(Yes, this is a hack) //TODO(danielwh): Remove the cookieDocument stuff when Chrome fix frame support var cookieDocument = ChromeDriverContentScript.currentDocument; try { var tempFullpath = ChromeDriverContentScript.currentDocument.location.pathname; } catch (e) { cookieDocument = document; } var cookieStrings = cookieDocument.cookie.split('; '); var cookies = []; for (var i = 0; i < cookieStrings.length; ++i) { if (cookieStrings[i] == '') { break; } cookies.push(cookieStrings[i]); } return cookies; } /** * Add the passed cookie to the page's cookies * @param cookie org.openqa.selenium.Cookie to add */ function setCookie(cookie) { //It's possible we're trying to delete cookies within iframes. //iframe stuff is unsupported in Chrome at the moment (track crbug.com/20773) //But for the iframe to be loaded and have cookies, it must be of same origin, //so we'll try deleting the cookie as if it was on this page anyway... //(Yes, this is a hack) //TODO(danielwh): Remove the cookieDocument stuff when Chrome fix frame support try { var currLocation = ChromeDriverContentScript.currentDocument.location; var currDomain = currLocation.host; var cookieDocument = ChromeDriverContentScript.currentDocument; } catch (e) { cookieDocument = document; var currLocation = ChromeDriverContentScript.currentDocument.location; var currDomain = currLocation.host; } if (currLocation.port != 80) { currDomain += ":" + currLocation.port; } if (cookie.domain != null && cookie.domain !== undefined && currDomain.indexOf(cookie.domain) == -1) { // Not quite right, but close enough. (See r783) return {statusCode: 2, value: { message: "You may only set cookies for the current domain"}}; } else if (guessPageType() != "html") { return {statusCode: 2, value: { message: "You may only set cookies on html documents"}}; } else { cookieDocument.cookie = cookie.name + '=' + escape(cookie.value) + ((cookie.expiry == null || cookie.expiry === undefined) ? '' : ';expires=' + (new Date(cookie.expiry.time)).toGMTString()) + ((cookie.path == null || cookie.path === undefined) ? '' : ';path=' + cookie.path); return {statusCode: 0}; } } /** * Get an element, or a set of elements, by some lookup * Called by both findElement and findElements * @param plural true if want array of all elements, false if singular element * @param parsed array showing how to look up, e.g. ["id", "cheese"] or * [{"id": 0, using: "id", value: "cheese"}] */ function getElement(plural, lookupBy, lookupValue, id) { var root = ""; var parent = null; if (id !== undefined && id != null) { try { parent = internalGetElement(id); } catch (e) { return e; } //Looking for children root = getXPathOfElement(parent); } else { parent = ChromeDriverContentScript.currentDocument; } var elements = []; var attribute = ''; switch (lookupBy) { case "class name": elements = getElementsByXPath(root + "//*[contains(concat(' ',normalize-space(@class),' '),' " + lookupValue + " ')]"); break; case "name": attribute = 'name'; break; case "id": attribute = 'id'; break; case "link text": elements = getElementsByLinkText(parent, lookupValue); break; case "partial link text": elements = getElementsByPartialLinkText(parent, lookupValue); break; case "tag name": elements = getElementsByXPath(root + "//" + lookupValue); break; case "css": elements = parent.querySelectorAll(lookupValue); break; case "xpath": if (root != "" && lookupValue[0] != "/") { //Because xpath is relative to the parent, if there is a parent, and the lookup seems to be implied sub-lookup, we add a / root = root + "/"; } var xpath = root + lookupValue; try { elements = getElementsByXPath(xpath); } catch (e) { return {statusCode: 19, value: {message: "Could not look up element by xpath " + xpath}}; } break; } if (attribute != '') { elements = getElementsByXPath(root + "//*[@" + attribute + "='" + lookupValue + "']"); } if (elements == null || elements.length == 0) { if (plural) { //Fine, no elements matched return {statusCode: 0, value: []}; } else { //Problem - we were expecting an element return {statusCode: 7, value: { message: "Unable to find element with " + lookupBy + " " + lookupValue}}; } } else { var elementsToReturnArray = []; if (plural) { //Add all found elements to the page's elements, and push each to the array to return var addedElements = addElementsToInternalArray(elements); for (var addedElement in addedElements) { elementsToReturnArray.push(addedElements[addedElement].toString()); } } else { if (!elements[0]) { return {statusCode: 7, value: { message: "Unable to find element with " + lookupBy + " " + lookupValue}}; } //Add the first found elements to the page's elements, and push it to the array to return elementsToReturnArray.push(addElementToInternalArray(elements[0]).toString()); } return {statusCode: 0, value: elementsToReturnArray}; } } /** * Adds the element to the internal element store, if it isn't already there. * @return index of element in the array */ function addElementToInternalArray(element) { for (var existingElement in ChromeDriverContentScript.internalElementArray) { if (element == ChromeDriverContentScript.internalElementArray[existingElement]) { return existingElement; } } ChromeDriverContentScript.internalElementArray.push(element); return (ChromeDriverContentScript.internalElementArray.length - 1); } function addElementsToInternalArray(elements) { var toReturn = []; for (var element in elements) { toReturn.push(addElementToInternalArray(elements[element])); } return toReturn; } /** * Gets an element which we have previously looked up by its internal ID * @param elementIdAsString the element's internal ID * @return element with the passed internal ID * @throws if element ID was stale: wrapped up * org.openqa.selenium.StaleElementReferenceException ready to send */ function internalGetElement(elementIdAsString) { var elementId = parseInt(elementIdAsString); if (elementId != null && elementId >= 0 && ChromeDriverContentScript.internalElementArray.length >= elementId + 1) { var element = ChromeDriverContentScript.internalElementArray[elementId]; var parent = element; while (parent && parent != element.ownerDocument.documentElement) { parent = parent.parentNode; } if (parent !== element.ownerDocument.documentElement) { throw {statusCode: 10, value: {message: "Element is obsolete"}}; } return element; } else { throw {statusCode: 10, value: {message: "Element is obsolete"}}; } } /** * Ensures the passed element is in view, so that the native click event can be sent * @return object to send back to background page to trigger a native click */ function clickElement(element, elementId) { try { checkElementIsDisplayed(element) } catch (e) { return e; } element.scrollIntoView(true); var size = getOffsetSizeFromSubElements(element); var coords = getElementCoords(element); return {statusCode: "no-op", elementId: elementId, x: parseInt(coords[0] - ChromeDriverContentScript.currentDocument.body.scrollLeft + (size.width ? size.width / 2 : 0)), y: parseInt(coords[1] - ChromeDriverContentScript.currentDocument.body.scrollTop + (size.height ? size.height / 2 : 0))}; } /** * Ensures the passed element is in view, so that the native click event can be sent * @return object to send back to background page to trigger a native click */ function hoverElement(element, elementId) { try { checkElementIsDisplayed(element) } catch (e) { return e; } element.scrollIntoView(true); var size = getOffsetSizeFromSubElements(element) var coords = getElementCoords(element); console.log("element.clientX: " + element.clientX); return {statusCode: "no-op", elementId: elementId, oldX: 0, oldY: 0, newX: coords[0] - ChromeDriverContentScript.currentDocument.body.scrollLeft + (size.width ? size.width / 2 : 0), newY: coords[1] - ChromeDriverContentScript.currentDocument.body.scrollTop + (size.height ? size.height / 2 : 0)}; } /** * Clears the passed element */ function clearElement(element) { var oldValue = element.value; element.value = ''; if (oldValue != '') { //TODO: Work out a way of firing events, //now that synthesising them gives appendMessage errors Utils.fireHtmlEvent(element, "change"); } return {statusCode: 0}; } /** * Gets the attribute of the element * If the attribute is {disabled, selected, checked, index}, always returns * the sensible default (i.e. never null) */ function getElementAttribute(element, attribute) { var value = null; switch (attribute.toLowerCase()) { case "disabled": value = (element.disabled ? element.disabled : "false"); break; case "selected": value = findWhetherElementIsSelected(element); break; case "checked": value = (element.checked ? element.checked : "false"); break; case "index": value = element.index; break; } if (value == null) { value = element.getAttribute(attribute); } return {statusCode: 0, value: value}; } /** * Gets the source of the current document */ function getSource() { if (guessPageType() == "html") { return {statusCode: 0, value: ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].outerHTML}; } else if (guessPageType() == "text") { return {statusCode: 0, value: ChromeDriverContentScript.currentDocument.getElementsByTagName("pre")[0].innerHTML}; } } /** * Get whether the element is currently displayed (i.e. can be seen in the browser) */ function isElementDisplayed(element) { try { checkElementIsDisplayed(element) } catch (e) { return false; } return true; } /** * Selects the element (i.e. sets its selected/checked value to true) * @param element An option element or input element with type checkbox or radio */ function selectElement(element) { var oldValue = true; try { checkElementIsDisplayed(element); checkElementNotDisabled(element); var tagName = element.tagName.toLowerCase(); if (tagName == "option") { oldValue = element.selected; element.selected = true; } else if (tagName == "input") { var type = element.getAttribute("type").toLowerCase(); if (type == "checkbox") { oldValue = element.checked; element.checked = true; } else if (type == "radio") { oldValue = element.checked; element.checked = true; } else { throw {statusCode: 12, value: {message: "Cannot select an input." + type}}; } } else { throw {statusCode: 12, value: {message: "Cannot select a " + tagName}}; } } catch(e) { return e; } if (!oldValue) { //TODO: Work out a way of firing events, //now that synthesising them gives appendMessage errors if (tagName == "option") { var select = element; while (select.parentNode != null && select.tagName.toLowerCase() != "select") { select = select.parentNode; } if (select.tagName.toLowerCase() == "select") { element = select; } else { //If we're not within a select element, fire the event from the option, and hope that it bubbles up console.log("Falling back to event firing from option, not select element"); } } Utils.fireHtmlEvent(element, "change"); } return {statusCode: 0}; } /** * Focus the element so that native code can type to it * @param keys the string to type. Must not be an array. */ function sendElementKeys(element, keys, elementId) { try { checkElementIsDisplayed(element) } catch (e) { return e; } var oldFocusedElement = ChromeDriverContentScript.currentDocument.activeElement; if (oldFocusedElement != element) { //TODO: Work out a way of firing events, //now that synthesising them gives appendMessage errors Utils.fireHtmlEventAndConditionallyPerformAction(oldFocusedElement, "blur", function() {oldFocusedElement.blur();}); Utils.fireHtmlEventAndConditionallyPerformAction(element, "focus", function() {element.focus();}); } return {statusCode: "no-op", keys: keys, elementId: elementId}; } /** * @param keys the string to type. Must not be an array. */ function sendElementNonNativeKeys(element, keys) { //TODO(danielwh): Any kind of actually support for non-native keys for (var i = 0; i < keys.length; i++) { element.value += keys.charAt(i); } return {statusCode: 0}; } /** * Submits the element if it is a form, or the closest enclosing form otherwise */ function submitElement(element) { while (element != null) { if (element.tagName.toLowerCase() == "form") { Utils.fireHtmlEventAndConditionallyPerformAction(element, "submit", function() {element.submit();}); return {statusCode: 0}; } element = element.parentNode; } return {statusCode: 12, value: {message: "Cannot submit an element not in a form"}}; } /** * Toggles the element if it is an input element of type checkbox, * or option element in a multi-select select element */ function toggleElement(element) { var changed = false; var oldValue = null; var newValue = null; try { checkElementIsDisplayed(element); checkElementNotDisabled(element); var tagName = element.tagName.toLowerCase(); if (tagName == "option") { var parent = element; while (parent != null && parent.tagName.toLowerCase() != "select") { parent = parent.parentNode; } if (parent == null) { throw {statusCode: 12, value: {message: "option tag had no select tag parent"}}; } oldValue = element.selected; if (oldValue && !parent.multiple) { throw {statusCode: 12, value: {message: "Cannot unselect a single element select"}}; } newValue = element.selected = !oldValue; } else if (tagName == "input") { var type = element.getAttribute("type").toLowerCase(); if (type == "checkbox") { oldValue = element.checked; newValue = element.checked = !oldValue; changed = true; } else { throw {statusCode: 12, value: {message: "Cannot toggle an input." + type}}; } } else { throw {statusCode: 12, value: {message: "Cannot toggle a " + tagName}}; } } catch (e) { return e; } console.log("New value: " + newValue); if (changed) { //TODO: Work out a way of firing events, //now that synthesising them gives appendMessage errors Utils.fireHtmlEvent(element, "change"); } return {statusCode: 0, value: newValue}; } /** * Gets the CSS property asked for * @param style CSS property to get */ function getStyle(element, style) { var value = ChromeDriverContentScript.currentDocument.defaultView.getComputedStyle(element, null).getPropertyValue(style); return rgbToRRGGBB(value); } function switchToNamedIFrameIfOneExists(name) { var iframes = ChromeDriverContentScript.currentDocument.getElementsByTagName("iframe"); for (var i = 0; i < iframes.length; ++i) { if (iframes[i].name == name || iframes[i].id == name) { ChromeDriverContentScript.currentDocument = iframes[i].contentDocument; return {statusCode: 0}; } } return {statusCode: 8, value: {message: 'Could not find iframe to switch to by name:' + name}}; } function sniffForMetaRedirects() { for (var i = 0; i < ChromeDriverContentScript.currentDocument.getElementsByTagName("meta").length; ++i) { if (ChromeDriverContentScript.currentDocument.getElementsByTagName("meta")[i].hasAttribute("http-equiv") && ChromeDriverContentScript.currentDocument.getElementsByTagName("meta")[i].getAttribute("http-equiv").toLowerCase == "refresh") { return {statusCode: "no-op", value: true}; } } return {statusCode: "no-op", value: false}; } function parseWrappedArguments(argument) { //Parse the arguments into actual values (which are wrapped up in JSON) if (argument.length !== undefined) { var array = []; for (var i = 0; i < argument.length; ++i) { array.push(parseWrappedArguments(argument[i])); } return {success: true, value: array}; } switch (argument.type) { case "ELEMENT": //Wrap up as a special object with the element's canonical xpath, which the page can work out var element_id = argument.value; var element = null; try { element = internalGetElement(element_id); } catch (e) { return {success: false, value: {response: "execute", value: {statusCode: 10, message: "Tried use obsolete element as argument when executing javascript."}}}; } return {success: true, value: {webdriverElementXPath: getXPathOfElement(element)}}; break; //Intentional falling through because Javascript will parse things properly case "STRING": case "BOOLEAN": case "NUMBER": return {success: true, value: argument.value}; break; } return {success: false, value: {statusCode: 17, message: "Bad argument to javascript."}} } /** * Execute arbitrary javascript in the page. * Returns by callback to returnFromJavascriptInPage. * Yes, this is *horribly* hacky. * We can't share objects between content script and page, so have to wrap up arguments as JSON * @param script script to execute as a string * @param passedArgs array of arguments to pass to the script * @param callback function to call when the result is returned. Passed a DOMAttrModified event which should be parsed as returnFromJavascriptInPage * TODO: Make the callback be passed the parsed result. */ function execute_(script, passedArgs, callback) { console.log("executing " + script + ", args: " + JSON.stringify(passedArgs)); var func = "function(){" + script + "}"; var args = []; for (var i = 0; i < passedArgs.length; ++i) { console.log("Parsing: " + JSON.stringify(passedArgs[i])); var value = parseWrappedArguments(passedArgs[i]); if (value.success) { args.push(value.value); } else { ChromeDriverContentScript.port.postMessage({response: value.value, sequenceNumber: ChromeDriverContentScript.currentSequenceNumber}); return; } } //Add a script tag to the page, containing the script we wish to execute var scriptTag = ChromeDriverContentScript.currentDocument.createElement('script'); var argsString = JSON.stringify(args).replace(/"/g, "\\\""); scriptTag.innerText = 'var e = document.createEvent("MutationEvent");' + //Dump our arguments in an array 'var args = JSON.parse("' + argsString + '");' + 'var element = null;' + 'for (var i = 0; i < args.length; ++i) {' + 'if (args[i] && typeof(args[i]) == "object" && args[i].webdriverElementXPath) {' + //If this is an element (because it has the proper xpath), turn it into an actual element object 'args[i] = document.evaluate(args[i].webdriverElementXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;' + '}' + '}' + 'try {' + 'var val = eval(' + func + ').apply(window, args);' + 'if (typeof(val) == "string") { val = JSON.stringify(val); }' + //TODO(danielwh): Deal with the undefined != null case better 'else if (val === undefined) { val = null; }' + 'else if (typeof(val) == "object" && val && val.nodeType == 1) {' + //If we're returning an element, turn it into a special xpath-containing object 'var path = "";' + 'for (; val && val.nodeType == 1; val = val.parentNode) {' + 'var index = 1;' + 'for (var sibling = val.previousSibling; sibling; sibling = sibling.previousSibling) {' + 'if (sibling.nodeType == 1 && sibling.tagName && sibling.tagName == val.tagName) {' + 'index++;' + '}' + '}' + 'path = "/" + val.tagName + "[" + index + "]" + path;' + '}' + 'val = JSON.stringify({webdriverElementXPath: path});' + '} else {' + 'val = JSON.stringify(val);' + '}' + //Fire mutation event with newValue set to the JSON of our return value 'e.initMutationEvent("DOMAttrModified", true, false, null, null, \'{\"value\": \' + val + \'}\', null, 0);' + '} catch (exn) {' + //Fire mutation event with prevValue set to EXCEPTION to indicate an error in the script 'e.initMutationEvent("DOMAttrModified", true, false, null, "EXCEPTION", null, null, 0);' + '}' + 'document.getElementsByTagName("script")[document.getElementsByTagName("script").length - 1].dispatchEvent(e);' + 'document.getElementsByTagName("html")[0].removeChild(document.getElementsByTagName("script")[document.getElementsByTagName("script").length - 1]);'; scriptTag.addEventListener('DOMAttrModified', callback, false); console.log("Injecting script element"); ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].appendChild(scriptTag); } function execute(script, passedArgs) { execute_(script, passedArgs, returnFromJavascriptInPage); } function parseReturnValueFromScript(result) { console.log("Parsing: " + JSON.stringify(result)); var value = {"type":"NULL"}; if (result !== undefined && result != null && typeof(result) == "object") { if (result.webdriverElementXPath) { //If we're returning an element, turn it into an actual element object value = {value: addElementToInternalArray(getElementsByXPath(result.webdriverElementXPath)[0]).toString(), type:"ELEMENT"}; } else if (result.length !== undefined) { value = []; for (var i = 0; i < result.length; ++i) { value.push(parseReturnValueFromScript(result[i])); } } } else if (result !== undefined && result != null) { switch (typeof(result)) { //Intentional falling through because we treat all "VALUE"s the same case "string": case "number": case "boolean": value = {value: result, type: "VALUE"}; break; } } return value; } /** * Callback from execute */ function returnFromJavascriptInPage(e) { if (e.prevValue == "EXCEPTION") { ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber, response: {response: "execute", value: {statusCode: 17, message: "Tried to execute bad javascript."}}}); return; } console.log("Got result"); console.log("Result was: " + e.newValue.value); var result = JSON.parse(e.newValue).value; var value = parseReturnValueFromScript(result); console.log("Return value: " + JSON.stringify(value)); ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber, response: {response: "execute", value: {statusCode: 0, value: value}}}); } function getFrameNameFromIndex(index) { var scriptTag = ChromeDriverContentScript.currentDocument.createElement('script'); scriptTag.innerText = 'var e = document.createEvent("MutationEvent");' + 'try {' + 'var val = window.frames[' + index + '].name;' + 'e.initMutationEvent("DOMAttrModified", true, false, null, null, val, null, 0);' + '} catch (exn) {' + //Fire mutation event with prevValue set to EXCEPTION to indicate an error in the script 'e.initMutationEvent("DOMAttrModified", true, false, null, "EXCEPTION", null, null, 0);' + '}' + 'document.getElementsByTagName("script")[document.getElementsByTagName("script").length - 1].dispatchEvent(e);' + 'document.getElementsByTagName("html")[0].removeChild(document.getElementsByTagName("script")[document.getElementsByTagName("script").length - 1]);'; scriptTag.addEventListener('DOMAttrModified', returnFromGetFrameNameFromIndexJavascriptInPage, false); try { if (ChromeDriverContentScript.currentDocument.getElementsByTagName("frameset").length > 0) { ChromeDriverContentScript.currentDocument.getElementsByTagName("frameset")[0].appendChild(scriptTag); } else { ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].appendChild(scriptTag); } } catch (e) { ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber, response: {response: "getFrameNameFromIndex", value: {statusCode: 8, message: "Page seemed not to be a frameset. Couldn't find frame"}}}); } } function returnFromGetFrameNameFromIndexJavascriptInPage(e) { if (e.prevValue == "EXCEPTION") { ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber, response: {response: "getFrameNameFromIndex", value: {statusCode: 8, message: "No such frame"}}}); } else { ChromeDriverContentScript.port.postMessage({sequenceNumber: ChromeDriverContentScript.currentSequenceNumber, response: {response: "getFrameNameFromIndex", value: {statusCode: "no-op", name: e.newValue}}}); } } /** * Inject an embed tag so the native code can grab the HWND */ function injectEmbed() { ChromeDriverContentScript.injectedEmbedElement = ChromeDriverContentScript.currentDocument.createElement('embed'); ChromeDriverContentScript.injectedEmbedElement.setAttribute("type", "application/x-chromedriver-reporter"); ChromeDriverContentScript.injectedEmbedElement.setAttribute("style", "width: 0; height: 0;"); ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].appendChild(ChromeDriverContentScript.injectedEmbedElement); //Give the embed time to render. Hope that the followup doesn't count embeds or anything setTimeout(removeInjectedEmbed, 100); } function removeInjectedEmbed() { if (ChromeDriverContentScript.injectedEmbedElement != null) { ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].removeChild(ChromeDriverContentScript.injectedEmbedElement); ChromeDriverContentScript.injectedEmbedElement = null; } } /** * Guesses whether we have an HTML document or a text file */ function guessPageType() { var source = ChromeDriverContentScript.currentDocument.getElementsByTagName("html")[0].outerHTML; var textSourceBegins = '
'; var textSourceEnds = ''; if (source.substr(0, textSourceBegins.length) == textSourceBegins && source.substr(0 - textSourceEnds.length) == textSourceEnds) { return "text"; } else { return "html"; } } /** * Gets an array of elements which match the passed xpath string */ function getElementsByXPath(xpath) { var elements = []; var foundElements = ChromeDriverContentScript.currentDocument.evaluate(xpath, ChromeDriverContentScript.currentDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); var this_element = foundElements.iterateNext(); while (this_element) { elements.push(this_element); this_element = foundElements.iterateNext(); } return elements; } /** * Gets canonical xpath of the passed element, e.g. /HTML/BODY/P[1] */ function getXPathOfElement(element) { var path = ""; for (; element && element.nodeType == 1; element = element.parentNode) { index = getElementIndexForXPath(element); path = "/" + element.tagName + "[" + index + "]" + path; } return path; } /** * Returns n for the nth element of type element.tagName in the page */ function getElementIndexForXPath(element) { var index = 1; for (var sibling = element.previousSibling; sibling ; sibling = sibling.previousSibling) { if (sibling.nodeType == 1 && sibling.tagName == element.tagName) { index++; } } return index; } /** * Gets an array of link elements whose displayed text is linkText */ function getElementsByLinkText(parent, linkText) { var links = parent.getElementsByTagName("a"); var matchingLinks = []; for (var i = 0; i < links.length; i++) { if (Utils.getText(links[i]) == linkText) { matchingLinks.push(links[i]); } } return matchingLinks; } /** * Gets an array of link elements whose displayed text includes linkText */ function getElementsByPartialLinkText(parent, partialLinkText) { var links = parent.getElementsByTagName("a"); var matchingLinks = []; for (var i = 0; i < links.length; i++) { if (Utils.getText(links[i]).indexOf(partialLinkText) > -1) { matchingLinks.push(links[i]); } } return matchingLinks; } /** * Throws exception if element is not displayed * @return nothing if element is displayed * @throws ElementNotVisibleException object ready to be sent if element is not displayed */ function checkElementIsDisplayed(element) { if (element.tagName.toLowerCase() == "title") { //Always visible return; } if (!Utils.isDisplayed(element)) { throw {statusCode: 11, value: {message: "Element was not visible"}}; } } /** * Throws exception if element is disabled * @return nothing if element is enabled * @throws UnsupoprtedOperationException object ready to be sent if element is disabled */ function checkElementNotDisabled(element) { if (element.disabled) { throw {statusCode: 12, value: {message: "Cannot operate on disabled element"}}; } } /** * Checks whether element is selected/checked * @return true if element is {selectable and selected, checkable and checked}, * false otherwise */ function findWhetherElementIsSelected(element) { var selected = false; try { var tagName = element.tagName.toLowerCase(); if (tagName == "option") { selected = element.selected; } else if (tagName == "input") { var type = element.getAttribute("type").toLowerCase(); if (type == "checkbox" || type == "radio") { selected = element.checked; } } else { selected = element.getAttribute("selected"); } } catch (e) { selected = false; } return selected; } /** * Gets the coordinates of the top-left corner of the element on the browser window * (NOT the displayed portion, the WHOLE page) * Heavily influenced by com.google.gwt.dom.client.DOMImplSafari, * which is released under Apache 2 * It's not actually correct... * @return array: [x, y] */ function getElementCoords(elem) { var left = 0; var top = 0; if (frameElement) { left += frameElement.offsetLeft; top += frameElement.offsetTop; } try { if (elem.getBoundingClientRect) { var rect = elem.getBoundingClientRect(); left += rect.left + ChromeDriverContentScript.currentDocument.body.scrollLeft; top += rect.top + ChromeDriverContentScript.currentDocument.body.scrollTop; return [left, top]; } } catch(e) { var left = 0; var top = 0; if (frameElement) { left += frameElement.offsetLeft; top += frameElement.offsetTop; } } //The below is ugly and NOT ACTUALLY RIGHT // Unattached elements and elements (or their ancestors) with style // 'display: none' have no offset{Top,Left}. if (elem.offsetTop == null || elem.offsetLeft == null) { return [left, top]; } var doc = elem.ownerDocument; var curr = elem.parentNode; if (curr) { // This intentionally excludes body which has a null offsetParent. while (curr.offsetParent) { top -= curr.scrollTop; left -= curr.scrollLeft; // In RTL mode, offsetLeft is relative to the left edge of the // scrollable area when scrolled all the way to the right, so we need // to add back that difference. if (getStyle(curr, 'direction') == 'rtl') { left += (curr.scrollWidth - curr.clientWidth); } curr = curr.parentNode; } } while (elem) { top += elem.offsetTop; left += elem.offsetLeft; if (getStyle(elem, 'position') == 'fixed') { top += doc.body.scrollTop; left += doc.body.scrollLeft; return [left, top]; } // Webkit bug: a top-level absolutely positioned element includes the // body's offset position already. var parent = elem.offsetParent; if (parent && (parent.tagName == 'BODY') && (getStyle(elem, 'position') == 'absolute')) { break; } elem = parent; } return [left, top]; } /** * Gets the maximum offsetHeight and offsetWidth of an element or those of its sub-elements * In place because element.offset{Height,Width} returns incorrectly in WebKit (see bug 28810) * @param element element to get max dimensions of * @param width optional greatest width seen so far (omit when calling) * @param height optional greatest height seen so far (omit when calling) * @return an object of form: {type: "DIMENSION", width: maxOffsetWidth, height: maxOffsetHeight} */ function getOffsetSizeFromSubElements(element, maxWidth, maxHeight) { if (element.getBoundingClientRect) { var rect = element.getBoundingClientRect(); return {type: "DIMENSION", width: rect.width, height: rect.height}; } //The below isn't correct, but is a hack with a decent probability of being correct, if the element has no BoundingClientRect //TODO(danielwh): Fix this up a bit maxWidth = (maxWidth === undefined || element.offsetWidth > maxWidth) ? element.offsetWidth : maxWidth; maxHeight = (maxHeight === undefined || element.offsetHeight > maxHeight) ? element.offsetHeight : maxHeight; for (var child in element.children) { var childSize = getOffsetSizeFromSubElements(element.children[child], maxWidth, maxHeight); maxWidth = (childSize.width > maxWidth) ? childSize.width : maxWidth; maxHeight = (childSize.height > maxHeight) ? childSize.height : maxHeight; } return {type: "DIMENSION", width: maxWidth, height: maxHeight}; } /** * Converts rgb(x, y, z) colours to #RRGGBB colours * @param rgb string of form either rgb(x, y, z) or rgba(x, y, z, a) with x, y, z, a numbers * @return string of form #RRGGBB where RR, GG, BB are two-digit lower-case hex values */ function rgbToRRGGBB(rgb) { var r, g, b; var values = rgb.split(","); if (values.length == 3 && values[0].length > 4 && values[0].substr(0, 4) == "rgb(") { r = decimalToHex(values[0].substr(4)); g = decimalToHex(values[1]); b = decimalToHex(values[2].substr(0, values[2].length - 1)); if (r == null || g == null || b == null) { return null; } return "#" + r + g + b; } else if (rgb == "rgba(0, 0, 0, 0)") { return "transparent"; } else { return rgb; } } /** * Convert a number from decimal to a hex string of at least two digits * @return null if value was not an int, two digit string representation * (with leading zero if needed) of value in base 16 otherwise */ function decimalToHex(value) { value = parseInt(value).toString(16); if (value == null) { return null; } if (value.length == 1) { value = '0' + '' + value; } return value; }