chromium/chrome/test/data/webui/chromeos/test_api.js

// 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);