| /* |
| * Copyright (C) 2016 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| //# sourceURL=__InjectedScript_WebAutomationSessionProxy.js |
| |
| (function (sessionIdentifier, evaluate, createUUID, isValidNodeIdentifier) { |
| |
| const sessionNodePropertyName = "session-node-" + sessionIdentifier; |
| |
| let AutomationSessionProxy = class AutomationSessionProxy |
| { |
| constructor() |
| { |
| this._nodeToIdMap = new Map; |
| this._idToNodeMap = new Map; |
| } |
| |
| // Public |
| |
| evaluateJavaScriptFunction(functionString, argumentStrings, expectsImplicitCallbackArgument, frameID, callbackID, resultCallback, callbackTimeout) |
| { |
| this._execute(functionString, argumentStrings, expectsImplicitCallbackArgument, callbackTimeout) |
| .then(result => { resultCallback(frameID, callbackID, this._jsonStringify(result)); }) |
| .catch(error => { resultCallback(frameID, callbackID, error); }); |
| } |
| |
| nodeForIdentifier(identifier) |
| { |
| this._clearStaleNodes(); |
| try { |
| return this._nodeForIdentifier(identifier); |
| } catch (error) { |
| return null; |
| } |
| } |
| |
| // Private |
| |
| _execute(functionString, argumentStrings, expectsImplicitCallbackArgument, callbackTimeout) |
| { |
| let timeoutPromise; |
| let timeoutIdentifier = 0; |
| if (callbackTimeout >= 0) { |
| timeoutPromise = new Promise((resolve, reject) => { |
| timeoutIdentifier = setTimeout(() => { |
| reject({ name: "JavaScriptTimeout", message: "script timed out after " + callbackTimeout + "ms" }); |
| }, callbackTimeout); |
| }); |
| } |
| |
| let promise = new Promise((resolve, reject) => { |
| // The script is expected to be a function declaration. Evaluate it inside parenthesis to get the function value. |
| let functionValue = evaluate("(async " + functionString + ")"); |
| if (typeof functionValue !== "function") |
| reject(new TypeError("Script did not evaluate to a function.")); |
| |
| this._clearStaleNodes(); |
| |
| let argumentValues = argumentStrings.map(this._jsonParse, this); |
| if (expectsImplicitCallbackArgument) |
| argumentValues.push(resolve); |
| let resultPromise = functionValue.apply(null, argumentValues); |
| |
| let promises = [resultPromise]; |
| if (timeoutPromise) |
| promises.push(timeoutPromise); |
| Promise.race(promises) |
| .then(result => { |
| if (!expectsImplicitCallbackArgument) { |
| resolve(result); |
| } |
| }) |
| .catch(error => { |
| reject(error); |
| }); |
| }); |
| |
| // Async scripts can call Promise.resolve() in the function script, generating a new promise that is resolved in a |
| // timer (see w3c test execute_async_script/promise.py::test_promise_resolve_timeout). In that case, the internal race |
| // finishes resolved, so we need to start a new one here to wait for the second promise to be resolved or the timeout. |
| let promises = [promise]; |
| if (timeoutPromise) |
| promises.push(timeoutPromise); |
| return Promise.race(promises) |
| .finally(() => { |
| if (timeoutIdentifier) { |
| clearTimeout(timeoutIdentifier); |
| } |
| }); |
| } |
| |
| _jsonParse(string) |
| { |
| if (!string) |
| return undefined; |
| return JSON.parse(string, (key, value) => this._reviveJSONValue(key, value)); |
| } |
| |
| _jsonStringify(value) |
| { |
| return JSON.stringify(this._jsonClone(value)); |
| } |
| |
| _reviveJSONValue(key, value) |
| { |
| if (value && typeof value === "object" && value[sessionNodePropertyName]) |
| return this._nodeForIdentifier(value[sessionNodePropertyName]); |
| return value; |
| } |
| |
| _isCollection(value) { |
| switch (Object.prototype.toString.call(value)) { |
| case "[object Arguments]": |
| case "[object Array]": |
| case "[object FileList]": |
| case "[object HTMLAllCollection]": |
| case "[object HTMLCollection]": |
| case "[object HTMLFormControlsCollection]": |
| case "[object HTMLOptionsCollection]": |
| case "[object NodeList]": |
| return true; |
| } |
| return false; |
| } |
| |
| _checkCyclic(value, stack = []) |
| { |
| function isCyclic(value, proxy, stack = []) { |
| if (value === undefined || value === null) |
| return false; |
| |
| if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") |
| return false; |
| |
| if (value instanceof Node) |
| return false; |
| |
| if (stack.includes(value)) |
| return true; |
| |
| if (proxy._isCollection(value)) { |
| stack.push(value); |
| for (let i = 0; i < value.length; i++) { |
| if (isCyclic(value[i], proxy, stack)) |
| return true; |
| } |
| |
| stack.pop(); |
| return false; |
| } |
| |
| stack.push(value); |
| for (let property in value) { |
| if (isCyclic(value[property], proxy, stack)) |
| return true; |
| } |
| |
| stack.pop(); |
| return false; |
| } |
| |
| if (isCyclic(value, this)) |
| throw new TypeError("cannot serialize cyclic structures."); |
| } |
| |
| _jsonClone(value) |
| { |
| // Internal JSON clone algorithm. |
| // https://w3c.github.io/webdriver/#dfn-internal-json-clone-algorithm |
| if (value === undefined || value === null) |
| return null; |
| |
| if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") |
| return value; |
| |
| if (this._isCollection(value)) { |
| this._checkCyclic(value); |
| return [...value].map(item => this._jsonClone(item)); |
| } |
| |
| if (value instanceof Node) |
| return this._createNodeHandle(value); |
| |
| // FIXME: implement window proxy serialization. |
| |
| if (typeof value.toJSON === "function") |
| return value.toJSON(); |
| |
| let customObject = {}; |
| for (let property in value) { |
| this._checkCyclic(value); |
| customObject[property] = this._jsonClone(value[property]); |
| } |
| return customObject; |
| } |
| |
| _createNodeHandle(node) |
| { |
| if (node.ownerDocument !== window.document || !node.isConnected) |
| throw {name: "NodeNotFound", message: "Stale element found when trying to create the node handle"}; |
| |
| return {[sessionNodePropertyName]: this._identifierForNode(node)}; |
| } |
| |
| _nodeForIdentifier(identifier) |
| { |
| if (!isValidNodeIdentifier(identifier)) |
| throw {name: "InvalidNodeIdentifier", message: "Node identifier '" + identifier + "' is invalid"}; |
| |
| let node = this._idToNodeMap.get(identifier); |
| if (node) |
| return node; |
| throw {name: "NodeNotFound", message: "Node with identifier '" + identifier + "' was not found"}; |
| } |
| |
| _identifierForNode(node) |
| { |
| let identifier = this._nodeToIdMap.get(node); |
| if (identifier) |
| return identifier; |
| |
| identifier = "node-" + createUUID(); |
| |
| this._nodeToIdMap.set(node, identifier); |
| this._idToNodeMap.set(identifier, node); |
| |
| return identifier; |
| } |
| |
| _clearStaleNodes() |
| { |
| for (var [node, identifier] of this._nodeToIdMap) { |
| if (!document.contains(node)) { |
| this._nodeToIdMap.delete(node); |
| this._idToNodeMap.delete(identifier); |
| } |
| } |
| } |
| }; |
| |
| return new AutomationSessionProxy; |
| |
| }) |