From f18744fd5ec25ee780633e039a9f53d2cce3b49c Mon Sep 17 00:00:00 2001 From: "kkania@chromium.org" Date: Fri, 16 Apr 2010 16:34:15 +0000 Subject: Add ability to manipulate DOM elements from the automation proxy. Rework the way that javascript is packaged and parsed in the JavascriptExecutionController, and add some waiting methods. BUG=none TEST=none Review URL: http://codereview.chromium.org/1632001 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@44778 0039d316-1c4b-4281-b951-d872f2087c98 --- chrome/renderer/resources/dom_automation.js | 294 ++++++++++++++++++++++------ 1 file changed, 239 insertions(+), 55 deletions(-) (limited to 'chrome/renderer') diff --git a/chrome/renderer/resources/dom_automation.js b/chrome/renderer/resources/dom_automation.js index 16a4dd0..0af645b 100644 --- a/chrome/renderer/resources/dom_automation.js +++ b/chrome/renderer/resources/dom_automation.js @@ -15,8 +15,21 @@ var domAutomation = domAutomation || {}; // 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) { @@ -51,20 +64,84 @@ var domAutomation = domAutomation || {}; 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 { - var result = [true, "", getConvertedValue(eval(javascript))]; + sendCompletedResponse(eval(javascript)); } catch (exception) { - var message = exception.message; - if (typeof message != "string") - message = JSON.stringify(message); - var result = [false, message, exception]; + sendErrorResponse(exception); } - domAutomationController.sendJSON(JSON.stringify(result)); + } + + // 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|. @@ -80,9 +157,18 @@ var domAutomation = domAutomation || {}; // 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 = []; @@ -109,24 +195,49 @@ var domAutomation = domAutomation || {}; 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); + // 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; + } - for (var i = 0; i < frameNames.length; i++) { - frame = frame.frames[frameNames[i]]; - if (typeof frame == "undefined" || !frame) { - return null; + // 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 frame.document; + return true; } - domAutomation.findByXPath = function(context, xpath) { + // 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); @@ -137,39 +248,107 @@ var domAutomation = domAutomation || {}; return elements; } - domAutomation.find1ByXPath = function(context, xpath) { + // 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; } - domAutomation.findBySelectors = function(context, selectors) { + // 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)); } - domAutomation.find1BySelectors = function(context, 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); } - domAutomation.findByText = function(context, text) { + // 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 = domAutomation.findByXPath(context, xpath); + var elements = findByXPath(context, xpath); // Limit to what is visible. - var final_list = []; - for (var i = 0; i < elements.length; i++) { - if (domAutomation.isVisible(elements[i])) - final_list.push(elements[i]); + 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); } - return final_list; } - domAutomation.find1ByText = function(context, text) { - return domAutomation.findByText(context, text)[0]; + 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) { @@ -201,34 +380,39 @@ var domAutomation = domAutomation || {}; return false; } - domAutomation.getText = function(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 += domAutomation.getText(element.childNodes[i]); - } - return childrenText; + domAutomation.getProperty = function(element, property) { + return element[property]; } - domAutomation.getInnerHTML = function(element) { - return trim(element.innerHTML); + domAutomation.getAttribute = function(element, attribute) { + return element.getAttribute(attribute); } - domAutomation.isVisible = function(element) { - while (element.style) { - if (element.style.display == 'none' || - element.style.visibility == 'hidden' || - element.style.visibility == 'collapse') { - return false; - } - element = element.parentNode; + domAutomation.getValue = function(element, type) { + if (type == "text") { + return getText(element); + } else if (type == "innerhtml") { + return trim(element.innerHTML); + } else if (type == "visibility") { + return isVisible(element); + } else if (type == "id") { + return element.id; + } else if (type == "contentdocument") { + return element.contentDocument; } - return true; + } + + 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); + } + })(); } })(); -- cgit v1.1