// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // dom_automation.js // Methods for performing common DOM operations. Used in Chrome testing // involving the DomAutomationController. var domAutomation = domAutomation || {}; (function() { // |objects| is used to track objects which are sent back to the // DomAutomationController. Since JavaScript does not have a map type, // |objects| is simply an object in which the property name and // property value serve as the key-value pair. The key is the handle number // and the value is the tracked object. domAutomation.objects = {}; // The next object handle to use. domAutomation.nextHandle = 1; // The current call ID for which a response is awaited. Each asynchronous // function is given a call ID. When the function has a result to return, // it must supply that call ID. If a result has not yet been received for // that call ID, a response containing the result will be sent to the // domAutomationController. domAutomation.currentCallId = 1; // The current timeout for an asynchronous JavaScript evaluation. Can be given // to window.clearTimeout. domAutomation.currentTimeout = null; // Returns |value| after converting it to an acceptable type for return, if // necessary. function getConvertedValue(value) { if (typeof value == "undefined" || !value) { return ""; } if (value instanceof Array) { var result = []; for (var i = 0; i < value.length; i++) { result.push(getConvertedValue(value[i])); } return result; } if (typeof(value) == "object") { var handle = getHandleForObject(value); if (handle == -1) { // Track this object. var handle = domAutomation.nextHandle++; domAutomation.objects[handle] = value; } return handle; } return value; } // Returns the handle for |obj|, or -1 if no handle exists. function getHandleForObject(obj) { for (var property in domAutomation.objects) { if (domAutomation.objects[property] == obj) return parseInt(property); } return -1; } // Sends a completed response back to the domAutomationController with a // return value, which can be of any type. function sendCompletedResponse(returnValue) { var result = [true, "", getConvertedValue(returnValue)]; domAutomationController.sendJSON(JSON.stringify(result)); } // Sends a error response back to the domAutomationController. |exception| // should be a string or an exception. function sendErrorResponse(exception) { var message = exception.message; if (typeof message == "undefined") message = exception; if (typeof message != "string") message = JSON.stringify(message); var result = [false, message, exception]; domAutomationController.sendJSON(JSON.stringify(result)); } // Safely evaluates |javascript| and sends a response back via the // DomAutomationController. See javascript_execution_controller.cc // for more details. domAutomation.evaluateJavaScript = function(javascript) { try { sendCompletedResponse(eval(javascript)); } catch (exception) { sendErrorResponse(exception); } } // Called by a function when it has completed successfully. Any value, // including undefined, is acceptable for |returnValue|. This should only // be used by functions with an asynchronous response. function onAsyncJavaScriptComplete(callId, returnValue) { if (domAutomation.currentCallId != callId) { // We are not waiting for a response for this call anymore, // because it already responded. return; } domAutomation.currentCallId++; window.clearTimeout(domAutomation.currentTimeout); sendCompletedResponse(returnValue); } // Calld by a function when it has an error preventing its successful // execution. |exception| should be an exception or a string. function onAsyncJavaScriptError(callId, exception) { if (domAutomation.currentCallId != callId) { // We are not waiting for a response for this call anymore, // because it already responded. return; } domAutomation.currentCallId++; window.clearTimeout(domAutomation.currentTimeout); sendErrorResponse(exception); } // Returns whether the call with the given ID has already finished. If true, // this means that the call timed out or that it already gave a response. function didCallFinish(callId) { return domAutomation.currentCallId != callId; } // Safely evaluates |javascript|. The JavaScript is expected to return // a response via either onAsyncJavaScriptComplete or // onAsyncJavaScriptError. The script should respond within the |timeoutMs|. domAutomation.evaluateAsyncJavaScript = function(javascript, timeoutMs) { try { eval(javascript); } catch (exception) { onAsyncJavaScriptError(domAutomation.currentCallId, exception); return; } domAutomation.currentTimeout = window.setTimeout( onAsyncJavaScriptError, timeoutMs, domAutomation.currentCallId, "JavaScript timed out waiting for response."); } // Stops tracking the object associated with |handle|. domAutomation.removeObject = function(handle) { delete domAutomation.objects[handle]; } // Stops tracking all objects. domAutomation.removeAll = function() { domAutomation.objects = {}; domAutomation.nextHandle = 1; } // Gets the object associated with this |handle|. domAutomation.getObject = function(handle) { var obj = domAutomation.objects[handle] if (typeof obj == "undefined") { throw "Object with handle " + handle + " does not exist." } return domAutomation.objects[handle]; } // Gets the ID for this asynchronous call. domAutomation.getCallId = function() { return domAutomation.currentCallId; } // Converts an indexable list with a length property to an array. function getArray(list) { var arr = []; for (var i = 0; i < list.length; i++) { arr.push(list[i]); } return arr; } // Removes whitespace at the beginning and end of |text|. function trim(text) { return text.replace(/^\s+|\s+$/g, ""); } // Returns the window (which is a sub window of |win|) which // directly contains |doc|. May return null. function findWindowForDocument(win, doc) { if (win.document == doc) return win; for (var i = 0; i < win.frames.length; i++) { if (findWindowForDocument(win.frames[i], doc)) return win.frames[i]; } return null; } // Returns |element|'s text. This includes all descendants' text. // For textareas and inputs, the text is the element's value. For Text, // it is the textContent. function getText(element) { if (element instanceof Text) { return trim(element.textContent); } else if (element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement) { return element.value || ""; } var childrenText = ""; for (var i = 0; i < element.childNodes.length; i++) { childrenText += getText(element.childNodes[i]); } return childrenText; } // Returns whether |element| is visible. function isVisible(element) { while (element.style) { if (element.style.display == 'none' || element.style.visibility == 'hidden' || element.style.visibility == 'collapse') { return false; } element = element.parentNode; } return true; } // Returns an array of the visible elements found in the |elements| array. function getVisibleElements(elements) { var visibleElements = []; for (var i = 0; i < elements.length; i++) { if (isVisible(elements[i])) visibleElements.push(elements[i]); } return visibleElements; } // Finds all the elements which satisfy the xpath query using the context // node |context|. This function may throw an exception. function findByXPath(context, xpath) { var xpathResult = document.evaluate(xpath, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); var elements = []; for (var i = 0; i < xpathResult.snapshotLength; i++) { elements.push(xpathResult.snapshotItem(i)); } return elements; } // Finds the first element which satisfies the xpath query using the context // node |context|. This function may throw an exception. function find1ByXPath(context, xpath) { var xpathResult = document.evaluate(xpath, context, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return xpathResult.singleNodeValue; } // Finds all the elements which satisfy the selectors query using the context // node |context|. This function may throw an exception. function findBySelectors(context, selectors) { return getArray(context.querySelectorAll(selectors)); } // Finds the first element which satisfies the selectors query using the // context node |context|. This function may throw an exception. function find1BySelectors(context, selectors) { return context.querySelector(selectors); } // Finds all the elements which contain |text| using the context // node |context|. See getText for details about what constitutes the text // of an element. This function may throw an exception. function findByText(context, text) { // Find all elements containing this text and all inputs containing // this text. var xpath = ".//*[contains(text(), '" + text + "')] | " + ".//input[contains(@value, '" + text + "')]"; var elements = findByXPath(context, xpath); // Limit to what is visible. return getVisibleElements(elements); } // Finds the first element which contains |text| using the context // node |context|. See getText for details about what constitutes the text // of an element. This function may throw an exception. function find1ByText(context, text) { var elements = findByText(context, text); if (elements.length > 0) return findByText(context, text)[0]; return null; } //// DOM Element automation methods //// See dom_element_proxy.h for method details. domAutomation.getDocumentFromFrame = function(element, frameNames) { // Find the window this element is in. var containingDocument = element.ownerDocument || element; var frame = findWindowForDocument(window, containingDocument); for (var i = 0; i < frameNames.length; i++) { frame = frame.frames[frameNames[i]]; if (typeof frame == "undefined" || !frame) { return null; } } return frame.document; } domAutomation.findElement = function(context, query) { var type = query.type; var queryString = query.queryString; if (type == "xpath") { return find1ByXPath(context, queryString); } else if (type == "selectors") { return find1BySelectors(context, queryString); } else if (type == "text") { return find1ByText(context, queryString); } } domAutomation.findElements = function(context, query) { var type = query.type; var queryString = query.queryString; if (type == "xpath") { return findByXPath(context, queryString); } else if (type == "selectors") { return findBySelectors(context, queryString); } else if (type == "text") { return findByText(context, queryString); } } domAutomation.waitForVisibleElementCount = function(context, query, count, callId) { (function waitHelper() { try { var elements = domAutomation.findElements(context, query); var visibleElements = getVisibleElements(elements); if (visibleElements.length == count) onAsyncJavaScriptComplete(callId, visibleElements); else if (!didCallFinish(callId)) window.setTimeout(waitHelper, 500); } catch (exception) { onAsyncJavaScriptError(callId, exception); } })(); } domAutomation.click = function(element) { var evt = document.createEvent('MouseEvents'); evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); while (element) { element.dispatchEvent(evt); element = element.parentNode; } } domAutomation.type = function(element, text) { if (element instanceof HTMLTextAreaElement || (element instanceof HTMLInputElement && element.type == "text")) { element.value += text; return true; } return false; } domAutomation.setText = function(element, text) { if (element instanceof HTMLTextAreaElement || (element instanceof HTMLInputElement && element.type == "text")) { element.value = text; return true; } return false; } domAutomation.getProperty = function(element, property) { return element[property]; } domAutomation.getAttribute = function(element, attribute) { return element.getAttribute(attribute); } domAutomation.getValue = function(element, type) { if (type == "text") { return getText(element); } else if (type == "innerhtml") { return trim(element.innerHTML); } else if (type == "innertext") { return trim(element.innerText); } else if (type == "visibility") { return isVisible(element); } else if (type == "id") { return element.id; } else if (type == "contentdocument") { return element.contentDocument; } } domAutomation.waitForAttribute = function(element, attribute, value, callId) { (function waitForAttributeHelper() { try { if (element.getAttribute(attribute) == value) onAsyncJavaScriptComplete(callId); else if (!didCallFinish(callId)) window.setTimeout(waitForAttributeHelper, 200); } catch (exception) { onAsyncJavaScriptError(callId, exception); } })(); } })();