// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Tests that an observation matches the expected value.
* @param {*} expected The expected value.
* @param {*} observed The actual value.
* @param {string=} opt_message Optional message to include with a test
* failure.
*/
function assertEquals(expected, observed, opt_message) {
if (observed !== expected) {
let message = 'Assertion Failed\n Observed: ' + observed +
'\n Expected: ' + expected;
if (opt_message) {
message = message + '\n ' + opt_message;
}
throw new Error(message);
}
}
/**
* Verifies that a test result is true.
* @param {boolean} observed The observed value.
* @param {string=} opt_message Optional message to include with a test
* failure.
*/
function assertTrue(observed, opt_message) {
assertEquals(true, observed, opt_message);
}
/**
* Verifies that a test result is false.
* @param {boolean} observed The observed value.
* @param {string=} opt_message Optional message to include with a test
* failure.
*/
function assertFalse(observed, opt_message) {
assertEquals(false, observed, opt_message);
}
/**
* Verifies that the observed and reference values differ.
* @param {*} reference The target value for comparison.
* @param {*} observed The test result.
* @param {string=} opt_message Optional message to include with a test
* failure.
*/
function assertNotEqual(reference, observed, opt_message) {
if (observed === reference) {
let message = 'Assertion Failed\n Observed: ' + observed +
'\n Reference: ' + reference;
if (opt_message) {
message = message + '\n ' + opt_message;
}
throw new Error(message);
}
}
/**
* Verifies that a test evaluation results in an exception.
* @param {!Function} f The test function.
*/
function assertThrows(f) {
let triggeredError = false;
try {
f();
} catch (err) {
triggeredError = true;
}
if (!triggeredError) {
throw new Error('Assertion Failed: throw expected.');
}
}
/**
* Verifies that the contents of the expected and observed arrays match.
* @param {!Array} expected The expected result.
* @param {!Array} observed The actual result.
*/
function assertArrayEquals(expected, observed) {
const v1 = Array.prototype.slice.call(expected);
const v2 = Array.prototype.slice.call(observed);
let equal = v1.length === v2.length;
if (equal) {
for (let i = 0; i < v1.length; i++) {
if (v1[i] !== v2[i]) {
equal = false;
break;
}
}
}
if (!equal) {
const message =
['Assertion Failed', 'Observed: ' + v2, 'Expected: ' + v1].join('\n ');
throw new Error(message);
}
}
/**
* Verifies that the expected and observed result have the same content.
* @param {*} expected The expected result.
* @param {*} observed The actual result.
*/
function assertDeepEquals(expected, observed, opt_message) {
if (typeof expected === 'object' && expected !== null) {
assertNotEqual(null, observed);
for (const key in expected) {
assertTrue(key in observed, opt_message);
assertDeepEquals(expected[key], observed[key], opt_message);
}
for (const key in observed) {
assertTrue(key in expected, opt_message);
}
} else {
assertEquals(expected, observed, opt_message);
}
}
/**
* Decorates |window| with runTests() and endTests().
*
* @param {{
* runTests: (function(Object=):void|undefined),
* endTests: (function(boolean):void|undefined)
* }} exports
*/
(function(exports) {
/**
* Optional setup and teardown hooks that can be defined in a test scope.
* |setUpPage| is invoked once. |setUp|/|tearDown| are invoked before/after each
* test*() declared in the test scope.
*
* @typedef {{
* setUpPage: (function(): void|undefined),
* setUp: (function(): void|undefined),
* tearDown: (function(): void|undefined),
* }}
*/
let WebUiTestHarness;
/**
* Scope containing testXXX functions.
* @type {!Object}
*/
let testScope = {};
/**
* Test harness entrypoints on |testScope|.
* @type {!WebUiTestHarness}
*/
let testHarness = {};
/**
* List of test cases.
* @type {Array<string>} List of function names for tests to run.
*/
const testCases = [];
/**
* Indicates if all tests have run successfully.
* @type {boolean}
*/
let cleanTestRun = true;
/**
* Armed during setup of a test to call the matching tear down code.
* @type {Function}
*/
let pendingTearDown = null;
/**
* Name of current test.
* @type {?string}
*/
let testName = null;
/**
* Time current test started.
* @type {number}
*/
let testStartTime = 0;
/**
* Time first test started.
* @type {number}
*/
let runnerStartTime = 0;
/**
* Runs all functions starting with test and reports success or
* failure of the test suite.
* @param {Object=} opt_testScope optional scope containing testXXX functions.
* Uses global 'window' by default.
*/
function runTests(opt_testScope) {
runnerStartTime = performance.now();
testScope = opt_testScope || window;
testHarness = /** @type{!WebUiTestHarness} */ (testScope);
for (const name in testScope) {
// To avoid unnecessary getting properties, test name first.
if (/^test/.test(name) && typeof testScope[name] === 'function') {
testCases.push(name);
}
}
if (!testCases.length) {
console.error('Failed to find test cases.');
cleanTestRun = false;
}
try {
if (testHarness.setUpPage) {
testHarness.setUpPage();
}
} catch (err) {
cleanTestRun = false;
}
startTesting();
}
/**
* @suppress {missingProperties}
*/
function startTesting() {
if (window.waitUser) {
setTimeout(startTesting, 1000);
return;
}
continueTesting();
}
/**
* Runs the next test in the queue. Reports the test results if the queue is
* empty.
* @param {boolean=} opt_asyncTestFailure Optional parameter indicated if the
* last asynchronous test failed.
*/
async function continueTesting(opt_asyncTestFailure) {
const now = performance.now();
if (testName) {
console.info(
'TEST ' + testName +
' complete, status=' + (opt_asyncTestFailure ? 'FAIL' : 'PASS') +
', duration=' + Math.round(now - testStartTime) + 'ms');
}
if (opt_asyncTestFailure) {
cleanTestRun = false;
}
let done = false;
if (pendingTearDown) {
pendingTearDown();
pendingTearDown = null;
}
if (testCases.length > 0) {
testStartTime = now;
testName = testCases.pop();
console.info('TEST ' + testName + ' starting...');
const isAsyncTest = testScope[testName].length;
let testError = false;
try {
if (testHarness.setUp) {
testHarness.setUp();
}
pendingTearDown = testHarness.tearDown || null;
await testScope[testName](continueTesting);
} catch (err) {
console.error('Failure in test ' + testName + '\n' + err);
console.info(err.stack);
cleanTestRun = false;
testError = true;
}
// Asynchronous tests must manually call continueTesting when complete
// unless they throw an exception.
if (!isAsyncTest || testError) {
continueTesting();
}
} else {
done = true;
endTests(cleanTestRun);
}
if (!done) {
window.domAutomationController.send('PENDING');
}
}
/**
* Signals completion of a test.
* @param {boolean} success Indicates if the test completed successfully.
*/
function endTests(success) {
const duration =
runnerStartTime === 0 ? 0 : performance.now() - runnerStartTime;
console.info(
'TEST all complete, status=' + (success ? 'PASS' : 'FAIL') +
', duration=' + Math.round(duration) + 'ms');
testName = null;
runnerStartTime = 0;
window.domAutomationController.send(success ? 'SUCCESS' : 'FAILURE');
}
exports.runTests = runTests;
exports.endTests = endTests;
})(window);
/**
* @type {!function(Object=):void}
*/
window.runTests;
/**
* @type {!function(boolean):void}
*/
window.endTests;
window.onerror = function() {
window.endTests(false);
};