// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Library providing basic test framework functionality.
*/
/* eslint-disable no-console */
/**
* See assert.js for where this is used.
* @suppress {globalThis}
*/
this.traceAssertionsForTesting = true;
/** @suppress {globalThis} */
// eslint-disable-next-line no-var
var hasWindow = !!this.window;
/**
* Namespace for |Test|.
* @type {Object}
*/
// eslint-disable-next-line no-var
var testing = {};
(function(exports) {
/**
* Hold the currentTestCase across between preLoad and run.
* @type {TestCase}
*/
let currentTestCase = null;
/**
* Value set to true by WebUIBrowserTest if test harness should wait for user to
* attach a debugger.
*
* @type {boolean}
*/
let waitUser = false;
/**
* The string representation of the currently running test function.
* @type {?string}
*/
let currentTestFunction = null;
/**
* The arguments of the currently running test.
* @type {Array}
*/
let currentTestArguments = [];
/**
* This class will be exported as testing.Test, and is provided to hold the
* fixture's configuration and callback methods for the various phases of
* invoking a test. It is called "Test" rather than TestFixture to roughly
* mimic the gtest's class names.
* @constructor
*/
function Test() {}
/**
* Make all transitions and animations take 0ms. NOTE: this will completely
* disable webkitTransitionEnd events. If your code relies on them firing, it
* will break. animationend events should still work.
*/
Test.disableAnimationsAndTransitions = function() {
const all = document.body.querySelectorAll('*');
const ZERO_MS_IMPORTANT = '0ms !important';
for (let i = 0, l = all.length; i < l; ++i) {
const style = all[i].style;
style.animationDelay = ZERO_MS_IMPORTANT;
style.animationDuration = ZERO_MS_IMPORTANT;
style.transitionDelay = ZERO_MS_IMPORTANT;
style.transitionDuration = ZERO_MS_IMPORTANT;
}
const realElementAnimate = Element.prototype.animate;
Element.prototype.animate = function(keyframes, opt_options) {
if (typeof opt_options === 'object') {
opt_options.duration = 0;
} else {
opt_options = 0;
}
return realElementAnimate.call(this, keyframes, opt_options);
};
if (document.timeline && document.timeline.play) {
const realTimelinePlay = document.timeline.play;
document.timeline.play = function(a) {
a.timing.duration = 0;
return realTimelinePlay.call(document.timeline, a);
};
}
};
Test.prototype = {
/**
* The name of the test.
*/
name: null,
/**
* When set to a string value representing a url, generate BrowsePreload
* call, which will browse to the url and call fixture.preLoad of the
* currentTestCase.
* @type {?string}
*/
browsePreload: null,
/** @type {?string} */
webuiHost: null,
/**
* When set to a function, will be called in the context of the test
* generation inside the function, after AddLibrary calls and before
* generated C++.
* @type {?function(string,string)}
*/
testGenPreamble: null,
/**
* When set to a function, will be called in the context of the test
* generation inside the function, and after any generated C++.
* @type {?function(string,string)}
*/
testGenPostamble: null,
/** @type {?function()} */
testGenCppIncludes: null,
/**
* When set to a non-null string, auto-generate typedef before generating
* TEST*: {@code typedef typedefCppFixture testFixture}.
* @type {string}
*/
typedefCppFixture: 'WebUIBrowserTest',
/** @type {?Array<{switchName: string, switchValue: string}>} */
commandLineSwitches: null,
/** @type {?{enabled: !Array<string>, disabled: !Array<string>}} */
featureList: null,
/**
* @type {?Array<!{
* featureName: string,
* parameters: !Array<{name: string, value: string}>}>}
*/
featuresWithParameters: null,
/**
* Value is passed through call to C++ RunJavascriptF to invoke this test.
* @type {boolean}
*/
isAsync: false,
/**
* True when the test is expected to fail for testing the test framework.
* @type {boolean}
*/
testShouldFail: false,
/**
* Starts a local test server if true and injects the server's base url to
* each test. The url can be accessed from
* |testRunnerParams.testServerBaseUrl|.
* @type {boolean}
*/
testServer: false,
/**
* Extra libraries to add before loading this test file.
* @type {Array<string>}
*/
extraLibraries: [],
/**
* Extra libraries to add before loading this test file.
* This list is in the form of Closure library style object
* names. To support this, a closure deps.js file must
* be specified when generating the test C++ source.
* The specified libraries will be included with their transitive
* dependencies according to the deps file.
* @type {Array<string>}
*/
closureModuleDeps: [],
/**
* Override this method to perform initialization during preload (such as
* creating mocks and registering handlers).
* @type {Function}
*/
preLoad: function() {},
/**
* Override this method to perform tasks before running your test.
* @type {Function}
*/
setUp: function() {},
/**
* Override this method to perform tasks after running your test.
* @type {Function}
*/
tearDown: function() {
if (typeof document !== 'undefined') {
const noAnimationStyle = document.getElementById('no-animation');
if (noAnimationStyle) {
noAnimationStyle.parentNode.removeChild(noAnimationStyle);
}
}
},
/**
* Called to run the body from the perspective of this fixture.
* @type {Function}
*/
runTest: function(testBody) {
testBody.call(this);
},
/**
* Create a closure function for continuing the test at a later time. May be
* used as a listener function.
* @param {WhenTestDone} whenTestDone Call testDone() at the appropriate
* time.
* @param {!Function} completion The function to call to complete the test.
* @param {...*} var_args Arguments to pass when calling completionAction.
* @return {function(): void} Return a function, bound to this test fixture,
* which continues the test.
*/
continueTest: function(whenTestDone, completion, var_args) {
const savedArgs = new SaveMockArguments();
const completionAction = new CallFunctionAction(
this, savedArgs, completion, Array.prototype.slice.call(arguments, 2));
if (whenTestDone === WhenTestDone.DEFAULT) {
whenTestDone = WhenTestDone.ASSERT;
}
const runAll = new RunAllAction(true, whenTestDone, [completionAction]);
return function() {
savedArgs.arguments = Array.prototype.slice.call(arguments);
runAll.invoke();
};
},
/**
* Call this during setUp to defer the call to runTest() until later. The
* caller must call the returned function at some point to run the test.
* @param {WhenTestDone} whenTestDone Call testDone() at the appropriate
* time.
* @param {...*} var_args Arguments to pass when running the
* |currentTestCase|.
* @return {function(): void} A function which will run the current body of
* the currentTestCase.
*/
deferRunTest: function(whenTestDone, var_args) {
if (whenTestDone === WhenTestDone.DEFAULT) {
whenTestDone = WhenTestDone.ALWAYS;
}
return currentTestCase.deferRunTest.apply(
currentTestCase,
[whenTestDone].concat(Array.prototype.slice.call(arguments, 1)));
},
};
/**
* This class is not exported and is available to hold the state of the
* |currentTestCase| throughout preload and test run.
* @param {string} name The name of the test case.
* @param {Test} fixture The fixture object for this test case.
* @param {Function} body The code to run for the test.
* @constructor
*/
function TestCase(name, fixture, body) {
this.name = name;
this.fixture = fixture;
this.body = body;
}
TestCase.prototype = {
/**
* The name of this test.
* @type {?string}
*/
name: null,
/**
* The test fixture to set |this| to when running the test |body|.
* @type {Test}
*/
fixture: null,
/**
* The test body to execute in runTest().
* @type {Function}
*/
body: null,
/**
* True when the test fixture will run the test later.
* @type {boolean}
* @private
*/
deferred_: false,
/**
* Called at preload time, proxies to the fixture.
* @type {Function}
*/
preLoad: function(name) {
if (this.fixture) {
this.fixture.preLoad();
}
},
/**
* Called before a test runs.
*/
setUp: function() {
if (this.fixture) {
this.fixture.setUp();
}
},
/**
* Called before a test is torn down (by testDone()).
*/
tearDown: function() {
if (this.fixture) {
this.fixture.tearDown();
}
},
/**
* Called to run this test's body.
*/
runTest: function() {
if (this.body && this.fixture) {
this.fixture.runTest(this.body);
}
},
/**
* Runs this test case with |this| set to the |fixture|.
*
* Note: Tests created with TEST_F may depend upon |this| being set to an
* instance of this.fixture. The current implementation of TEST creates a
* dummy constructor, but tests created with TEST should not rely on |this|
* being set.
* @type {Function}
*/
run: function() {
try {
this.setUp();
} catch (e) {
// Mock4JSException doesn't inherit from Error, so fall back on
// toString().
console.error(e.stack || e.toString());
}
if (!this.deferred_) {
this.runTest();
}
// tearDown called by testDone().
},
/**
* Cause this TestCase to be deferred (don't call runTest()) until the
* returned function is called.
* @param {WhenTestDone} whenTestDone Call testDone() at the appropriate
* time.
* @param {...*} var_args Arguments to pass when running the
* |currentTestCase|.
* @return {function(): void} A function that will run this TestCase when
* called.
*/
deferRunTest: function(whenTestDone, var_args) {
this.deferred_ = true;
const savedArgs = new SaveMockArguments();
const completionAction = new CallFunctionAction(
this, savedArgs, this.runTest,
Array.prototype.slice.call(arguments, 1));
const runAll = new RunAllAction(true, whenTestDone, [completionAction]);
return function() {
savedArgs.arguments = Array.prototype.slice.call(arguments);
runAll.invoke();
};
},
};
/**
* true when testDone has been called.
* @type {boolean}
*/
let testIsDone = false;
/**
* Holds the errors, if any, caught by expects so that the test case can
* fail. Cleared when results are reported from runTest() or testDone().
* @type {Array<Error>}
*/
const errors = [];
/**
* URL to dummy WebUI page for testing framework.
* @type {string}
*/
const DUMMY_URL = 'chrome://DummyURL';
/**
* Resets test state by clearing |errors| and |testIsDone| flags.
*/
function resetTestState() {
errors.splice(0, errors.length);
testIsDone = false;
}
/**
* Notifies the running browser test of the test results. Clears |errors|.
* No tuple type: b/131114945 (result should be {[boolean, string]}).
* @param {Array=} result When passed, this is used for the testResult message.
*/
function testDone(result) {
if (!testIsDone) {
testIsDone = true;
if (currentTestCase) {
let ok = true;
ok = createExpect(currentTestCase.tearDown.bind(currentTestCase))
.call(null) &&
ok;
if (!ok && result) {
result = [false, errorsToMessage(errors, result[1])];
}
currentTestCase = null;
}
if (!result) {
result = testResult();
}
const [success, errorMessage] = /** @type {!Array} */ (result);
if (hasWindow && window.reportMojoWebUITestResult) {
// For "mojo_webui" test types, reportMojoWebUITestResult should already
// be defined globally, because such tests must manually import the
// mojo_webui_test_support.js module which defines it.
if (success) {
window.reportMojoWebUITestResult();
} else {
window.reportMojoWebUITestResult(errorMessage);
}
} else if (hasWindow && window.webUiTest) {
let testRunner;
if (webUiTest.mojom.TestRunnerRemote) {
// For mojo-lite WebUI tests.
testRunner = webUiTest.mojom.TestRunner.getRemote();
} else {
assertNotReached(
'Mojo bindings found, but no valid test interface loaded');
}
if (success) {
testRunner.testComplete(null);
} else {
testRunner.testComplete(errorMessage);
}
} else if (chrome.send) {
// For WebUI and v8 unit tests.
chrome.send('testResult', result);
} else if (chrome.test.sendScriptResult) {
// For extension tests.
const valueResult = {'result': success, message: errorMessage};
chrome.test.sendScriptResult(JSON.stringify(valueResult));
} else {
assertNotReached('No test framework available');
}
errors.splice(0, errors.length);
} else {
console.warn('testIsDone already');
}
}
/**
* Converts each Error in |errors| to a suitable message, adding them to
* |message|, and returns the message string.
* @param {Array<Error>} errors Array of errors to add to |message|.
* @param {string=} opt_message Message to append error messages to.
* @return {string} |opt_message| + messages of all |errors|.
*/
function errorsToMessage(errors, opt_message) {
let message = '';
if (opt_message) {
message += opt_message + '\n';
}
for (let i = 0; i < errors.length; ++i) {
const errorMessage = errors[i].stack || errors[i].message;
// Cast JSON.stringify to Function to avoid formal parameter mismatch.
message += 'Failed: ' + currentTestFunction + '(' +
currentTestArguments.map(/** @type{Function} */ (JSON.stringify)) +
')\n' + errorMessage;
}
return message;
}
/**
* Returns [success, message] & clears |errors|.
* @param {boolean=} errorsOk When true, errors are ok.
*
* No tuple type: b/131114945 (result should be {[boolean, string]}).
* @return {Array}
*/
function testResult(errorsOk) {
let result = [true, ''];
if (errors.length) {
result = [!!errorsOk, errorsToMessage(errors)];
}
return result;
}
// Asserts.
// Use the following assertions to verify a condition within a test.
/**
* @param {boolean} value The value to check.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertTrue(value, opt_message) {
chai.assert.isTrue(value, opt_message);
}
/**
* @param {boolean} value The value to check.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertFalse(value, opt_message) {
chai.assert.isFalse(value, opt_message);
}
/**
* @param {number} value1 The first operand.
* @param {number} value2 The second operand.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertGE(value1, value2, opt_message) {
chai.expect(value1).to.be.at.least(value2, opt_message);
}
/**
* @param {number} value1 The first operand.
* @param {number} value2 The second operand.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertGT(value1, value2, opt_message) {
chai.assert.isAbove(value1, value2, opt_message);
}
/**
* @param {*} expected The expected value.
* @param {*} actual The actual value.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertEquals(expected, actual, opt_message) {
chai.assert.strictEqual(actual, expected, opt_message);
}
/**
* @param {*} expected
* @param {*} actual
* {string=} opt_message
* @throws {Error}
*/
function assertDeepEquals(expected, actual, opt_message) {
chai.assert.deepEqual(actual, expected, opt_message);
}
/**
* @param {number} value1 The first operand.
* @param {number} value2 The second operand.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertLE(value1, value2, opt_message) {
chai.expect(value1).to.be.at.most(value2, opt_message);
}
/**
* @param {number} value1 The first operand.
* @param {number} value2 The second operand.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertLT(value1, value2, opt_message) {
chai.assert.isBelow(value1, value2, opt_message);
}
/**
* @param {*} expected The expected value.
* @param {*} actual The actual value.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertNotEquals(expected, actual, opt_message) {
chai.assert.notStrictEqual(actual, expected, opt_message);
}
/**
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertNotReached(opt_message) {
chai.assert.fail(null, null, opt_message);
}
/**
* @param {function()} testFunction
* @param {(Function|string|RegExp)=} opt_expected_or_constructor The expected
* Error constructor, partial or complete error message string, or RegExp to
* test the error message.
* @param {string=} opt_message Additional error message.
* @throws {Error}
*/
function assertThrows(testFunction, opt_expected_or_constructor, opt_message) {
// The implementation of assert.throws goes like:
// function (fn, errt, errs, msg) {
// if ('string' === typeof errt || errt instanceof RegExp) {
// errs = errt;
// errt = null;
// }
// ...
// That is, if the second argument is string or RegExp, the type of the
// exception is not checked: only the error message. This is achieved by
// partially "shifting" parameters (the "additional error message" is not
// shifted and will be lost). "Shifting" isn't a thing Closure understands, so
// just cast to string.
// TODO(crbug.com/40097498): Refactor this into something that makes sense when
// tests are actually compiled and we can do that safely.
chai.assert.throws(
testFunction,
/** @type{string} */ (opt_expected_or_constructor), opt_message);
}
/**
* Creates a function based upon a function that throws an exception on
* failure. The new function stuffs any errors into the |errors| array for
* checking by runTest. This allows tests to continue running other checks,
* while failing the overall test if any errors occurred.
* @param {Function} assertFunc The function which may throw an Error.
* @return {function(...*):boolean} A function that applies its arguments to
* |assertFunc| and returns true if |assertFunc| passes.
* @see errors
* @see runTestFunction
*/
function createExpect(assertFunc) {
return function() {
try {
assertFunc.apply(null, arguments);
} catch (e) {
errors.push(e);
return false;
}
return true;
};
}
/**
* This is the starting point for tests run by WebUIBrowserTest. If an error
* occurs, it reports a failure and a message created by joining individual
* error messages. This supports sync tests and async tests by calling
* testDone() when |isAsync| is not true, relying on async tests to call
* testDone() when they complete.
* @param {boolean} isAsync When false, call testDone() with the test result
* otherwise only when assertions are caught.
* @param {string} testFunction The function name to call.
* @param {Array} testArguments The arguments to call |testFunction| with.
* @return {boolean} true always to signal successful execution (but not
* necessarily successful results) of this test.
* @see errors
* @see runTestFunction
*/
function runTest(isAsync, testFunction, testArguments) {
// If waiting for user to attach a debugger, retry in 1 second.
if (waitUser) {
setTimeout(runTest, 1000, isAsync, testFunction, testArguments);
return true;
}
// Avoid eval() if at all possible, since it will not work on pages
// that have enabled content-security-policy.
/** @type {?Function} */
let testBody = this[testFunction]; // global object -- not a method.
let testName = testFunction;
// Depending on how we were called, |this| might not resolve to the global
// context.
if (testName === 'RUN_TEST_F' && testBody === undefined) {
testBody = RUN_TEST_F;
}
if (typeof testBody === 'undefined') {
testBody = /** @type{Function} */ (eval(testFunction));
testName = testBody.toString();
}
if (testBody !== RUN_TEST_F) {
console.log('Running test ' + testName);
}
// Async allow expect errors, but not assert errors.
const result =
runTestFunction(testFunction, testBody, testArguments, isAsync);
if (!isAsync || !result[0]) {
testDone(result);
}
return true;
}
/**
* This is the guts of WebUIBrowserTest. It runs the test surrounded by an
* expect to catch Errors. If |errors| is non-empty, it reports a failure and
* a message by joining |errors|. Consumers can use this to use assert/expect
* functions asynchronously, but are then responsible for reporting errors to
* the browser themselves through testDone().
* @param {string} testFunction The function name to report on failure.
* @param {Function} testBody The function to call.
* @param {Array} testArguments The arguments to call |testBody| with.
* @param {boolean} onlyAssertFails When true, only assertions cause failing
* testResult.
*
* No tuple type: b/131114945 (result should be {[boolean, string]}).
* @return {Array} [test-succeeded, message-if-failed]
* @see createExpect
* @see testResult
*/
function runTestFunction(
testFunction, testBody, testArguments, onlyAssertFails) {
currentTestFunction = testFunction;
currentTestArguments = testArguments;
const ok = createExpect(testBody).apply(null, testArguments);
return testResult(onlyAssertFails && ok);
}
/**
* Creates a new test case for the given |testFixture| and |testName|. Assumes
* |testFixture| describes a globally available subclass of type Test.
* @param {string} testFixture The fixture for this test case.
* @param {string} testName The name for this test case.
* @return {TestCase} A newly created TestCase.
*/
function createTestCase(testFixture, testName) {
const fixtureConstructor = this[testFixture];
assertTrue(
!!fixtureConstructor,
`The testFixture \'${testFixture}\' was not found.`);
const testBody = fixtureConstructor.testCaseBodies[testName];
assertTrue(
!!testBody, `Test \'${testName} was not found in \'${testFixture}\'.`);
const fixture = new fixtureConstructor();
fixture.name = testFixture;
return new TestCase(testName, fixture, testBody);
}
/**
* Used by WebUIBrowserTest to preload the javascript libraries at the
* appropriate time for javascript injection into the current page. This
* creates a test case and calls its preLoad for any early initialization such
* as registering handlers before the page's javascript runs it's OnLoad
* method. This is called before the page is loaded.
* @param {string} testFixture The test fixture name.
* @param {string} testName The test name.
*/
function preloadJavascriptLibraries(testFixture, testName) {
currentTestCase = createTestCase(testFixture, testName);
currentTestCase.preLoad();
}
/**
* Sets |waitUser| to true so |runTest| function waits for user to attach a
* debugger.
*/
function setWaitUser() {
waitUser = true;
exports.go = () => waitUser = false;
console.log('Waiting for debugger...');
console.log('Run: go() in the JS console when you are ready.');
}
/**
* During generation phase, this outputs; do nothing at runtime.
*/
function GEN() {}
/**
* During generation phase, this outputs; do nothing at runtime.
*/
function GEN_INCLUDE() {}
/**
* At runtime, register the testName with its fixture. Stuff the |name| into
* the |testFixture|'s prototype, if needed, and the |testCaseBodies| into its
* constructor.
* @param {string} testFixture The name of the test fixture class.
* @param {string} testName The name of the test function.
* @param {Function} testBody The body to execute when running this test.
* @param {string=} opt_preamble C++ code to be generated before the test. Does
* nothing here in the runtime phase.
*/
function TEST_F(testFixture, testName, testBody, opt_preamble) {
const fixtureConstructor = this[testFixture];
if (!fixtureConstructor.prototype.name) {
fixtureConstructor.prototype.name = testFixture;
}
if (!fixtureConstructor.hasOwnProperty('testCaseBodies')) {
fixtureConstructor.testCaseBodies = {};
}
fixtureConstructor.testCaseBodies[testName] = testBody;
}
/**
* Similar to TEST_F above but with a mandatory |preamble|.
* @param {string} preamble C++ code to be generated before the test. Does
* nothing here in the runtime phase.
* @param {string} testFixture The name of the test fixture class.
* @param {string} testName The name of the test function.
* @param {Function} testBody The body to execute when running this test.
*/
function TEST_F_WITH_PREAMBLE(preamble, testFixture, testName, testBody) {
TEST_F(testFixture, testName, testBody);
}
/**
* RunJavascriptTestF uses this as the |testFunction| when invoking
* runTest. If |currentTestCase| is non-null at this point, verify that
* |testFixture| and |testName| agree with the preloaded values. Create
* |currentTestCase|, if needed, run it, and clear the |currentTestCase|.
* @param {string} testFixture The name of the test fixture class.
* @param {string} testName The name of the test function.
* @see preloadJavascriptLibraries
* @see runTest
*/
function RUN_TEST_F(testFixture, testName) {
if (!currentTestCase) {
currentTestCase = createTestCase(testFixture, testName);
}
assertEquals(currentTestCase.name, testName);
assertEquals(currentTestCase.fixture.name, testFixture);
console.log('Running TestCase ' + testFixture + '.' + testName);
currentTestCase.run();
}
/**
* This Mock4JS matcher object pushes each |actualArgument| parameter to
* match() calls onto |args|.
* @param {Array} args The array to push |actualArgument| onto.
* @param {Object} realMatcher The real matcher check arguments with.
* @constructor
*/
function SaveMockArgumentMatcher(args, realMatcher) {
this.arguments_ = args;
this.realMatcher_ = realMatcher;
}
SaveMockArgumentMatcher.prototype = {
/**
* Holds the arguments to push each |actualArgument| onto.
* @type {Array}
* @private
*/
arguments_: null,
/**
* The real Mock4JS matcher object to check arguments with.
* @type {Object}
*/
realMatcher_: null,
/**
* Pushes |actualArgument| onto |arguments_| and call |realMatcher_|. Clears
* |arguments_| on non-match.
* @param {*} actualArgument The argument to match and save.
* @return {boolean} Result of calling the |realMatcher|.
*/
argumentMatches: function(actualArgument) {
this.arguments_.push(actualArgument);
const match = this.realMatcher_.argumentMatches(actualArgument);
if (!match) {
this.arguments_.splice(0, this.arguments_.length);
}
return match;
},
/**
* Proxy to |realMatcher_| for description.
* @return {string} Description of this Mock4JS matcher.
*/
describe: function() {
return this.realMatcher_.describe();
},
};
/**
* Actions invoked by Mock4JS's "will()" syntax do not receive arguments from
* the mocked method. This class works with SaveMockArgumentMatcher to save
* arguments so that the invoked Action can pass arguments through to the
* invoked function.
* @constructor
*/
function SaveMockArguments() {
this.arguments = [];
}
SaveMockArguments.prototype = {
/**
* Wraps the |realMatcher| with an object which will push its argument onto
* |arguments| and call realMatcher.
* @param {Object} realMatcher A Mock4JS matcher object for this argument.
* @return {SaveMockArgumentMatcher} A new matcher which will push its
* argument onto |arguments|.
*/
match: function(realMatcher) {
return new SaveMockArgumentMatcher(this.arguments, realMatcher);
},
/**
* Remember the argument passed to this stub invocation.
* @type {Array}
*/
arguments: null,
};
/**
* CallFunctionAction is provided to allow mocks to have side effects.
* @param {Object} obj The object to set |this| to when calling |func_|.
* @param {?SaveMockArguments} savedArgs when non-null, saved arguments are
* passed to |func|.
* @param {!Function} func The function to call.
* @param {Array=} args Any arguments to pass to func.
* @constructor
*/
function CallFunctionAction(obj, savedArgs, func, args) {
/**
* Set |this| to |obj_| when calling |func_|.
* @type {?Object}
*/
this.obj_ = obj;
/**
* The SaveMockArguments to hold arguments when invoking |func_|.
* @type {?SaveMockArguments}
* @private
*/
this.savedArgs_ = savedArgs;
/**
* The function to call when invoked.
* @type {!Function}
* @private
*/
this.func_ = func;
/**
* Arguments to pass to |func_| when invoked.
* @type {!Array}
*/
this.args_ = args || [];
}
CallFunctionAction.prototype = {
/**
* Accessor for |func_|.
* @return {Function} The function to invoke.
*/
get func() {
return this.func_;
},
/**
* Called by Mock4JS when using .will() to specify actions for stubs() or
* expects(). Clears |savedArgs_| so it can be reused.
* @return The results of calling |func_| with the concatenation of
* |savedArgs_| and |args_|.
*/
invoke: function() {
let prependArgs = [];
if (this.savedArgs_) {
prependArgs =
this.savedArgs_.arguments.splice(0, this.savedArgs_.arguments.length);
}
return this.func.apply(this.obj_, prependArgs.concat(this.args_));
},
/**
* Describe this action to Mock4JS.
* @return {string} A description of this action.
*/
describe: function() {
return 'calls the given function with saved arguments and ' + this.args_;
},
};
/**
* Syntactic sugar for use with will() on a Mock4JS.Mock.
* @param {SaveMockArguments} savedArgs Arguments saved with this object
* are passed to |func|.
* @param {!Function} func The function to call when the method is invoked.
* @param {...*} var_args Arguments to pass when calling func.
* @return {CallFunctionAction} Action for use in will.
*/
function callFunctionWithSavedArgs(savedArgs, func, var_args) {
return new CallFunctionAction(
null, savedArgs, func, Array.prototype.slice.call(arguments, 2));
}
/**
* When to call testDone().
* @enum {number}
*/
const WhenTestDone = {
/**
* Default for the method called.
*/
DEFAULT: -1,
/**
* Never call testDone().
*/
NEVER: 0,
/**
* Call testDone() on assert failure.
*/
ASSERT: 1,
/**
* Call testDone() if there are any assert or expect failures.
*/
EXPECT: 2,
/**
* Always call testDone().
*/
ALWAYS: 3,
};
/**
* Runs all |actions|.
* @param {boolean} isAsync When true, call testDone() on Errors.
* @param {WhenTestDone} whenTestDone Call testDone() at the appropriate
* time.
* @param {Array<Object>} actions Actions to run.
* @constructor
*/
function RunAllAction(isAsync, whenTestDone, actions) {
this.isAsync_ = isAsync;
this.whenTestDone_ = whenTestDone;
this.actions_ = actions;
}
RunAllAction.prototype = {
/**
* When true, call testDone() on Errors.
* @type {boolean}
* @private
*/
isAsync_: false,
/**
* Call testDone() at appropriate time.
* @type {WhenTestDone}
* @private
* @see WhenTestDone
*/
whenTestDone_: WhenTestDone.ASSERT,
/**
* Holds the actions to execute when invoked.
* @type {Array}
* @private
*/
actions_: null,
/**
* Runs all |actions_|, returning the last one. When running in sync mode,
* throws any exceptions to be caught by runTest() or
* runTestFunction(). Call testDone() according to |whenTestDone_| setting.
*/
invoke: function() {
try {
let result;
for (let i = 0; i < this.actions_.length; ++i) {
result = this.actions_[i].invoke();
}
if ((this.whenTestDone_ === WhenTestDone.EXPECT && errors.length) ||
this.whenTestDone_ === WhenTestDone.ALWAYS) {
testDone();
}
return result;
} catch (e) {
if (!(e instanceof Error)) {
e = new Error(e.toString());
}
if (!this.isAsync_) {
throw e;
}
errors.push(e);
if (this.whenTestDone_ !== WhenTestDone.NEVER) {
testDone();
}
}
},
/**
* Describe this action to Mock4JS.
* @return {string} A description of this action.
*/
describe: function() {
return 'Calls all actions: ' + this.actions_;
},
};
/**
* Syntactic sugar for use with will() on a Mock4JS.Mock.
* @param {...*} var_args Actions to run.
* @return {RunAllAction} Action for use in will.
*/
function runAllActions(var_args) {
return new RunAllAction(
false, WhenTestDone.NEVER, Array.prototype.slice.call(arguments));
}
/**
* Syntactic sugar for use with will() on a Mock4JS.Mock.
* @param {WhenTestDone} whenTestDone Call testDone() at the appropriate
* time.
* @param {...*} var_args Actions to run.
* @return {RunAllAction} Action for use in will.
*/
function runAllActionsAsync(whenTestDone, var_args) {
return new RunAllAction(
true, whenTestDone, Array.prototype.slice.call(arguments, 1));
}
/**
* Runs a test isolated from the other test-runner machinery in this file which
* is mostly for the deprecated js2gtest suites. Designed for running with the
* newer, better `EvalJs` machinery (rather than chrome.send).
*
* @param {string} suite Name of the test suite object on `window`.
* @param {string} name Test method on the `suite`.
* @param {string} helper A method on `suite` that takes `name` as a string.
* @return {string}
*/
async function isolatedTestRunner(suite, name, helper) {
console.log(`Running ${suite}.${name} with isolatedTestRunner(${helper}).`);
const testSuite = window[suite];
try {
if (helper) {
await testSuite[helper](name);
} else {
await testSuite[name]();
}
console.log(`${suite}.${name} ran to completion.`);
return 'test_completed';
} catch (/* @type {Error} */ error) {
let message = 'exception';
if (typeof error === 'object' && error !== null && error['message']) {
message = error['message'];
console.log(error['stack']);
} else {
console.log(error);
}
console.log(`${suite}.${name} threw: ${message}`, error);
throw error;
}
}
/**
* Exports assertion methods. All assertion methods delegate to the chai.js
* assertion library.
*/
function exportChaiAsserts() {
exports.assertTrue = assertTrue;
exports.assertFalse = assertFalse;
exports.assertGE = assertGE;
exports.assertGT = assertGT;
exports.assertEquals = assertEquals;
exports.assertDeepEquals = assertDeepEquals;
exports.assertLE = assertLE;
exports.assertLT = assertLT;
exports.assertNotEquals = assertNotEquals;
exports.assertNotReached = assertNotReached;
exports.assertThrows = assertThrows;
}
/**
* Exports methods related to Mock4JS mocking.
*/
function exportMock4JsHelpers() {
exports.callFunctionWithSavedArgs = callFunctionWithSavedArgs;
exports.SaveMockArguments = SaveMockArguments;
}
// Exports.
testing.Test = Test;
exports.testDone = testDone;
exportChaiAsserts();
exportMock4JsHelpers();
exports.preloadJavascriptLibraries = preloadJavascriptLibraries;
exports.setWaitUser = setWaitUser;
exports.resetTestState = resetTestState;
exports.runAllActions = runAllActions;
exports.runAllActionsAsync = runAllActionsAsync;
exports.runTest = runTest;
exports.runTestFunction = runTestFunction;
exports.DUMMY_URL = DUMMY_URL;
exports.TEST_F = TEST_F;
exports.TEST_F_WITH_PREAMBLE = TEST_F_WITH_PREAMBLE;
exports.RUNTIME_TEST_F = TEST_F;
exports.GEN = GEN;
exports.GEN_INCLUDE = GEN_INCLUDE;
exports.WhenTestDone = WhenTestDone;
exports.isolatedTestRunner = isolatedTestRunner;
})(this);