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