Web Inspector: add InspectorTest.expectException() and use it
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Test / TestHarness.js
1 /*
2  * Copyright (C) 2015, 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 TestHarness = class TestHarness extends WI.Object
27 {
28     constructor()
29     {
30         super();
31
32         this._logCount = 0;
33         this._failureObjects = new Map;
34         this._failureObjectIdentifier = 1;
35
36         // Options that are set per-test for debugging purposes.
37         this.forceDebugLogging = false;
38
39         // Options that are set per-test to ensure deterministic output.
40         this.suppressStackTraces = false;
41     }
42
43     completeTest()
44     {
45         throw new Error("Must be implemented by subclasses.");
46     }
47
48     addResult()
49     {
50         throw new Error("Must be implemented by subclasses.");
51     }
52
53     debugLog()
54     {
55         throw new Error("Must be implemented by subclasses.");
56     }
57
58     evaluateInPage(string, callback)
59     {
60         throw new Error("Must be implemented by subclasses.");
61     }
62
63     debug()
64     {
65         throw new Error("Must be implemented by subclasses.");
66     }
67
68     createAsyncSuite(name)
69     {
70         return new AsyncTestSuite(this, name);
71     }
72
73     createSyncSuite(name)
74     {
75         return new SyncTestSuite(this, name);
76     }
77
78     get logCount()
79     {
80         return this._logCount;
81     }
82
83     log(message)
84     {
85         ++this._logCount;
86
87         if (this.forceDebugLogging)
88             this.debugLog(message);
89         else
90             this.addResult(message);
91     }
92
93     json(object, filter)
94     {
95         this.log(JSON.stringify(object, filter || null, 2));
96     }
97
98     assert(condition, message)
99     {
100         if (condition)
101             return;
102
103         let stringifiedMessage = TestHarness.messageAsString(message);
104         this.log("ASSERT: " + stringifiedMessage);
105     }
106
107     expectThat(actual, message)
108     {
109         this._expect(TestHarness.ExpectationType.True, !!actual, message, actual);
110     }
111
112     expectFalse(actual, message)
113     {
114         this._expect(TestHarness.ExpectationType.False, !actual, message, actual);
115     }
116
117     expectNull(actual, message)
118     {
119         this._expect(TestHarness.ExpectationType.Null, actual === null, message, actual, null);
120     }
121
122     expectNotNull(actual, message)
123     {
124         this._expect(TestHarness.ExpectationType.NotNull, actual !== null, message, actual);
125     }
126
127     expectEqual(actual, expected, message)
128     {
129         this._expect(TestHarness.ExpectationType.Equal, expected === actual, message, actual, expected);
130     }
131
132     expectNotEqual(actual, expected, message)
133     {
134         this._expect(TestHarness.ExpectationType.NotEqual, expected !== actual, message, actual, expected);
135     }
136
137     expectShallowEqual(actual, expected, message)
138     {
139         this._expect(TestHarness.ExpectationType.ShallowEqual, Object.shallowEqual(actual, expected), message, actual, expected);
140     }
141
142     expectNotShallowEqual(actual, expected, message)
143     {
144         this._expect(TestHarness.ExpectationType.NotShallowEqual, !Object.shallowEqual(actual, expected), message, actual, expected);
145     }
146
147     expectEqualWithAccuracy(actual, expected, accuracy, message)
148     {
149         console.assert(typeof expected === "number");
150         console.assert(typeof actual === "number");
151
152         this._expect(TestHarness.ExpectationType.EqualWithAccuracy, Math.abs(expected - actual) <= accuracy, message, actual, expected, accuracy);
153     }
154
155     expectLessThan(actual, expected, message)
156     {
157         this._expect(TestHarness.ExpectationType.LessThan, actual < expected, message, actual, expected);
158     }
159
160     expectLessThanOrEqual(actual, expected, message)
161     {
162         this._expect(TestHarness.ExpectationType.LessThanOrEqual, actual <= expected, message, actual, expected);
163     }
164
165     expectGreaterThan(actual, expected, message)
166     {
167         this._expect(TestHarness.ExpectationType.GreaterThan, actual > expected, message, actual, expected);
168     }
169
170     expectGreaterThanOrEqual(actual, expected, message)
171     {
172         this._expect(TestHarness.ExpectationType.GreaterThanOrEqual, actual >= expected, message, actual, expected);
173     }
174
175     pass(message)
176     {
177         let stringifiedMessage = TestHarness.messageAsString(message);
178         this.log("PASS: " + stringifiedMessage);
179     }
180
181     fail(message)
182     {
183         let stringifiedMessage = TestHarness.messageAsString(message);
184         this.log("FAIL: " + stringifiedMessage);
185     }
186
187     // Use this to expect an exception. To further examine the exception,
188     // chain onto the result with .then() and add your own test assertions.
189     // The returned promise is rejected if an exception was not thrown.
190     expectException(work)
191     {
192         if (typeof work !== "function")
193             throw new Error("Invalid argument to catchException: work must be a function.");
194
195         let expectAndDumpError = (e) => {
196             this.expectNotNull(e, "Should produce an exception.");
197             if (e)
198                 this.log(e.toString());
199         }
200
201         let error = null;
202         let result = null;
203         try {
204             result = work();
205         } catch (caughtError) {
206             error = caughtError;
207         } finally {
208             // If 'work' returns a promise, it will settle (resolve or reject) by itself.
209             // Invert the promise's settled state to match the expectation of the caller.
210             if (result instanceof Promise) {
211                 return result.then((resolvedValue) => {
212                     expectAndDumpError(null);
213                     return Promise.reject(resolvedValue);
214                 }).catch((e) => {
215                     expectAndDumpError(e);
216                     return Promise.resolve(e);
217                 });
218             }
219
220             // If a promise is not produced, turn the exception into a resolved promise, and a
221             // resolved value into a rejected value (since an exception was expected).
222             expectAndDumpError(error);
223             return error ? Promise.resolve(error) : Promise.reject(result);
224         }
225     }
226
227     // Protected
228
229     static messageAsString(message)
230     {
231         if (message instanceof Element)
232             return message.textContent;
233
234         return typeof message !== "string" ? JSON.stringify(message) : message;
235     }
236
237     static sanitizeURL(url)
238     {
239         if (!url)
240             return "(unknown)";
241
242         let lastPathSeparator = Math.max(url.lastIndexOf("/"), url.lastIndexOf("\\"));
243         let location = lastPathSeparator > 0 ? url.substr(lastPathSeparator + 1) : url;
244         if (!location.length)
245             location = "(unknown)";
246
247         // Clean up the location so it is bracketed or in parenthesis.
248         if (url.indexOf("[native code]") !== -1)
249             location = "[native code]";
250
251         return location;
252     }
253
254     static sanitizeStackFrame(frame, i)
255     {
256         // Most frames are of the form "functionName@file:///foo/bar/File.js:345".
257         // But, some frames do not have a functionName. Get rid of the file path.
258         let nameAndURLSeparator = frame.indexOf("@");
259         let frameName = nameAndURLSeparator > 0 ? frame.substr(0, nameAndURLSeparator) : "(anonymous)";
260
261         let lastPathSeparator = Math.max(frame.lastIndexOf("/"), frame.lastIndexOf("\\"));
262         let frameLocation = lastPathSeparator > 0 ? frame.substr(lastPathSeparator + 1) : frame;
263         if (!frameLocation.length)
264             frameLocation = "unknown";
265
266         // Clean up the location so it is bracketed or in parenthesis.
267         if (frame.indexOf("[native code]") !== -1)
268             frameLocation = "[native code]";
269         else
270             frameLocation = "(" + frameLocation + ")";
271
272         return `#${i}: ${frameName} ${frameLocation}`;
273     }
274
275     sanitizeStack(stack)
276     {
277         if (this.suppressStackTraces)
278             return "(suppressed)";
279
280         if (!stack || typeof stack !== "string")
281             return "(unknown)";
282
283         return stack.split("\n").map(TestHarness.sanitizeStackFrame).join("\n");
284     }
285
286     // Private
287
288     _expect(type, condition, message, ...values)
289     {
290         console.assert(values.length > 0, "Should have an 'actual' value.");
291
292         if (!message || !condition) {
293             values = values.map(this._expectationValueAsString.bind(this));
294             message = message || this._expectationMessageFormat(type).format(...values);
295         }
296
297         if (condition) {
298             this.pass(message);
299             return;
300         }
301
302         message += "\n    Expected: " + this._expectedValueFormat(type).format(...values.slice(1));
303         message += "\n    Actual: " + values[0];
304
305         this.fail(message);
306     }
307
308     _expectationValueAsString(value)
309     {
310         let instanceIdentifier = (object) => {
311             let id = this._failureObjects.get(object);
312             if (!id) {
313                 id = this._failureObjectIdentifier++;
314                 this._failureObjects.set(object, id);
315             }
316             return "#" + id;
317         };
318
319         const maximumValueStringLength = 200;
320         const defaultValueString = String(new Object); // [object Object]
321
322         // Special case for numbers, since JSON.stringify converts Infinity and NaN to null.
323         if (typeof value === "number")
324             return value;
325
326         try {
327             let valueString = JSON.stringify(value);
328             if (valueString.length <= maximumValueStringLength)
329                 return valueString;
330         } catch { }
331
332         try {
333             let valueString = String(value);
334             if (valueString === defaultValueString && value.constructor && value.constructor.name !== "Object")
335                 return value.constructor.name + " instance " + instanceIdentifier(value);
336             return valueString;
337         } catch {
338             return defaultValueString;
339         }
340     }
341
342     _expectationMessageFormat(type)
343     {
344         switch (type) {
345         case TestHarness.ExpectationType.True:
346             return "expectThat(%s)";
347         case TestHarness.ExpectationType.False:
348             return "expectFalse(%s)";
349         case TestHarness.ExpectationType.Null:
350             return "expectNull(%s)";
351         case TestHarness.ExpectationType.NotNull:
352             return "expectNotNull(%s)";
353         case TestHarness.ExpectationType.Equal:
354             return "expectEqual(%s, %s)";
355         case TestHarness.ExpectationType.NotEqual:
356             return "expectNotEqual(%s, %s)";
357         case TestHarness.ExpectationType.ShallowEqual:
358             return "expectShallowEqual(%s, %s)";
359         case TestHarness.ExpectationType.NotShallowEqual:
360             return "expectNotShallowEqual(%s, %s)";
361         case TestHarness.ExpectationType.EqualWithAccuracy:
362             return "expectEqualWithAccuracy(%s, %s, %s)";
363         case TestHarness.ExpectationType.LessThan:
364             return "expectLessThan(%s, %s)";
365         case TestHarness.ExpectationType.LessThanOrEqual:
366             return "expectLessThanOrEqual(%s, %s)";
367         case TestHarness.ExpectationType.GreaterThan:
368             return "expectGreaterThan(%s, %s)";
369         case TestHarness.ExpectationType.GreaterThanOrEqual:
370             return "expectGreaterThanOrEqual(%s, %s)";
371         default:
372             console.error("Unknown TestHarness.ExpectationType type: " + type);
373             return null;
374         }
375     }
376
377     _expectedValueFormat(type)
378     {
379         switch (type) {
380         case TestHarness.ExpectationType.True:
381             return "truthy";
382         case TestHarness.ExpectationType.False:
383             return "falsey";
384         case TestHarness.ExpectationType.NotNull:
385             return "not null";
386         case TestHarness.ExpectationType.NotEqual:
387         case TestHarness.ExpectationType.NotShallowEqual:
388             return "not %s";
389         case TestHarness.ExpectationType.EqualWithAccuracy:
390             return "%s +/- %s";
391         case TestHarness.ExpectationType.LessThan:
392             return "less than %s";
393         case TestHarness.ExpectationType.LessThanOrEqual:
394             return "less than or equal to %s";
395         case TestHarness.ExpectationType.GreaterThan:
396             return "greater than %s";
397         case TestHarness.ExpectationType.GreaterThanOrEqual:
398             return "greater than or equal to %s";
399         default:
400             return "%s";
401         }
402     }
403 };
404
405 TestHarness.ExpectationType = {
406     True: Symbol("expect-true"),
407     False: Symbol("expect-false"),
408     Null: Symbol("expect-null"),
409     NotNull: Symbol("expect-not-null"),
410     Equal: Symbol("expect-equal"),
411     NotEqual: Symbol("expect-not-equal"),
412     ShallowEqual: Symbol("expect-shallow-equal"),
413     NotShallowEqual: Symbol("expect-not-shallow-equal"),
414     EqualWithAccuracy: Symbol("expect-equal-with-accuracy"),
415     LessThan: Symbol("expect-less-than"),
416     LessThanOrEqual: Symbol("expect-less-than-or-equal"),
417     GreaterThan: Symbol("expect-greater-than"),
418     GreaterThanOrEqual: Symbol("expect-greater-than-or-equal"),
419 };