REGRESSION: [ Mac wk2 ] http/tests/inspector/target/provisional-load-cancels-previous...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Test / FrontendTestHarness.js
1 /*
2  * Copyright (C) 2013-2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
15  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
18  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
23  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 FrontendTestHarness = class FrontendTestHarness extends TestHarness
27 {
28     constructor()
29     {
30         super();
31
32         this._results = [];
33         this._testPageHasLoaded = false;
34
35         // Options that are set per-test for debugging purposes.
36         this.dumpActivityToSystemConsole = false;
37     }
38
39     // TestHarness Overrides
40
41     completeTest()
42     {
43         if (this.dumpActivityToSystemConsole)
44             InspectorFrontendHost.unbufferedLog("completeTest()");
45
46         // Wait for results to be resent before requesting completeTest(). Otherwise, messages will be
47         // queued after pending dispatches run to zero and the test page will quit before processing them.
48         if (this._testPageIsReloading) {
49             this._completeTestAfterReload = true;
50             return;
51         }
52
53         InspectorBackend.runAfterPendingDispatches(this.evaluateInPage.bind(this, "TestPage.completeTest()"));
54     }
55
56     addResult(message)
57     {
58         let stringifiedMessage = TestHarness.messageAsString(message);
59
60         // Save the stringified message, since message may be a DOM element that won't survive reload.
61         this._results.push(stringifiedMessage);
62
63         if (this.dumpActivityToSystemConsole)
64             InspectorFrontendHost.unbufferedLog(stringifiedMessage);
65
66         if (!this._testPageIsReloading)
67             this.evaluateInPage(`TestPage.addResult(unescape("${escape(stringifiedMessage)}"))`);
68     }
69
70     debugLog(message)
71     {
72         let stringifiedMessage = TestHarness.messageAsString(message);
73
74         if (this.dumpActivityToSystemConsole)
75             InspectorFrontendHost.unbufferedLog(stringifiedMessage);
76
77         this.evaluateInPage(`TestPage.debugLog(unescape("${escape(stringifiedMessage)}"));`);
78     }
79
80     evaluateInPage(expression, callback, options = {})
81     {
82         let remoteObjectOnly = !!options.remoteObjectOnly;
83         let target = WI.assumingMainTarget();
84
85         // If we load this page outside of the inspector, or hit an early error when loading
86         // the test frontend, then defer evaluating the commands (indefinitely in the former case).
87         if (this._originalConsole && (!target || !target.hasDomain("Runtime"))) {
88             this._originalConsole["error"]("Tried to evaluate in test page, but connection not yet established:", expression);
89             return;
90         }
91
92         // Return primitive values directly, otherwise return a WI.RemoteObject instance.
93         function translateResult(result) {
94             let remoteObject = WI.RemoteObject.fromPayload(result);
95             return (!remoteObjectOnly && remoteObject.hasValue()) ? remoteObject.value : remoteObject;
96         }
97
98         let response = target.RuntimeAgent.evaluate.invoke({expression, objectGroup: "test", includeCommandLineAPI: false});
99         if (callback && typeof callback === "function") {
100             response = response.then(({result, wasThrown}) => callback(null, translateResult(result), wasThrown));
101             response = response.catch((error) => callback(error, null, false));
102         } else {
103             // Turn a thrown Error result into a promise rejection.
104             return response.then(({result, wasThrown}) => {
105                 result = translateResult(result);
106                 if (result && wasThrown)
107                     return Promise.reject(new Error(result.description));
108                 return Promise.resolve(result);
109             });
110         }
111     }
112
113     debug()
114     {
115         this.dumpActivityToSystemConsole = true;
116         InspectorBackend.dumpInspectorProtocolMessages = true;
117     }
118
119     // Frontend test-specific methods.
120
121     expectNoError(error)
122     {
123         if (error) {
124             InspectorTest.log("PROTOCOL ERROR: " + error);
125             InspectorTest.completeTest();
126             throw "PROTOCOL ERROR";
127         }
128     }
129
130     deferOutputUntilTestPageIsReloaded()
131     {
132         console.assert(!this._testPageIsReloading);
133         this._testPageIsReloading = true;
134     }
135
136     testPageDidLoad()
137     {
138         if (this.dumpActivityToSystemConsole)
139             InspectorFrontendHost.unbufferedLog("testPageDidLoad()");
140
141         this._testPageIsReloading = false;
142         if (this._testPageHasLoaded)
143             this._resendResults();
144         else
145             this._testPageHasLoaded = true;
146
147         this.dispatchEventToListeners(FrontendTestHarness.Event.TestPageDidLoad);
148
149         if (this._completeTestAfterReload)
150             this.completeTest();
151     }
152
153     reloadPage(options = {})
154     {
155         console.assert(!this._testPageIsReloading);
156         console.assert(!this._testPageReloadedOnce);
157
158         this._testPageIsReloading = true;
159
160         let {ignoreCache, revalidateAllResources} = options;
161         ignoreCache = !!ignoreCache;
162         revalidateAllResources = !!revalidateAllResources;
163
164         let target = WI.assumingMainTarget();
165         return target.PageAgent.reload.invoke({ignoreCache, revalidateAllResources})
166             .then(() => {
167                 this._testPageReloadedOnce = true;
168
169                 return Promise.resolve(null);
170             });
171     }
172
173     redirectRequestAnimationFrame()
174     {
175         console.assert(!this._originalRequestAnimationFrame);
176         if (this._originalRequestAnimationFrame)
177             return;
178
179         this._originalRequestAnimationFrame = window.requestAnimationFrame;
180         this._requestAnimationFrameCallbacks = new Map;
181         this._nextRequestIdentifier = 1;
182
183         window.requestAnimationFrame = (callback) => {
184             let requestIdentifier = this._nextRequestIdentifier++;
185             this._requestAnimationFrameCallbacks.set(requestIdentifier, callback);
186             if (this._requestAnimationFrameTimer)
187                 return requestIdentifier;
188
189             let dispatchCallbacks = () => {
190                 let callbacks = this._requestAnimationFrameCallbacks;
191                 this._requestAnimationFrameCallbacks = new Map;
192                 this._requestAnimationFrameTimer = undefined;
193                 let timestamp = window.performance.now();
194                 for (let callback of callbacks.values())
195                     callback(timestamp);
196             };
197
198             this._requestAnimationFrameTimer = setTimeout(dispatchCallbacks, 0);
199             return requestIdentifier;
200         };
201
202         window.cancelAnimationFrame = (requestIdentifier) => {
203             if (!this._requestAnimationFrameCallbacks.delete(requestIdentifier))
204                 return;
205
206             if (!this._requestAnimationFrameCallbacks.size) {
207                 clearTimeout(this._requestAnimationFrameTimer);
208                 this._requestAnimationFrameTimer = undefined;
209             }
210         };
211     }
212
213     redirectConsoleToTestOutput()
214     {
215         // We can't use arrow functions here because of 'arguments'. It might
216         // be okay once rest parameters work.
217         let self = this;
218         function createProxyConsoleHandler(type) {
219             return function() {
220                 self.addResult(`${type}: ` + Array.from(arguments).join(" "));
221             };
222         }
223
224         function createProxyConsoleTraceHandler(){
225             return function() {
226                 try {
227                     throw new Exception();
228                 } catch (e) {
229                     // Skip the first frame which is added by this function.
230                     let frames = e.stack.split("\n").slice(1);
231                     let sanitizedFrames = frames.map(TestHarness.sanitizeStackFrame);
232                     self.addResult("TRACE: " + Array.from(arguments).join(" "));
233                     self.addResult(sanitizedFrames.join("\n"));
234                 }
235             };
236         }
237
238         let redirectedMethods = {};
239         for (let key in window.console)
240             redirectedMethods[key] = window.console[key];
241
242         for (let type of ["log", "error", "info", "warn"])
243             redirectedMethods[type] = createProxyConsoleHandler(type.toUpperCase());
244
245         redirectedMethods["trace"] = createProxyConsoleTraceHandler();
246
247         this._originalConsole = window.console;
248         window.console = redirectedMethods;
249     }
250
251     reportUnhandledRejection(error)
252     {
253         let message = error.message;
254         let stack = error.stack;
255         let result = `Unhandled promise rejection in inspector page: ${message}\n`;
256         if (stack) {
257             let sanitizedStack = this.sanitizeStack(stack);
258             result += `\nStack Trace: ${sanitizedStack}\n`;
259         }
260
261         // If the connection to the test page is not set up, then just dump to console and give up.
262         // Errors encountered this early can be debugged by loading Test.html in a normal browser page.
263         if (this._originalConsole && !this._testPageHasLoaded)
264             this._originalConsole["error"](result);
265
266         this.addResult(result);
267         this.completeTest();
268
269         // Stop default handler so we can empty InspectorBackend's message queue.
270         return true;
271     }
272
273     reportUncaughtExceptionFromEvent(message, url, lineNumber, columnNumber)
274     {
275         // An exception thrown from a timer callback does not report a URL.
276         if (url === "undefined")
277             url = "global";
278
279         return this.reportUncaughtException({message, url, lineNumber, columnNumber});
280     }
281
282     reportUncaughtException({message, url, lineNumber, columnNumber, stack, code})
283     {
284         let result;
285         let sanitizedURL = TestHarness.sanitizeURL(url);
286         let sanitizedStack = this.sanitizeStack(stack);
287         if (url || lineNumber || columnNumber)
288             result = `Uncaught exception in Inspector page: ${message} [${sanitizedURL}:${lineNumber}:${columnNumber}]\n`;
289         else
290             result = `Uncaught exception in Inspector page: ${message}\n`;
291
292         if (stack)
293             result += `\nStack Trace:\n${sanitizedStack}\n`;
294         if (code)
295             result += `\nEvaluated Code:\n${code}`;
296
297         // If the connection to the test page is not set up, then just dump to console and give up.
298         // Errors encountered this early can be debugged by loading Test.html in a normal browser page.
299         if (this._originalConsole && !this._testPageHasLoaded)
300             this._originalConsole["error"](result);
301
302         this.addResult(result);
303         this.completeTest();
304         // Stop default handler so we can empty InspectorBackend's message queue.
305         return true;
306     }
307
308     // Private
309
310     _resendResults()
311     {
312         console.assert(this._testPageHasLoaded);
313
314         if (this.dumpActivityToSystemConsole)
315             InspectorFrontendHost.unbufferedLog("_resendResults()");
316
317         this.evaluateInPage("TestPage.clearOutput()");
318         for (let result of this._results)
319             this.evaluateInPage(`TestPage.addResult(unescape("${escape(result)}"))`);
320     }
321 };
322
323 FrontendTestHarness.Event = {
324     TestPageDidLoad: "frontend-test-test-page-did-load"
325 };