/* * Copyright (C) 2015-2017 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 WI.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."); } // If 'callback' is a function, it will be with the arguments // callback(error, result, wasThrown). Otherwise, a promise is // returned that resolves with 'result' or rejects with 'error'. // The options object accepts the following keys and values: // 'remoteObjectOnly': if true, do not unwrap the result payload to a // primitive value even if possible. Useful if testing WI.RemoteObject directly. evaluateInPage(string, callback, options={}) { 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); } newline() { this.log(""); } json(object, filter) { this.log(JSON.stringify(object, filter || null, 2)); } 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); } expectTrue(actual, message) { this._expect(TestHarness.ExpectationType.True, !!actual, message, actual); } expectFalse(actual, message) { this._expect(TestHarness.ExpectationType.False, !actual, message, actual); } expectEmpty(actual, message) { if (Array.isArray(actual) || typeof actual === "string") { this._expect(TestHarness.ExpectationType.Empty, !actual.length, message, actual); return; } if (actual instanceof Set || actual instanceof Map) { this._expect(TestHarness.ExpectationType.Empty, !actual.size, message, actual); return; } if (typeof actual === "object") { this._expect(TestHarness.ExpectationType.Empty, isEmptyObject(actual), message, actual); return; } this.fail("expectEmpty should not be called with a non-object:\n Actual: " + this._expectationValueAsString(actual)); } expectNotEmpty(actual, message) { if (Array.isArray(actual) || typeof actual === "string") { this._expect(TestHarness.ExpectationType.NotEmpty, !!actual.length, message, actual); return; } if (actual instanceof Set || actual instanceof Map) { this._expect(TestHarness.ExpectationType.NotEmpty, !!actual.size, message, actual); return; } if (typeof actual === "object") { this._expect(TestHarness.ExpectationType.NotEmpty, !isEmptyObject(actual), message, actual); return; } this.fail("expectNotEmpty should not be called with a non-object:\n Actual: " + this._expectationValueAsString(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); } passOrFail(condition, message) { if (condition) this.pass(message); else this.fail(message); } // Use this to expect an exception. To further examine the exception, // chain onto the result with .then() and add your own test assertions. // The returned promise is rejected if an exception was not thrown. expectException(work) { if (typeof work !== "function") throw new Error("Invalid argument to catchException: work must be a function."); let expectAndDumpError = (e, resolvedValue) => { this.expectNotNull(e, "Should produce an exception."); if (!e) { this.expectEqual(resolvedValue, undefined, "Exception-producing work should not return a value"); return; } if (e instanceof Error || !(e instanceof Object)) this.log(e.toString()); else { try { this.json(e); } catch { this.log(e.constructor.name); } } } let error = null; let result = null; try { result = work(); } catch (caughtError) { error = caughtError; } finally { // If 'work' returns a promise, it will settle (resolve or reject) by itself. // Invert the promise's settled state to match the expectation of the caller. if (result instanceof Promise) { return result.then((resolvedValue) => { expectAndDumpError(null, resolvedValue); return Promise.reject(resolvedValue); }, (e) => { // Don't chain the .catch as it will log the value we just rejected. expectAndDumpError(e); return Promise.resolve(e); }); } // If a promise is not produced, turn the exception into a resolved promise, and a // resolved value into a rejected value (since an exception was expected). expectAndDumpError(error); return error ? Promise.resolve(error) : Promise.reject(result); } } // 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 { } try { let valueString = String(value); if (valueString === defaultValueString && value.constructor && value.constructor.name !== "Object") return value.constructor.name + " instance " + instanceIdentifier(value); return valueString; } catch { return defaultValueString; } } _expectationMessageFormat(type) { switch (type) { case TestHarness.ExpectationType.True: return "expectThat(%s)"; case TestHarness.ExpectationType.False: return "expectFalse(%s)"; case TestHarness.ExpectationType.Empty: return "expectEmpty(%s)"; case TestHarness.ExpectationType.NotEmpty: return "expectNotEmpty(%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.Empty: return "empty"; case TestHarness.ExpectationType.NotEmpty: return "not empty"; 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"), Empty: Symbol("expect-empty"), NotEmpty: Symbol("expect-not-empty"), 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"), };