diff options
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Test')
7 files changed, 1301 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Test/FrontendTestHarness.js b/Source/WebInspectorUI/UserInterface/Test/FrontendTestHarness.js new file mode 100644 index 000000000..5d9bb202e --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Test/FrontendTestHarness.js @@ -0,0 +1,256 @@ +/* + * 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) + { + // 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; + } + + RuntimeAgent.evaluate.invoke({expression, objectGroup: "test", includeCommandLineAPI: false}, callback); + } + + 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(shouldIgnoreCache) + { + console.assert(!this._testPageIsReloading); + console.assert(!this._testPageReloadedOnce); + + this._testPageIsReloading = true; + + return PageAgent.reload(!!shouldIgnoreCache) + .then(() => { + this._shouldResendResults = true; + this._testPageReloadedOnce = true; + + return Promise.resolve(null); + }); + } + + 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" +}; diff --git a/Source/WebInspectorUI/UserInterface/Test/InspectorProtocol.js b/Source/WebInspectorUI/UserInterface/Test/InspectorProtocol.js new file mode 100644 index 000000000..60ec624f1 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Test/InspectorProtocol.js @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2012 Samsung Electronics. All rights reserved. + * Copyright (C) 2014, 2015 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. + */ + +InspectorProtocol = {}; +InspectorProtocol._dispatchTable = []; +InspectorProtocol._placeholderRequestIds = []; +InspectorProtocol._requestId = -1; +InspectorProtocol.eventHandler = {}; + +InspectorProtocol.sendCommand = function(methodOrObject, params, handler) +{ + // Allow new-style arguments object, as in awaitCommand. + let method = methodOrObject; + if (typeof methodOrObject === "object") + ({method, params, handler} = methodOrObject); + else if (!params) + params = {}; + + this._dispatchTable[++this._requestId] = handler; + let messageObject = {method, params, id: this._requestId}; + this._sendMessage(messageObject); + + return this._requestId; +}; + +InspectorProtocol.awaitCommand = function(args) +{ + let {method, params} = args; + let messageObject = {method, params, id: ++this._requestId}; + + return this.awaitMessage(messageObject); +}; + +InspectorProtocol.awaitMessage = function(messageObject) +{ + // Send a raw message to the backend. Mostly used to test the backend's error handling. + return new Promise((resolve, reject) => { + let requestId = messageObject.id; + + // If the caller did not provide an id, then make one up so that the response + // can be used to settle a promise. + if (typeof requestId !== "number") { + requestId = ++this._requestId; + this._placeholderRequestIds.push(requestId); + } + + this._dispatchTable[requestId] = {resolve, reject}; + this._sendMessage(messageObject); + }); +}; + +InspectorProtocol.awaitEvent = function(args) +{ + let event = args.event; + if (typeof event !== "string") + throw new Error("Event must be a string."); + + return new Promise((resolve, reject) => { + InspectorProtocol.eventHandler[event] = function(message) { + InspectorProtocol.eventHandler[event] = undefined; + resolve(message); + }; + }); +}; + +InspectorProtocol._sendMessage = function(messageObject) +{ + let messageString = typeof messageObject !== "string" ? JSON.stringify(messageObject) : messageObject; + + if (ProtocolTest.dumpInspectorProtocolMessages) + InspectorFrontendHost.unbufferedLog(`frontend: ${messageString}`); + + InspectorFrontendHost.sendMessageToBackend(messageString); +}; + +InspectorProtocol.addEventListener = function(eventTypeOrObject, listener) +{ + let event = eventTypeOrObject; + if (typeof eventTypeOrObject === "object") + ({event, listener} = eventTypeOrObject); + + if (typeof event !== "string") + throw new Error("Event name must be a string."); + + if (typeof listener !== "function") + throw new Error("Event listener must be callable."); + + // Convert to an array of listeners. + let listeners = InspectorProtocol.eventHandler[event]; + if (!listeners) + listeners = InspectorProtocol.eventHandler[event] = []; + else if (typeof listeners === "function") + listeners = InspectorProtocol.eventHandler[event] = [listeners]; + + // Prevent registering multiple times. + if (listeners.includes(listener)) + throw new Error("Cannot register the same listener more than once."); + + listeners.push(listener); +}; + +InspectorProtocol.checkForError = function(responseObject) +{ + if (responseObject.error) { + ProtocolTest.log("PROTOCOL ERROR: " + JSON.stringify(responseObject.error)); + ProtocolTest.completeTest(); + throw "PROTOCOL ERROR"; + } +}; + +InspectorProtocol.dispatchMessageFromBackend = function(messageObject) +{ + // This matches the debug dumping in InspectorBackend, which is bypassed + // by InspectorProtocol. Return messages should be dumped by InspectorBackend. + if (ProtocolTest.dumpInspectorProtocolMessages) + InspectorFrontendHost.unbufferedLog("backend: " + JSON.stringify(messageObject)); + + // If the message has an id, then it is a reply to a command. + let messageId = messageObject.id; + + // If the id is 'null', then it may be an error response. + if (messageId === null) + messageId = InspectorProtocol._placeholderRequestIds.shift(); + + // If we could figure out a requestId, then dispatch the message. + if (typeof messageId === "number") { + let handler = InspectorProtocol._dispatchTable[messageId]; + if (!handler) + return; + + if (typeof handler === "function") + handler(messageObject); + else if (typeof handler === "object") { + let {resolve, reject} = handler; + if ("error" in messageObject) + reject(messageObject.error); + else + resolve(messageObject.result); + } + } else { + // Otherwise, it is an event. + let eventName = messageObject["method"]; + let handler = InspectorProtocol.eventHandler[eventName]; + if (!handler) + return; + + if (typeof handler === "function") + handler(messageObject); + else if (handler instanceof Array) { + handler.map((listener) => { listener.call(null, messageObject); }); + } else if (typeof handler === "object") { + let {resolve, reject} = handler; + if ("error" in messageObject) + reject(messageObject.error); + else + resolve(messageObject.result); + } + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Test/ProtocolTestHarness.js b/Source/WebInspectorUI/UserInterface/Test/ProtocolTestHarness.js new file mode 100644 index 000000000..d0ef614b6 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Test/ProtocolTestHarness.js @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 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. + */ + +ProtocolTestHarness = class ProtocolTestHarness extends TestHarness +{ + // TestHarness Overrides + + completeTest() + { + if (this.dumpActivityToSystemConsole) + InspectorFrontendHost.unbufferedLog("completeTest()"); + + this.evaluateInPage("TestPage.closeTest();"); + } + + addResult(message) + { + let stringifiedMessage = TestHarness.messageAsString(message); + + if (this.dumpActivityToSystemConsole) + InspectorFrontendHost.unbufferedLog(stringifiedMessage); + + // Unfortunately, every string argument must be escaped because tests are not consistent + // with respect to escaping with single or double quotes. Some exceptions use single quotes. + this.evaluateInPage(`TestPage.log(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) + { + let args = { + method: "Runtime.evaluate", + params: {expression} + }; + + if (typeof callback === "function") + InspectorProtocol.sendCommand(args, callback); + else + return InspectorProtocol.awaitCommand(args); + } + + debug() + { + this.dumpActivityToSystemConsole = true; + this.dumpInspectorProtocolMessages = true; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Test/Test.js b/Source/WebInspectorUI/UserInterface/Test/Test.js new file mode 100644 index 000000000..f59bf7304 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Test/Test.js @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2013-2015 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 THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT + * HOLDER OR 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. + */ + +WebInspector.DebuggableType = { + Web: "web", + JavaScript: "javascript" +}; + +WebInspector.loaded = function() +{ + this.debuggableType = WebInspector.DebuggableType.Web; + this.hasExtraDomains = false; + + // Register observers for events from the InspectorBackend. + // The initialization order should match the same in Main.js. + InspectorBackend.registerInspectorDispatcher(new WebInspector.InspectorObserver); + InspectorBackend.registerPageDispatcher(new WebInspector.PageObserver); + InspectorBackend.registerConsoleDispatcher(new WebInspector.ConsoleObserver); + InspectorBackend.registerDOMDispatcher(new WebInspector.DOMObserver); + InspectorBackend.registerNetworkDispatcher(new WebInspector.NetworkObserver); + InspectorBackend.registerDebuggerDispatcher(new WebInspector.DebuggerObserver); + InspectorBackend.registerHeapDispatcher(new WebInspector.HeapObserver); + InspectorBackend.registerDOMStorageDispatcher(new WebInspector.DOMStorageObserver); + InspectorBackend.registerTimelineDispatcher(new WebInspector.TimelineObserver); + InspectorBackend.registerCSSDispatcher(new WebInspector.CSSObserver); + InspectorBackend.registerRuntimeDispatcher(new WebInspector.RuntimeObserver); + InspectorBackend.registerWorkerDispatcher(new WebInspector.WorkerObserver); + if (InspectorBackend.registerReplayDispatcher) + InspectorBackend.registerReplayDispatcher(new WebInspector.ReplayObserver); + + WebInspector.mainTarget = new WebInspector.MainTarget; + + // Instantiate controllers used by tests. + this.targetManager = new WebInspector.TargetManager; + this.frameResourceManager = new WebInspector.FrameResourceManager; + this.storageManager = new WebInspector.StorageManager; + this.domTreeManager = new WebInspector.DOMTreeManager; + this.cssStyleManager = new WebInspector.CSSStyleManager; + this.logManager = new WebInspector.LogManager; + this.issueManager = new WebInspector.IssueManager; + this.runtimeManager = new WebInspector.RuntimeManager; + this.heapManager = new WebInspector.HeapManager; + this.memoryManager = new WebInspector.MemoryManager; + this.timelineManager = new WebInspector.TimelineManager; + this.debuggerManager = new WebInspector.DebuggerManager; + this.probeManager = new WebInspector.ProbeManager; + this.workerManager = new WebInspector.WorkerManager; + this.replayManager = new WebInspector.ReplayManager; + + document.addEventListener("DOMContentLoaded", this.contentLoaded); + + // Enable agents. + InspectorAgent.enable(); + ConsoleAgent.enable(); + + // Perform one-time tasks. + WebInspector.CSSCompletions.requestCSSCompletions(); + + // Global settings. + this.showShadowDOMSetting = new WebInspector.Setting("show-shadow-dom", true); +}; + +WebInspector.contentLoaded = function() +{ + // Signal that the frontend is now ready to receive messages. + InspectorFrontendAPI.loadCompleted(); + + // Tell the InspectorFrontendHost we loaded, which causes the window to display + // and pending InspectorFrontendAPI commands to be sent. + InspectorFrontendHost.loaded(); +}; + +Object.defineProperty(WebInspector, "targets", +{ + get() { return this.targetManager.targets; } +}); + +WebInspector.assumingMainTarget = () => WebInspector.mainTarget; + +WebInspector.isDebugUIEnabled = () => false; + +WebInspector.UIString = (string) => string; + +WebInspector.indentString = () => " "; + +// Add stubs that are called by the frontend API. +WebInspector.updateDockedState = () => {}; +WebInspector.updateDockingAvailability = () => {}; +WebInspector.updateVisibilityState = () => {}; + +window.InspectorTest = new FrontendTestHarness(); + +InspectorTest.redirectConsoleToTestOutput(); + +WebInspector.reportInternalError = (e) => { console.error(e); }; + +window.reportUnhandledRejection = InspectorTest.reportUnhandledRejection.bind(InspectorTest); +window.onerror = InspectorTest.reportUncaughtExceptionFromEvent.bind(InspectorTest); diff --git a/Source/WebInspectorUI/UserInterface/Test/TestHarness.js b/Source/WebInspectorUI/UserInterface/Test/TestHarness.js new file mode 100644 index 000000000..53bee4a4b --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Test/TestHarness.js @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2015, 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. + */ + +TestHarness = class TestHarness extends WebInspector.Object +{ + constructor() + { + super(); + + this._logCount = 0; + this._failureObjects = new Map; + this._failureObjectIdentifier = 1; + + // Options that are set per-test for debugging purposes. + this.forceDebugLogging = false; + + // Options that are set per-test to ensure deterministic output. + this.suppressStackTraces = false; + } + + completeTest() + { + throw new Error("Must be implemented by subclasses."); + } + + addResult() + { + throw new Error("Must be implemented by subclasses."); + } + + debugLog() + { + throw new Error("Must be implemented by subclasses."); + } + + evaluateInPage(string, callback) + { + throw new Error("Must be implemented by subclasses."); + } + + debug() + { + throw new Error("Must be implemented by subclasses."); + } + + createAsyncSuite(name) + { + return new AsyncTestSuite(this, name); + } + + createSyncSuite(name) + { + return new SyncTestSuite(this, name); + } + + get logCount() + { + return this._logCount; + } + + log(message) + { + ++this._logCount; + + if (this.forceDebugLogging) + this.debugLog(message); + else + this.addResult(message); + } + + assert(condition, message) + { + if (condition) + return; + + let stringifiedMessage = TestHarness.messageAsString(message); + this.log("ASSERT: " + stringifiedMessage); + } + + expectThat(actual, message) + { + this._expect(TestHarness.ExpectationType.True, !!actual, message, actual); + } + + expectFalse(actual, message) + { + this._expect(TestHarness.ExpectationType.False, !actual, message, actual); + } + + expectNull(actual, message) + { + this._expect(TestHarness.ExpectationType.Null, actual === null, message, actual, null); + } + + expectNotNull(actual, message) + { + this._expect(TestHarness.ExpectationType.NotNull, actual !== null, message, actual); + } + + expectEqual(actual, expected, message) + { + this._expect(TestHarness.ExpectationType.Equal, expected === actual, message, actual, expected); + } + + expectNotEqual(actual, expected, message) + { + this._expect(TestHarness.ExpectationType.NotEqual, expected !== actual, message, actual, expected); + } + + expectShallowEqual(actual, expected, message) + { + this._expect(TestHarness.ExpectationType.ShallowEqual, Object.shallowEqual(actual, expected), message, actual, expected); + } + + expectNotShallowEqual(actual, expected, message) + { + this._expect(TestHarness.ExpectationType.NotShallowEqual, !Object.shallowEqual(actual, expected), message, actual, expected); + } + + expectEqualWithAccuracy(actual, expected, accuracy, message) + { + console.assert(typeof expected === "number"); + console.assert(typeof actual === "number"); + + this._expect(TestHarness.ExpectationType.EqualWithAccuracy, Math.abs(expected - actual) <= accuracy, message, actual, expected, accuracy); + } + + expectLessThan(actual, expected, message) + { + this._expect(TestHarness.ExpectationType.LessThan, actual < expected, message, actual, expected); + } + + expectLessThanOrEqual(actual, expected, message) + { + this._expect(TestHarness.ExpectationType.LessThanOrEqual, actual <= expected, message, actual, expected); + } + + expectGreaterThan(actual, expected, message) + { + this._expect(TestHarness.ExpectationType.GreaterThan, actual > expected, message, actual, expected); + } + + expectGreaterThanOrEqual(actual, expected, message) + { + this._expect(TestHarness.ExpectationType.GreaterThanOrEqual, actual >= expected, message, actual, expected); + } + + pass(message) + { + let stringifiedMessage = TestHarness.messageAsString(message); + this.log("PASS: " + stringifiedMessage); + } + + fail(message) + { + let stringifiedMessage = TestHarness.messageAsString(message); + this.log("FAIL: " + stringifiedMessage); + } + + // Protected + + static messageAsString(message) + { + if (message instanceof Element) + return message.textContent; + + return (typeof message !== "string") ? JSON.stringify(message) : message; + } + + static sanitizeURL(url) + { + if (!url) + return "(unknown)"; + + let lastPathSeparator = Math.max(url.lastIndexOf("/"), url.lastIndexOf("\\")); + let location = (lastPathSeparator > 0) ? url.substr(lastPathSeparator + 1) : url; + if (!location.length) + location = "(unknown)"; + + // Clean up the location so it is bracketed or in parenthesis. + if (url.indexOf("[native code]") !== -1) + location = "[native code]"; + + return location; + } + + static sanitizeStackFrame(frame, i) + { + // Most frames are of the form "functionName@file:///foo/bar/File.js:345". + // But, some frames do not have a functionName. Get rid of the file path. + let nameAndURLSeparator = frame.indexOf("@"); + let frameName = (nameAndURLSeparator > 0) ? frame.substr(0, nameAndURLSeparator) : "(anonymous)"; + + let lastPathSeparator = Math.max(frame.lastIndexOf("/"), frame.lastIndexOf("\\")); + let frameLocation = (lastPathSeparator > 0) ? frame.substr(lastPathSeparator + 1) : frame; + if (!frameLocation.length) + frameLocation = "unknown"; + + // Clean up the location so it is bracketed or in parenthesis. + if (frame.indexOf("[native code]") !== -1) + frameLocation = "[native code]"; + else + frameLocation = "(" + frameLocation + ")"; + + return `#${i}: ${frameName} ${frameLocation}`; + } + + sanitizeStack(stack) + { + if (this.suppressStackTraces) + return "(suppressed)"; + + if (!stack || typeof stack !== "string") + return "(unknown)"; + + return stack.split("\n").map(TestHarness.sanitizeStackFrame).join("\n"); + } + + // Private + + _expect(type, condition, message, ...values) + { + console.assert(values.length > 0, "Should have an 'actual' value."); + + if (!message || !condition) { + values = values.map(this._expectationValueAsString.bind(this)); + message = message || this._expectationMessageFormat(type).format(...values); + } + + if (condition) { + this.pass(message); + return; + } + + message += "\n Expected: " + this._expectedValueFormat(type).format(...values.slice(1)); + message += "\n Actual: " + values[0]; + + this.fail(message); + } + + _expectationValueAsString(value) + { + let instanceIdentifier = (object) => { + let id = this._failureObjects.get(object); + if (!id) { + id = this._failureObjectIdentifier++; + this._failureObjects.set(object, id); + } + return "#" + id; + }; + + const maximumValueStringLength = 200; + const defaultValueString = String(new Object); // [object Object] + + // Special case for numbers, since JSON.stringify converts Infinity and NaN to null. + if (typeof value === "number") + return value; + + try { + let valueString = JSON.stringify(value); + if (valueString.length <= maximumValueStringLength) + return valueString; + } catch (e) {} + + try { + let valueString = String(value); + if (valueString === defaultValueString && value.constructor && value.constructor.name !== "Object") + return value.constructor.name + " instance " + instanceIdentifier(value); + return valueString; + } catch (e) { + return defaultValueString; + } + } + + _expectationMessageFormat(type) + { + switch (type) { + case TestHarness.ExpectationType.True: + return "expectThat(%s)"; + case TestHarness.ExpectationType.False: + return "expectFalse(%s)"; + case TestHarness.ExpectationType.Null: + return "expectNull(%s)"; + case TestHarness.ExpectationType.NotNull: + return "expectNotNull(%s)"; + case TestHarness.ExpectationType.Equal: + return "expectEqual(%s, %s)"; + case TestHarness.ExpectationType.NotEqual: + return "expectNotEqual(%s, %s)"; + case TestHarness.ExpectationType.ShallowEqual: + return "expectShallowEqual(%s, %s)"; + case TestHarness.ExpectationType.NotShallowEqual: + return "expectNotShallowEqual(%s, %s)"; + case TestHarness.ExpectationType.EqualWithAccuracy: + return "expectEqualWithAccuracy(%s, %s, %s)"; + case TestHarness.ExpectationType.LessThan: + return "expectLessThan(%s, %s)"; + case TestHarness.ExpectationType.LessThanOrEqual: + return "expectLessThanOrEqual(%s, %s)"; + case TestHarness.ExpectationType.GreaterThan: + return "expectGreaterThan(%s, %s)"; + case TestHarness.ExpectationType.GreaterThanOrEqual: + return "expectGreaterThanOrEqual(%s, %s)"; + default: + console.error("Unknown TestHarness.ExpectationType type: " + type); + return null; + } + } + + _expectedValueFormat(type) + { + switch (type) { + case TestHarness.ExpectationType.True: + return "truthy"; + case TestHarness.ExpectationType.False: + return "falsey"; + case TestHarness.ExpectationType.NotNull: + return "not null"; + case TestHarness.ExpectationType.NotEqual: + case TestHarness.ExpectationType.NotShallowEqual: + return "not %s"; + case TestHarness.ExpectationType.EqualWithAccuracy: + return "%s +/- %s"; + case TestHarness.ExpectationType.LessThan: + return "less than %s"; + case TestHarness.ExpectationType.LessThanOrEqual: + return "less than or equal to %s"; + case TestHarness.ExpectationType.GreaterThan: + return "greater than %s"; + case TestHarness.ExpectationType.GreaterThanOrEqual: + return "greater than or equal to %s"; + default: + return "%s"; + } + } +}; + +TestHarness.ExpectationType = { + True: Symbol("expect-true"), + False: Symbol("expect-false"), + Null: Symbol("expect-null"), + NotNull: Symbol("expect-not-null"), + Equal: Symbol("expect-equal"), + NotEqual: Symbol("expect-not-equal"), + ShallowEqual: Symbol("expect-shallow-equal"), + NotShallowEqual: Symbol("expect-not-shallow-equal"), + EqualWithAccuracy: Symbol("expect-equal-with-accuracy"), + LessThan: Symbol("expect-less-than"), + LessThanOrEqual: Symbol("expect-less-than-or-equal"), + GreaterThan: Symbol("expect-greater-than"), + GreaterThanOrEqual: Symbol("expect-greater-than-or-equal"), +}; diff --git a/Source/WebInspectorUI/UserInterface/Test/TestStub.js b/Source/WebInspectorUI/UserInterface/Test/TestStub.js new file mode 100644 index 000000000..cda7c7ac6 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Test/TestStub.js @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 Samsung Electronics. All rights reserved. + * Copyright (C) 2014, 2015 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. + */ + +InspectorFrontendAPI = {}; +InspectorFrontendAPI.dispatchMessageAsync = InspectorProtocol.dispatchMessageFromBackend; + +window.ProtocolTest = new ProtocolTestHarness(); + +window.addEventListener("message", (event) => { + try { + eval(event.data); + } catch (e) { + alert(e.stack); + ProtocolTest.completeTest(); + throw e; + } +}); diff --git a/Source/WebInspectorUI/UserInterface/Test/TestSuite.js b/Source/WebInspectorUI/UserInterface/Test/TestSuite.js new file mode 100644 index 000000000..aacd313b7 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Test/TestSuite.js @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2015 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. + */ + +TestSuite = class TestSuite extends WebInspector.Object +{ + constructor(harness, name) { + if (!(harness instanceof TestHarness)) + throw new Error("Must pass the test's harness as the first argument."); + + if (typeof name !== "string" || !name.trim().length) + throw new Error("Tried to create TestSuite without string suite name."); + + super(); + + this.name = name; + this._harness = harness; + + this.testcases = []; + this.runCount = 0; + this.failCount = 0; + } + + // Use this if the test file only has one suite, and no handling + // of the value returned by runTestCases() is needed. + runTestCasesAndFinish() + { + throw new Error("Must be implemented by subclasses."); + } + + runTestCases() + { + throw new Error("Must be implemented by subclasses."); + } + + get passCount() + { + return this.runCount - this.failCount; + } + + get skipCount() + { + if (this.failCount) + return this.testcases.length - this.runCount; + else + return 0; + } + + addTestCase(testcase) + { + if (!testcase || !(testcase instanceof Object)) + throw new Error("Tried to add non-object test case."); + + if (typeof testcase.name !== "string" || !testcase.name.trim().length) + throw new Error("Tried to add test case without a name."); + + if (typeof testcase.test !== "function") + throw new Error("Tried to add test case without `test` function."); + + if (testcase.setup && typeof testcase.setup !== "function") + throw new Error("Tried to add test case with invalid `setup` parameter (must be a function)."); + + if (testcase.teardown && typeof testcase.teardown !== "function") + throw new Error("Tried to add test case with invalid `teardown` parameter (must be a function)."); + + this.testcases.push(testcase); + } + + // Protected + + logThrownObject(e) + { + let message = e; + let stack = "(unknown)"; + if (e instanceof Error) { + message = e.message; + if (e.stack) + stack = e.stack; + } + + if (typeof message !== "string") + message = JSON.stringify(message); + + let sanitizedStack = this._harness.sanitizeStack(stack); + + let result = `!! EXCEPTION: ${message}`; + if (stack) + result += `\nStack Trace: ${sanitizedStack}`; + + this._harness.log(result); + } +}; + +AsyncTestSuite = class AsyncTestSuite extends TestSuite +{ + runTestCasesAndFinish() + { + let finish = () => { this._harness.completeTest(); }; + + this.runTestCases() + .then(finish) + .catch(finish); + } + + runTestCases() + { + if (!this.testcases.length) + throw new Error("Tried to call runTestCases() for suite with no test cases"); + if (this._startedRunning) + throw new Error("Tried to call runTestCases() more than once."); + + this._startedRunning = true; + + this._harness.log(""); + this._harness.log(`== Running test suite: ${this.name}`); + + // Avoid adding newlines if nothing was logged. + let priorLogCount = this._harness.logCount; + let result = this.testcases.reduce((chain, testcase, i) => { + if (testcase.setup) { + chain = chain.then(() => { + this._harness.log("-- Running test setup."); + return new Promise(testcase.setup); + }); + } + + chain = chain.then(() => { + if (i > 0 && priorLogCount + 1 < this._harness.logCount) + this._harness.log(""); + + priorLogCount = this._harness.logCount; + this._harness.log(`-- Running test case: ${testcase.name}`); + this.runCount++; + return new Promise(testcase.test); + }); + + if (testcase.teardown) { + chain = chain.then(() => { + this._harness.log("-- Running test teardown."); + return new Promise(testcase.teardown); + }); + } + return chain; + }, Promise.resolve()); + + return result.catch((e) => { + this.failCount++; + this.logThrownObject(e); + + throw e; // Reject this promise by re-throwing the error. + }); + } +}; + +SyncTestSuite = class SyncTestSuite extends TestSuite +{ + runTestCasesAndFinish() + { + this.runTestCases(); + this._harness.completeTest(); + } + + runTestCases() + { + if (!this.testcases.length) + throw new Error("Tried to call runTestCases() for suite with no test cases"); + if (this._startedRunning) + throw new Error("Tried to call runTestCases() more than once."); + + this._startedRunning = true; + + this._harness.log(""); + this._harness.log(`== Running test suite: ${this.name}`); + + let priorLogCount = this._harness.logCount; + for (let i = 0; i < this.testcases.length; i++) { + let testcase = this.testcases[i]; + if (i > 0 && priorLogCount + 1 < this._harness.logCount) + this._harness.log(""); + + priorLogCount = this._harness.logCount; + + // Run the setup action, if one was provided. + if (testcase.setup) { + this._harness.log("-- Running test setup."); + try { + let result = testcase.setup.call(null); + if (result === false) { + this._harness.log("!! SETUP FAILED"); + return false; + } + } catch (e) { + this.logThrownObject(e); + return false; + } + } + + this._harness.log("-- Running test case: " + testcase.name); + this.runCount++; + try { + let result = testcase.test.call(null); + if (result === false) { + this.failCount++; + return false; + } + } catch (e) { + this.failCount++; + this.logThrownObject(e); + return false; + } + + // Run the teardown action, if one was provided. + if (testcase.teardown) { + this._harness.log("-- Running test teardown."); + try { + let result = testcase.teardown.call(null); + if (result === false) { + this._harness.log("!! TEARDOWN FAILED"); + return false; + } + } catch (e) { + this.logThrownObject(e); + return false; + } + } + } + + return true; + } +}; |