| /* |
| * Copyright (C) 2013-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 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 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. |
| */ |
| |
| FrontendTestHarness = class FrontendTestHarness extends TestHarness |
| { |
| constructor() |
| { |
| super(); |
| |
| this._results = []; |
| this._shouldResendResults = true; |
| |
| // Options that are set per-test for debugging purposes. |
| this.dumpActivityToSystemConsole = false; |
| } |
| |
| // TestHarness Overrides |
| |
| completeTest() |
| { |
| if (this.dumpActivityToSystemConsole) |
| InspectorFrontendHost.unbufferedLog("completeTest()"); |
| |
| // Wait for results to be resent before requesting completeTest(). Otherwise, messages will be |
| // queued after pending dispatches run to zero and the test page will quit before processing them. |
| if (this._testPageIsReloading) { |
| this._completeTestAfterReload = true; |
| return; |
| } |
| |
| InspectorBackend.runAfterPendingDispatches(this.evaluateInPage.bind(this, "TestPage.completeTest()")); |
| } |
| |
| addResult(message) |
| { |
| let stringifiedMessage = TestHarness.messageAsString(message); |
| |
| // Save the stringified message, since message may be a DOM element that won't survive reload. |
| this._results.push(stringifiedMessage); |
| |
| if (this.dumpActivityToSystemConsole) |
| InspectorFrontendHost.unbufferedLog(stringifiedMessage); |
| |
| if (!this._testPageIsReloading) |
| this.evaluateInPage(`TestPage.addResult(unescape("${escape(stringifiedMessage)}"))`); |
| } |
| |
| debugLog(message) |
| { |
| let stringifiedMessage = TestHarness.messageAsString(message); |
| |
| if (this.dumpActivityToSystemConsole) |
| InspectorFrontendHost.unbufferedLog(stringifiedMessage); |
| |
| this.evaluateInPage(`TestPage.debugLog(unescape("${escape(stringifiedMessage)}"));`); |
| } |
| |
| evaluateInPage(expression, callback, options={}) |
| { |
| let remoteObjectOnly = !!options.remoteObjectOnly; |
| |
| // If we load this page outside of the inspector, or hit an early error when loading |
| // the test frontend, then defer evaluating the commands (indefinitely in the former case). |
| if (this._originalConsole && !window.RuntimeAgent) { |
| this._originalConsole["error"]("Tried to evaluate in test page, but connection not yet established:", expression); |
| return; |
| } |
| |
| // Return primitive values directly, otherwise return a WI.RemoteObject instance. |
| function translateResult(result) { |
| let remoteObject = WI.RemoteObject.fromPayload(result); |
| return (!remoteObjectOnly && remoteObject.hasValue()) ? remoteObject.value : remoteObject; |
| } |
| |
| let response = RuntimeAgent.evaluate.invoke({expression, objectGroup: "test", includeCommandLineAPI: false}); |
| if (callback && typeof callback === "function") { |
| response = response.then(({result, wasThrown}) => callback(null, translateResult(result), wasThrown)); |
| response = response.catch((error) => callback(error, null, false)); |
| } else { |
| // Turn a thrown Error result into a promise rejection. |
| return response.then(({result, wasThrown}) => { |
| result = translateResult(result); |
| if (result && wasThrown) |
| return Promise.reject(new Error(result.description)); |
| return Promise.resolve(result); |
| }); |
| } |
| } |
| |
| debug() |
| { |
| this.dumpActivityToSystemConsole = true; |
| InspectorBackend.dumpInspectorProtocolMessages = true; |
| } |
| |
| // Frontend test-specific methods. |
| |
| expectNoError(error) |
| { |
| if (error) { |
| InspectorTest.log("PROTOCOL ERROR: " + error); |
| InspectorTest.completeTest(); |
| throw "PROTOCOL ERROR"; |
| } |
| } |
| |
| testPageDidLoad() |
| { |
| if (this.dumpActivityToSystemConsole) |
| InspectorFrontendHost.unbufferedLog("testPageDidLoad()"); |
| |
| this._testPageIsReloading = false; |
| this._resendResults(); |
| |
| this.dispatchEventToListeners(FrontendTestHarness.Event.TestPageDidLoad); |
| |
| if (this._completeTestAfterReload) |
| this.completeTest(); |
| } |
| |
| reloadPage(options = {}) |
| { |
| console.assert(!this._testPageIsReloading); |
| console.assert(!this._testPageReloadedOnce); |
| |
| this._testPageIsReloading = true; |
| |
| let {ignoreCache, revalidateAllResources} = options; |
| ignoreCache = !!ignoreCache; |
| revalidateAllResources = !!revalidateAllResources; |
| |
| return PageAgent.reload.invoke({ignoreCache, revalidateAllResources}) |
| .then(() => { |
| this._shouldResendResults = true; |
| this._testPageReloadedOnce = true; |
| |
| return Promise.resolve(null); |
| }); |
| } |
| |
| redirectRequestAnimationFrame() |
| { |
| console.assert(!this._originalRequestAnimationFrame); |
| if (this._originalRequestAnimationFrame) |
| return; |
| |
| this._originalRequestAnimationFrame = window.requestAnimationFrame; |
| this._requestAnimationFrameCallbacks = new Map; |
| this._nextRequestIdentifier = 1; |
| |
| window.requestAnimationFrame = (callback) => { |
| let requestIdentifier = this._nextRequestIdentifier++; |
| this._requestAnimationFrameCallbacks.set(requestIdentifier, callback); |
| if (this._requestAnimationFrameTimer) |
| return requestIdentifier; |
| |
| let dispatchCallbacks = () => { |
| let callbacks = this._requestAnimationFrameCallbacks; |
| this._requestAnimationFrameCallbacks = new Map; |
| this._requestAnimationFrameTimer = undefined; |
| let timestamp = window.performance.now(); |
| for (let callback of callbacks.values()) |
| callback(timestamp); |
| }; |
| |
| this._requestAnimationFrameTimer = setTimeout(dispatchCallbacks, 0); |
| return requestIdentifier; |
| }; |
| |
| window.cancelAnimationFrame = (requestIdentifier) => { |
| if (!this._requestAnimationFrameCallbacks.delete(requestIdentifier)) |
| return; |
| |
| if (!this._requestAnimationFrameCallbacks.size) { |
| clearTimeout(this._requestAnimationFrameTimer); |
| this._requestAnimationFrameTimer = undefined; |
| } |
| }; |
| } |
| |
| redirectConsoleToTestOutput() |
| { |
| // We can't use arrow functions here because of 'arguments'. It might |
| // be okay once rest parameters work. |
| let self = this; |
| function createProxyConsoleHandler(type) { |
| return function() { |
| self.addResult(`${type}: ` + Array.from(arguments).join(" ")); |
| }; |
| } |
| |
| function createProxyConsoleTraceHandler(){ |
| return function() { |
| try { |
| throw new Exception(); |
| } catch (e) { |
| // Skip the first frame which is added by this function. |
| let frames = e.stack.split("\n").slice(1); |
| let sanitizedFrames = frames.map(TestHarness.sanitizeStackFrame); |
| self.addResult("TRACE: " + Array.from(arguments).join(" ")); |
| self.addResult(sanitizedFrames.join("\n")); |
| } |
| }; |
| } |
| |
| let redirectedMethods = {}; |
| for (let key in window.console) |
| redirectedMethods[key] = window.console[key]; |
| |
| for (let type of ["log", "error", "info", "warn"]) |
| redirectedMethods[type] = createProxyConsoleHandler(type.toUpperCase()); |
| |
| redirectedMethods["trace"] = createProxyConsoleTraceHandler(); |
| |
| this._originalConsole = window.console; |
| window.console = redirectedMethods; |
| } |
| |
| reportUnhandledRejection(error) |
| { |
| let message = error.message; |
| let stack = error.stack; |
| let result = `Unhandled promise rejection in inspector page: ${message}\n`; |
| if (stack) { |
| let sanitizedStack = this.sanitizeStack(stack); |
| result += `\nStack Trace: ${sanitizedStack}\n`; |
| } |
| |
| // If the connection to the test page is not set up, then just dump to console and give up. |
| // Errors encountered this early can be debugged by loading Test.html in a normal browser page. |
| if (this._originalConsole && !this._testPageHasLoaded()) |
| this._originalConsole["error"](result); |
| |
| this.addResult(result); |
| this.completeTest(); |
| |
| // Stop default handler so we can empty InspectorBackend's message queue. |
| return true; |
| } |
| |
| reportUncaughtExceptionFromEvent(message, url, lineNumber, columnNumber) |
| { |
| // An exception thrown from a timer callback does not report a URL. |
| if (url === "undefined") |
| url = "global"; |
| |
| return this.reportUncaughtException({message, url, lineNumber, columnNumber}); |
| } |
| |
| reportUncaughtException({message, url, lineNumber, columnNumber, stack, code}) |
| { |
| let result; |
| let sanitizedURL = TestHarness.sanitizeURL(url); |
| let sanitizedStack = this.sanitizeStack(stack); |
| if (url || lineNumber || columnNumber) |
| result = `Uncaught exception in Inspector page: ${message} [${sanitizedURL}:${lineNumber}:${columnNumber}]\n`; |
| else |
| result = `Uncaught exception in Inspector page: ${message}\n`; |
| |
| if (stack) |
| result += `\nStack Trace:\n${sanitizedStack}\n`; |
| if (code) |
| result += `\nEvaluated Code:\n${code}`; |
| |
| // If the connection to the test page is not set up, then just dump to console and give up. |
| // Errors encountered this early can be debugged by loading Test.html in a normal browser page. |
| if (this._originalConsole && !this._testPageHasLoaded()) |
| this._originalConsole["error"](result); |
| |
| this.addResult(result); |
| this.completeTest(); |
| // Stop default handler so we can empty InspectorBackend's message queue. |
| return true; |
| } |
| |
| // Private |
| |
| _testPageHasLoaded() |
| { |
| return self._shouldResendResults; |
| } |
| |
| _resendResults() |
| { |
| console.assert(this._shouldResendResults); |
| this._shouldResendResults = false; |
| |
| if (this.dumpActivityToSystemConsole) |
| InspectorFrontendHost.unbufferedLog("_resendResults()"); |
| |
| for (let result of this._results) |
| this.evaluateInPage(`TestPage.addResult(unescape("${escape(result)}"))`); |
| } |
| }; |
| |
| FrontendTestHarness.Event = { |
| TestPageDidLoad: "frontend-test-test-page-did-load" |
| }; |