[common][webaudio][DPTTIZEN-3185, fix 1 tc issue] 87/228287/1
authorzhongyuan <zy123.yuan@samsung.com>
Fri, 20 Mar 2020 05:30:33 +0000 (13:30 +0800)
committerzhongyuan <zy123.yuan@samsung.com>
Fri, 20 Mar 2020 05:30:33 +0000 (13:30 +0800)
Change-Id: I31b2afd312c1c3c87bc52b5e5592ab98da166419

common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/audit-util.js [new file with mode: 0644]
common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/audit.js [new file with mode: 0644]
common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/testharness.js [new file with mode: 0644]
common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/the-gainnode-interface/gain-node-test.html

diff --git a/common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/audit-util.js b/common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/audit-util.js
new file mode 100644 (file)
index 0000000..4213fda
--- /dev/null
@@ -0,0 +1,195 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+/**
+ * @fileOverview  This file includes legacy utility functions for the layout
+ *                test.
+ */
+
+// How many frames in a WebAudio render quantum.
+let RENDER_QUANTUM_FRAMES = 128;
+
+// Compare two arrays (commonly extracted from buffer.getChannelData()) with
+// constraints:
+//   options.thresholdSNR: Minimum allowed SNR between the actual and expected
+//     signal. The default value is 10000.
+//   options.thresholdDiffULP: Maximum allowed difference between the actual
+//     and expected signal in ULP(Unit in the last place). The default is 0.
+//   options.thresholdDiffCount: Maximum allowed number of sample differences
+//     which exceeds the threshold. The default is 0.
+//   options.bitDepth: The expected result is assumed to come from an audio
+//     file with this number of bits of precision. The default is 16.
+function compareBuffersWithConstraints(should, actual, expected, options) {
+  if (!options)
+    options = {};
+
+  // Only print out the message if the lengths are different; the
+  // expectation is that they are the same, so don't clutter up the
+  // output.
+  if (actual.length !== expected.length) {
+    should(
+        actual.length === expected.length,
+        'Length of actual and expected buffers should match')
+        .beTrue();
+  }
+
+  let maxError = -1;
+  let diffCount = 0;
+  let errorPosition = -1;
+  let thresholdSNR = (options.thresholdSNR || 10000);
+
+  let thresholdDiffULP = (options.thresholdDiffULP || 0);
+  let thresholdDiffCount = (options.thresholdDiffCount || 0);
+
+  // By default, the bit depth is 16.
+  let bitDepth = (options.bitDepth || 16);
+  let scaleFactor = Math.pow(2, bitDepth - 1);
+
+  let noisePower = 0, signalPower = 0;
+
+  for (let i = 0; i < actual.length; i++) {
+    let diff = actual[i] - expected[i];
+    noisePower += diff * diff;
+    signalPower += expected[i] * expected[i];
+
+    if (Math.abs(diff) > maxError) {
+      maxError = Math.abs(diff);
+      errorPosition = i;
+    }
+
+    // The reference file is a 16-bit WAV file, so we will almost never get
+    // an exact match between it and the actual floating-point result.
+    if (Math.abs(diff) > scaleFactor)
+      diffCount++;
+  }
+
+  let snr = 10 * Math.log10(signalPower / noisePower);
+  let maxErrorULP = maxError * scaleFactor;
+
+  should(snr, 'SNR').beGreaterThanOrEqualTo(thresholdSNR);
+
+  should(
+      maxErrorULP,
+      options.prefix + ': Maximum difference (in ulp units (' + bitDepth +
+          '-bits))')
+      .beLessThanOrEqualTo(thresholdDiffULP);
+
+  should(diffCount, options.prefix + ': Number of differences between results')
+      .beLessThanOrEqualTo(thresholdDiffCount);
+}
+
+// Create an impulse in a buffer of length sampleFrameLength
+function createImpulseBuffer(context, sampleFrameLength) {
+  let audioBuffer =
+      context.createBuffer(1, sampleFrameLength, context.sampleRate);
+  let n = audioBuffer.length;
+  let dataL = audioBuffer.getChannelData(0);
+
+  for (let k = 0; k < n; ++k) {
+    dataL[k] = 0;
+  }
+  dataL[0] = 1;
+
+  return audioBuffer;
+}
+
+// Create a buffer of the given length with a linear ramp having values 0 <= x <
+// 1.
+function createLinearRampBuffer(context, sampleFrameLength) {
+  let audioBuffer =
+      context.createBuffer(1, sampleFrameLength, context.sampleRate);
+  let n = audioBuffer.length;
+  let dataL = audioBuffer.getChannelData(0);
+
+  for (let i = 0; i < n; ++i)
+    dataL[i] = i / n;
+
+  return audioBuffer;
+}
+
+// Create an AudioBuffer of length |sampleFrameLength| having a constant value
+// |constantValue|. If |constantValue| is a number, the buffer has one channel
+// filled with that value. If |constantValue| is an array, the buffer is created
+// wit a number of channels equal to the length of the array, and channel k is
+// filled with the k'th element of the |constantValue| array.
+function createConstantBuffer(context, sampleFrameLength, constantValue) {
+  let channels;
+  let values;
+
+  if (typeof constantValue === 'number') {
+    channels = 1;
+    values = [constantValue];
+  } else {
+    channels = constantValue.length;
+    values = constantValue;
+  }
+
+  let audioBuffer =
+      context.createBuffer(channels, sampleFrameLength, context.sampleRate);
+  let n = audioBuffer.length;
+
+  for (let c = 0; c < channels; ++c) {
+    let data = audioBuffer.getChannelData(c);
+    for (let i = 0; i < n; ++i)
+      data[i] = values[c];
+  }
+
+  return audioBuffer;
+}
+
+// Create a stereo impulse in a buffer of length sampleFrameLength
+function createStereoImpulseBuffer(context, sampleFrameLength) {
+  let audioBuffer =
+      context.createBuffer(2, sampleFrameLength, context.sampleRate);
+  let n = audioBuffer.length;
+  let dataL = audioBuffer.getChannelData(0);
+  let dataR = audioBuffer.getChannelData(1);
+
+  for (let k = 0; k < n; ++k) {
+    dataL[k] = 0;
+    dataR[k] = 0;
+  }
+  dataL[0] = 1;
+  dataR[0] = 1;
+
+  return audioBuffer;
+}
+
+// Convert time (in seconds) to sample frames.
+function timeToSampleFrame(time, sampleRate) {
+  return Math.floor(0.5 + time * sampleRate);
+}
+
+// Compute the number of sample frames consumed by noteGrainOn with
+// the specified |grainOffset|, |duration|, and |sampleRate|.
+function grainLengthInSampleFrames(grainOffset, duration, sampleRate) {
+  let startFrame = timeToSampleFrame(grainOffset, sampleRate);
+  let endFrame = timeToSampleFrame(grainOffset + duration, sampleRate);
+
+  return endFrame - startFrame;
+}
+
+// True if the number is not an infinity or NaN
+function isValidNumber(x) {
+  return !isNaN(x) && (x != Infinity) && (x != -Infinity);
+}
+
+// Compute the (linear) signal-to-noise ratio between |actual| and
+// |expected|.  The result is NOT in dB!  If the |actual| and
+// |expected| have different lengths, the shorter length is used.
+function computeSNR(actual, expected) {
+  let signalPower = 0;
+  let noisePower = 0;
+
+  let length = Math.min(actual.length, expected.length);
+
+  for (let k = 0; k < length; ++k) {
+    let diff = actual[k] - expected[k];
+    signalPower += expected[k] * expected[k];
+    noisePower += diff * diff;
+  }
+
+  return signalPower / noisePower;
+}
diff --git a/common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/audit.js b/common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/audit.js
new file mode 100644 (file)
index 0000000..896b9d5
--- /dev/null
@@ -0,0 +1,1442 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// See https://github.com/web-platform-tests/wpt/issues/12781 for information on
+// the purpose of audit.js, and why testharness.js does not suffice.
+
+/**
+ * @fileOverview  WebAudio layout test utility library. Built around W3C's
+ *                testharness.js. Includes asynchronous test task manager,
+ *                assertion utilities.
+ * @dependency    testharness.js
+ */
+
+
+(function() {
+
+  'use strict';
+
+  // Selected methods from testharness.js.
+  let testharnessProperties = [
+    'test', 'async_test', 'promise_test', 'promise_rejects_js', 'generate_tests',
+    'setup', 'done', 'assert_true', 'assert_false'
+  ];
+
+  // Check if testharness.js is properly loaded. Throw otherwise.
+  for (let name in testharnessProperties) {
+    if (!self.hasOwnProperty(testharnessProperties[name]))
+      throw new Error('Cannot proceed. testharness.js is not loaded.');
+  }
+})();
+
+
+window.Audit = (function() {
+
+  'use strict';
+
+  // NOTE: Moving this method (or any other code above) will change the location
+  // of 'CONSOLE ERROR...' message in the expected text files.
+  function _logError(message) {
+    console.error('[audit.js] ' + message);
+  }
+
+  function _logPassed(message) {
+    test(function(arg) {
+      assert_true(true);
+    }, message);
+  }
+
+  function _logFailed(message, detail) {
+    test(function() {
+      assert_true(false, detail);
+    }, message);
+  }
+
+  function _throwException(message) {
+    throw new Error(message);
+  }
+
+  // TODO(hongchan): remove this hack after confirming all the tests are
+  // finished correctly. (crbug.com/708817)
+  const _testharnessDone = window.done;
+  window.done = () => {
+    _throwException('Do NOT call done() method from the test code.');
+  };
+
+  // Generate a descriptive string from a target value in various types.
+  function _generateDescription(target, options) {
+    let targetString;
+
+    switch (typeof target) {
+      case 'object':
+        // Handle Arrays.
+        if (target instanceof Array || target instanceof Float32Array ||
+            target instanceof Float64Array || target instanceof Uint8Array) {
+          let arrayElements = target.length < options.numberOfArrayElements ?
+              String(target) :
+              String(target.slice(0, options.numberOfArrayElements)) + '...';
+          targetString = '[' + arrayElements + ']';
+        } else if (target === null) {
+          targetString = String(target);
+        } else {
+          targetString = '' + String(target).split(/[\s\]]/)[1];
+        }
+        break;
+      case 'function':
+        if (Error.isPrototypeOf(target)) {
+          targetString = "EcmaScript error " + target.name;
+        } else {
+          targetString = String(target);
+        }
+        break;
+      default:
+        targetString = String(target);
+        break;
+    }
+
+    return targetString;
+  }
+
+  // Return a string suitable for printing one failed element in
+  // |beCloseToArray|.
+  function _formatFailureEntry(index, actual, expected, abserr, threshold) {
+    return '\t[' + index + ']\t' + actual.toExponential(16) + '\t' +
+        expected.toExponential(16) + '\t' + abserr.toExponential(16) + '\t' +
+        (abserr / Math.abs(expected)).toExponential(16) + '\t' +
+        threshold.toExponential(16);
+  }
+
+  // Compute the error threshold criterion for |beCloseToArray|
+  function _closeToThreshold(abserr, relerr, expected) {
+    return Math.max(abserr, relerr * Math.abs(expected));
+  }
+
+  /**
+   * @class Should
+   * @description Assertion subtask for the Audit task.
+   * @param {Task} parentTask           Associated Task object.
+   * @param {Any} actual                Target value to be tested.
+   * @param {String} actualDescription  String description of the test target.
+   */
+  class Should {
+    constructor(parentTask, actual, actualDescription) {
+      this._task = parentTask;
+
+      this._actual = actual;
+      this._actualDescription = (actualDescription || null);
+      this._expected = null;
+      this._expectedDescription = null;
+
+      this._detail = '';
+      // If true and the test failed, print the actual value at the
+      // end of the message.
+      this._printActualForFailure = true;
+
+      this._result = null;
+
+      /**
+       * @param {Number} numberOfErrors   Number of errors to be printed.
+       * @param {Number} numberOfArrayElements  Number of array elements to be
+       *                                        printed in the test log.
+       * @param {Boolean} verbose         Verbose output from the assertion.
+       */
+      this._options = {
+        numberOfErrors: 4,
+        numberOfArrayElements: 16,
+        verbose: false
+      };
+    }
+
+    _processArguments(args) {
+      if (args.length === 0)
+        return;
+
+      if (args.length > 0)
+        this._expected = args[0];
+
+      if (typeof args[1] === 'string') {
+        // case 1: (expected, description, options)
+        this._expectedDescription = args[1];
+        Object.assign(this._options, args[2]);
+      } else if (typeof args[1] === 'object') {
+        // case 2: (expected, options)
+        Object.assign(this._options, args[1]);
+      }
+    }
+
+    _buildResultText() {
+      if (this._result === null)
+        _throwException('Illegal invocation: the assertion is not finished.');
+
+      let actualString = _generateDescription(this._actual, this._options);
+
+      // Use generated text when the description is not provided.
+      if (!this._actualDescription)
+        this._actualDescription = actualString;
+
+      if (!this._expectedDescription) {
+        this._expectedDescription =
+            _generateDescription(this._expected, this._options);
+      }
+
+      // For the assertion with a single operand.
+      this._detail =
+          this._detail.replace(/\$\{actual\}/g, this._actualDescription);
+
+      // If there is a second operand (i.e. expected value), we have to build
+      // the string for it as well.
+      this._detail =
+          this._detail.replace(/\$\{expected\}/g, this._expectedDescription);
+
+      // If there is any property in |_options|, replace the property name
+      // with the value.
+      for (let name in this._options) {
+        if (name === 'numberOfErrors' || name === 'numberOfArrayElements' ||
+            name === 'verbose') {
+          continue;
+        }
+
+        // The RegExp key string contains special character. Take care of it.
+        let re = '\$\{' + name + '\}';
+        re = re.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
+        this._detail = this._detail.replace(
+            new RegExp(re, 'g'), _generateDescription(this._options[name]));
+      }
+
+      // If the test failed, add the actual value at the end.
+      if (this._result === false && this._printActualForFailure === true) {
+        this._detail += ' Got ' + actualString + '.';
+      }
+    }
+
+    _finalize() {
+      if (this._result) {
+        _logPassed('  ' + this._detail);
+      } else {
+        _logFailed('X ' + this._detail);
+      }
+
+      // This assertion is finished, so update the parent task accordingly.
+      this._task.update(this);
+
+      // TODO(hongchan): configurable 'detail' message.
+    }
+
+    _assert(condition, passDetail, failDetail) {
+      this._result = Boolean(condition);
+      this._detail = this._result ? passDetail : failDetail;
+      this._buildResultText();
+      this._finalize();
+
+      return this._result;
+    }
+
+    get result() {
+      return this._result;
+    }
+
+    get detail() {
+      return this._detail;
+    }
+
+    /**
+     * should() assertions.
+     *
+     * @example All the assertions can have 1, 2 or 3 arguments:
+     *   should().doAssert(expected);
+     *   should().doAssert(expected, options);
+     *   should().doAssert(expected, expectedDescription, options);
+     *
+     * @param {Any} expected                  Expected value of the assertion.
+     * @param {String} expectedDescription    Description of expected value.
+     * @param {Object} options                Options for assertion.
+     * @param {Number} options.numberOfErrors Number of errors to be printed.
+     *                                        (if applicable)
+     * @param {Number} options.numberOfArrayElements  Number of array elements
+     *                                                to be printed. (if
+     *                                                applicable)
+     * @notes Some assertions can have additional options for their specific
+     *        testing.
+     */
+
+    /**
+     * Check if |actual| exists.
+     *
+     * @example
+     *   should({}, 'An empty object').exist();
+     * @result
+     *   "PASS   An empty object does exist."
+     */
+    exist() {
+      return this._assert(
+          this._actual !== null && this._actual !== undefined,
+          '${actual} does exist.', '${actual} does not exist.');
+    }
+
+    /**
+     * Check if |actual| operation wrapped in a function throws an exception
+     * with a expected error type correctly. |expected| is optional. If it is an
+     * instance of DOMException, then the description (second argument) can be
+     * provided to be more strict about the expected exception type. |expected|
+     * also can be other generic error types such as TypeError, RangeError or
+     * etc.
+     *
+     * @example
+     *   should(() => { let a = b; }, 'A bad code').throw();
+     *   should(() => { new SomeConstructor(); }, 'A bad construction')
+     *       .throw(DOMException, 'NotSupportedError');
+     *   should(() => { let c = d; }, 'Assigning d to c')
+     *       .throw(ReferenceError);
+     *   should(() => { let e = f; }, 'Assigning e to f')
+     *       .throw(ReferenceError, { omitErrorMessage: true });
+     *
+     * @result
+     *   "PASS   A bad code threw an exception of ReferenceError: b is not
+     *       defined."
+     *   "PASS   A bad construction threw DOMException:NotSupportedError."
+     *   "PASS   Assigning d to c threw ReferenceError: d is not defined."
+     *   "PASS   Assigning e to f threw ReferenceError: [error message
+     *       omitted]."
+     */
+    throw() {
+      this._processArguments(arguments);
+      this._printActualForFailure = false;
+
+      let didThrowCorrectly = false;
+      let passDetail, failDetail;
+
+      try {
+        // This should throw.
+        this._actual();
+        // Catch did not happen, so the test is failed.
+        failDetail = '${actual} did not throw an exception.';
+      } catch (error) {
+        let errorMessage = this._options.omitErrorMessage ?
+            ': [error message omitted]' :
+            ': "' + error.message + '"';
+        if (this._expected === null || this._expected === undefined) {
+          // The expected error type was not given.
+          didThrowCorrectly = true;
+          passDetail = '${actual} threw ' + error.name + errorMessage + '.';
+        } else if (this._expected === DOMException &&
+                   (this._expectedDescription === undefined ||
+                    this._expectedDescription === error.name)) {
+          // Handles DOMException with the associated name.
+          didThrowCorrectly = true;
+          passDetail = '${actual} threw ${expected}' + errorMessage + '.';
+        } else if (this._expected == error.constructor) {
+          // Handler other error types.
+          didThrowCorrectly = true;
+          passDetail = '${actual} threw ' + error.name + errorMessage + '.';
+        } else {
+          didThrowCorrectly = false;
+          failDetail =
+              '${actual} threw "' + error.name + '" instead of ${expected}.';
+        }
+      }
+
+      return this._assert(didThrowCorrectly, passDetail, failDetail);
+    }
+
+    /**
+     * Check if |actual| operation wrapped in a function does not throws an
+     * exception correctly.
+     *
+     * @example
+     *   should(() => { let foo = 'bar'; }, 'let foo = "bar"').notThrow();
+     *
+     * @result
+     *   "PASS   let foo = "bar" did not throw an exception."
+     */
+    notThrow() {
+      this._printActualForFailure = false;
+
+      let didThrowCorrectly = false;
+      let passDetail, failDetail;
+
+      try {
+        this._actual();
+        passDetail = '${actual} did not throw an exception.';
+      } catch (error) {
+        didThrowCorrectly = true;
+        failDetail = '${actual} incorrectly threw ' + error.name + ': "' +
+            error.message + '".';
+      }
+
+      return this._assert(!didThrowCorrectly, passDetail, failDetail);
+    }
+
+    /**
+     * Check if |actual| promise is resolved correctly. Note that the returned
+     * result from promise object will be passed to the following then()
+     * function.
+     *
+     * @example
+     *   should('My promise', promise).beResolve().then((result) => {
+     *     log(result);
+     *   });
+     *
+     * @result
+     *   "PASS   My promise resolved correctly."
+     *   "FAIL X My promise rejected *INCORRECTLY* with _ERROR_."
+     */
+    beResolved() {
+      return this._actual.then(
+          function(result) {
+            this._assert(true, '${actual} resolved correctly.', null);
+            return result;
+          }.bind(this),
+          function(error) {
+            this._assert(
+                false, null,
+                '${actual} rejected incorrectly with ' + error + '.');
+          }.bind(this));
+    }
+
+    /**
+     * Check if |actual| promise is rejected correctly.
+     *
+     * @example
+     *   should('My promise', promise).beRejected().then(nextStuff);
+     *
+     * @result
+     *   "PASS   My promise rejected correctly (with _ERROR_)."
+     *   "FAIL X My promise resolved *INCORRECTLY*."
+     */
+    beRejected() {
+      return this._actual.then(
+          function() {
+            this._assert(false, null, '${actual} resolved incorrectly.');
+          }.bind(this),
+          function(error) {
+            this._assert(
+                true, '${actual} rejected correctly with ' + error + '.', null);
+          }.bind(this));
+    }
+
+    /**
+     * Check if |actual| promise is rejected correctly.
+     *
+     * @example
+     *   should(promise, 'My promise').beRejectedWith('_ERROR_').then();
+     *
+     * @result
+     *   "PASS   My promise rejected correctly with _ERROR_."
+     *   "FAIL X My promise rejected correctly but got _ACTUAL_ERROR instead of
+     *           _EXPECTED_ERROR_."
+     *   "FAIL X My promise resolved incorrectly."
+     */
+    beRejectedWith() {
+      this._processArguments(arguments);
+
+      return this._actual.then(
+          function() {
+            this._assert(false, null, '${actual} resolved incorrectly.');
+          }.bind(this),
+          function(error) {
+            if (this._expected !== error.name) {
+              this._assert(
+                  false, null,
+                  '${actual} rejected correctly but got ' + error.name +
+                      ' instead of ' + this._expected + '.');
+            } else {
+              this._assert(
+                  true,
+                  '${actual} rejected correctly with ' + this._expected + '.',
+                  null);
+            }
+          }.bind(this));
+    }
+
+    /**
+     * Check if |actual| is a boolean true.
+     *
+     * @example
+     *   should(3 < 5, '3 < 5').beTrue();
+     *
+     * @result
+     *   "PASS   3 < 5 is true."
+     */
+    beTrue() {
+      return this._assert(
+          this._actual === true, '${actual} is true.',
+          '${actual} is not true.');
+    }
+
+    /**
+     * Check if |actual| is a boolean false.
+     *
+     * @example
+     *   should(3 > 5, '3 > 5').beFalse();
+     *
+     * @result
+     *   "PASS   3 > 5 is false."
+     */
+    beFalse() {
+      return this._assert(
+          this._actual === false, '${actual} is false.',
+          '${actual} is not false.');
+    }
+
+    /**
+     * Check if |actual| is strictly equal to |expected|. (no type coercion)
+     *
+     * @example
+     *   should(1).beEqualTo(1);
+     *
+     * @result
+     *   "PASS   1 is equal to 1."
+     */
+    beEqualTo() {
+      this._processArguments(arguments);
+      return this._assert(
+          this._actual === this._expected, '${actual} is equal to ${expected}.',
+          '${actual} is not equal to ${expected}.');
+    }
+
+    /**
+     * Check if |actual| is not equal to |expected|.
+     *
+     * @example
+     *   should(1).notBeEqualTo(2);
+     *
+     * @result
+     *   "PASS   1 is not equal to 2."
+     */
+    notBeEqualTo() {
+      this._processArguments(arguments);
+      return this._assert(
+          this._actual !== this._expected,
+          '${actual} is not equal to ${expected}.',
+          '${actual} should not be equal to ${expected}.');
+    }
+
+    /**
+     * check if |actual| is NaN
+     *
+     * @example
+     *   should(NaN).beNaN();
+     *
+     * @result
+     *   "PASS   NaN is NaN"
+     *
+     */
+    beNaN() {
+      this._processArguments(arguments);
+      return this._assert(
+          isNaN(this._actual),
+          '${actual} is NaN.',
+          '${actual} is not NaN but should be.');
+    }
+
+    /**
+     * check if |actual| is NOT NaN
+     *
+     * @example
+     *   should(42).notBeNaN();
+     *
+     * @result
+     *   "PASS   42 is not NaN"
+     *
+     */
+    notBeNaN() {
+      this._processArguments(arguments);
+      return this._assert(
+          !isNaN(this._actual),
+          '${actual} is not NaN.',
+          '${actual} is NaN but should not be.');
+    }
+
+    /**
+     * Check if |actual| is greater than |expected|.
+     *
+     * @example
+     *   should(2).beGreaterThanOrEqualTo(2);
+     *
+     * @result
+     *   "PASS   2 is greater than or equal to 2."
+     */
+    beGreaterThan() {
+      this._processArguments(arguments);
+      return this._assert(
+          this._actual > this._expected,
+          '${actual} is greater than ${expected}.',
+          '${actual} is not greater than ${expected}.');
+    }
+
+    /**
+     * Check if |actual| is greater than or equal to |expected|.
+     *
+     * @example
+     *   should(2).beGreaterThan(1);
+     *
+     * @result
+     *   "PASS   2 is greater than 1."
+     */
+    beGreaterThanOrEqualTo() {
+      this._processArguments(arguments);
+      return this._assert(
+          this._actual >= this._expected,
+          '${actual} is greater than or equal to ${expected}.',
+          '${actual} is not greater than or equal to ${expected}.');
+    }
+
+    /**
+     * Check if |actual| is less than |expected|.
+     *
+     * @example
+     *   should(1).beLessThan(2);
+     *
+     * @result
+     *   "PASS   1 is less than 2."
+     */
+    beLessThan() {
+      this._processArguments(arguments);
+      return this._assert(
+          this._actual < this._expected, '${actual} is less than ${expected}.',
+          '${actual} is not less than ${expected}.');
+    }
+
+    /**
+     * Check if |actual| is less than or equal to |expected|.
+     *
+     * @example
+     *   should(1).beLessThanOrEqualTo(1);
+     *
+     * @result
+     *   "PASS   1 is less than or equal to 1."
+     */
+    beLessThanOrEqualTo() {
+      this._processArguments(arguments);
+      return this._assert(
+          this._actual <= this._expected,
+          '${actual} is less than or equal to ${expected}.',
+          '${actual} is not less than or equal to ${expected}.');
+    }
+
+    /**
+     * Check if |actual| array is filled with a constant |expected| value.
+     *
+     * @example
+     *   should([1, 1, 1]).beConstantValueOf(1);
+     *
+     * @result
+     *   "PASS   [1,1,1] contains only the constant 1."
+     */
+    beConstantValueOf() {
+      this._processArguments(arguments);
+      this._printActualForFailure = false;
+
+      let passed = true;
+      let passDetail, failDetail;
+      let errors = {};
+
+      let actual = this._actual;
+      let expected = this._expected;
+      for (let index = 0; index < actual.length; ++index) {
+        if (actual[index] !== expected)
+          errors[index] = actual[index];
+      }
+
+      let numberOfErrors = Object.keys(errors).length;
+      passed = numberOfErrors === 0;
+
+      if (passed) {
+        passDetail = '${actual} contains only the constant ${expected}.';
+      } else {
+        let counter = 0;
+        failDetail =
+            '${actual}: Expected ${expected} for all values but found ' +
+            numberOfErrors + ' unexpected values: ';
+        failDetail += '\n\tIndex\tActual';
+        for (let errorIndex in errors) {
+          failDetail += '\n\t[' + errorIndex + ']' +
+              '\t' + errors[errorIndex];
+          if (++counter >= this._options.numberOfErrors) {
+            failDetail +=
+                '\n\t...and ' + (numberOfErrors - counter) + ' more errors.';
+            break;
+          }
+        }
+      }
+
+      return this._assert(passed, passDetail, failDetail);
+    }
+
+    /**
+     * Check if |actual| array is not filled with a constant |expected| value.
+     *
+     * @example
+     *   should([1, 0, 1]).notBeConstantValueOf(1);
+     *   should([0, 0, 0]).notBeConstantValueOf(0);
+     *
+     * @result
+     *   "PASS   [1,0,1] is not constantly 1 (contains 1 different value)."
+     *   "FAIL X [0,0,0] should have contain at least one value different
+     *     from 0."
+     */
+    notBeConstantValueOf() {
+      this._processArguments(arguments);
+      this._printActualForFailure = false;
+
+      let passed = true;
+      let passDetail;
+      let failDetail;
+      let differences = {};
+
+      let actual = this._actual;
+      let expected = this._expected;
+      for (let index = 0; index < actual.length; ++index) {
+        if (actual[index] !== expected)
+          differences[index] = actual[index];
+      }
+
+      let numberOfDifferences = Object.keys(differences).length;
+      passed = numberOfDifferences > 0;
+
+      if (passed) {
+        let valueString = numberOfDifferences > 1 ? 'values' : 'value';
+        passDetail = '${actual} is not constantly ${expected} (contains ' +
+            numberOfDifferences + ' different ' + valueString + ').';
+      } else {
+        failDetail = '${actual} should have contain at least one value ' +
+            'different from ${expected}.';
+      }
+
+      return this._assert(passed, passDetail, failDetail);
+    }
+
+    /**
+     * Check if |actual| array is identical to |expected| array element-wise.
+     *
+     * @example
+     *   should([1, 2, 3]).beEqualToArray([1, 2, 3]);
+     *
+     * @result
+     *   "[1,2,3] is identical to the array [1,2,3]."
+     */
+    beEqualToArray() {
+      this._processArguments(arguments);
+      this._printActualForFailure = false;
+
+      let passed = true;
+      let passDetail, failDetail;
+      let errorIndices = [];
+
+      if (this._actual.length !== this._expected.length) {
+        passed = false;
+        failDetail = 'The array length does not match.';
+        return this._assert(passed, passDetail, failDetail);
+      }
+
+      let actual = this._actual;
+      let expected = this._expected;
+      for (let index = 0; index < actual.length; ++index) {
+        if (actual[index] !== expected[index])
+          errorIndices.push(index);
+      }
+
+      passed = errorIndices.length === 0;
+
+      if (passed) {
+        passDetail = '${actual} is identical to the array ${expected}.';
+      } else {
+        let counter = 0;
+        failDetail =
+            '${actual} expected to be equal to the array ${expected} ' +
+            'but differs in ' + errorIndices.length + ' places:' +
+            '\n\tIndex\tActual\t\t\tExpected';
+        for (let index of errorIndices) {
+          failDetail += '\n\t[' + index + ']' +
+              '\t' + this._actual[index].toExponential(16) + '\t' +
+              this._expected[index].toExponential(16);
+          if (++counter >= this._options.numberOfErrors) {
+            failDetail += '\n\t...and ' + (errorIndices.length - counter) +
+                ' more errors.';
+            break;
+          }
+        }
+      }
+
+      return this._assert(passed, passDetail, failDetail);
+    }
+
+    /**
+     * Check if |actual| array contains only the values in |expected| in the
+     * order of values in |expected|.
+     *
+     * @example
+     *   Should([1, 1, 3, 3, 2], 'My random array').containValues([1, 3, 2]);
+     *
+     * @result
+     *   "PASS   [1,1,3,3,2] contains all the expected values in the correct
+     *           order: [1,3,2].
+     */
+    containValues() {
+      this._processArguments(arguments);
+      this._printActualForFailure = false;
+
+      let passed = true;
+      let indexedActual = [];
+      let firstErrorIndex = null;
+
+      // Collect the unique value sequence from the actual.
+      for (let i = 0, prev = null; i < this._actual.length; i++) {
+        if (this._actual[i] !== prev) {
+          indexedActual.push({index: i, value: this._actual[i]});
+          prev = this._actual[i];
+        }
+      }
+
+      // Compare against the expected sequence.
+      let failMessage =
+          '${actual} expected to have the value sequence of ${expected} but ' +
+          'got ';
+      if (this._expected.length === indexedActual.length) {
+        for (let j = 0; j < this._expected.length; j++) {
+          if (this._expected[j] !== indexedActual[j].value) {
+            firstErrorIndex = indexedActual[j].index;
+            passed = false;
+            failMessage += this._actual[firstErrorIndex] + ' at index ' +
+                firstErrorIndex + '.';
+            break;
+          }
+        }
+      } else {
+        passed = false;
+        let indexedValues = indexedActual.map(x => x.value);
+        failMessage += `${indexedActual.length} values, [${
+            indexedValues}], instead of ${this._expected.length}.`;
+      }
+
+      return this._assert(
+          passed,
+          '${actual} contains all the expected values in the correct order: ' +
+              '${expected}.',
+          failMessage);
+    }
+
+    /**
+     * Check if |actual| array does not have any glitches. Note that |threshold|
+     * is not optional and is to define the desired threshold value.
+     *
+     * @example
+     *   should([0.5, 0.5, 0.55, 0.5, 0.45, 0.5]).notGlitch(0.06);
+     *
+     * @result
+     *   "PASS   [0.5,0.5,0.55,0.5,0.45,0.5] has no glitch above the threshold
+     *           of 0.06."
+     *
+     */
+    notGlitch() {
+      this._processArguments(arguments);
+      this._printActualForFailure = false;
+
+      let passed = true;
+      let passDetail, failDetail;
+
+      let actual = this._actual;
+      let expected = this._expected;
+      for (let index = 0; index < actual.length; ++index) {
+        let diff = Math.abs(actual[index - 1] - actual[index]);
+        if (diff >= expected) {
+          passed = false;
+          failDetail = '${actual} has a glitch at index ' + index +
+              ' of size ' + diff + '.';
+        }
+      }
+
+      passDetail =
+          '${actual} has no glitch above the threshold of ${expected}.';
+
+      return this._assert(passed, passDetail, failDetail);
+    }
+
+    /**
+     * Check if |actual| is close to |expected| using the given relative error
+     * |threshold|.
+     *
+     * @example
+     *   should(2.3).beCloseTo(2, { threshold: 0.3 });
+     *
+     * @result
+     *   "PASS    2.3 is 2 within an error of 0.3."
+     * @param {Object} options              Options for assertion.
+     * @param {Number} options.threshold    Threshold value for the comparison.
+     */
+    beCloseTo() {
+      this._processArguments(arguments);
+
+      // The threshold is relative except when |expected| is zero, in which case
+      // it is absolute.
+      let absExpected = this._expected ? Math.abs(this._expected) : 1;
+      let error = Math.abs(this._actual - this._expected) / absExpected;
+
+      // debugger;
+
+      return this._assert(
+          error <= this._options.threshold,
+          '${actual} is ${expected} within an error of ${threshold}.',
+          '${actual} is not close to ${expected} within a relative error of ' +
+              '${threshold} (RelErr=' + error + ').');
+    }
+
+    /**
+     * Check if |target| array is close to |expected| array element-wise within
+     * a certain error bound given by the |options|.
+     *
+     * The error criterion is:
+     *   abs(actual[k] - expected[k]) < max(absErr, relErr * abs(expected))
+     *
+     * If nothing is given for |options|, then absErr = relErr = 0. If
+     * absErr = 0, then the error criterion is a relative error. A non-zero
+     * absErr value produces a mix intended to handle the case where the
+     * expected value is 0, allowing the target value to differ by absErr from
+     * the expected.
+     *
+     * @param {Number} options.absoluteThreshold    Absolute threshold.
+     * @param {Number} options.relativeThreshold    Relative threshold.
+     */
+    beCloseToArray() {
+      this._processArguments(arguments);
+      this._printActualForFailure = false;
+
+      let passed = true;
+      let passDetail, failDetail;
+
+      // Parsing options.
+      let absErrorThreshold = (this._options.absoluteThreshold || 0);
+      let relErrorThreshold = (this._options.relativeThreshold || 0);
+
+      // A collection of all of the values that satisfy the error criterion.
+      // This holds the absolute difference between the target element and the
+      // expected element.
+      let errors = {};
+
+      // Keep track of the max absolute error found.
+      let maxAbsError = -Infinity, maxAbsErrorIndex = -1;
+
+      // Keep track of the max relative error found, ignoring cases where the
+      // relative error is Infinity because the expected value is 0.
+      let maxRelError = -Infinity, maxRelErrorIndex = -1;
+
+      let actual = this._actual;
+      let expected = this._expected;
+
+      for (let index = 0; index < expected.length; ++index) {
+        let diff = Math.abs(actual[index] - expected[index]);
+        let absExpected = Math.abs(expected[index]);
+        let relError = diff / absExpected;
+
+        if (diff >
+            Math.max(absErrorThreshold, relErrorThreshold * absExpected)) {
+          if (diff > maxAbsError) {
+            maxAbsErrorIndex = index;
+            maxAbsError = diff;
+          }
+
+          if (!isNaN(relError) && relError > maxRelError) {
+            maxRelErrorIndex = index;
+            maxRelError = relError;
+          }
+
+          errors[index] = diff;
+        }
+      }
+
+      let numberOfErrors = Object.keys(errors).length;
+      let maxAllowedErrorDetail = JSON.stringify({
+        absoluteThreshold: absErrorThreshold,
+        relativeThreshold: relErrorThreshold
+      });
+
+      if (numberOfErrors === 0) {
+        // The assertion was successful.
+        passDetail = '${actual} equals ${expected} with an element-wise ' +
+            'tolerance of ' + maxAllowedErrorDetail + '.';
+      } else {
+        // Failed. Prepare the detailed failure log.
+        passed = false;
+        failDetail = '${actual} does not equal ${expected} with an ' +
+            'element-wise tolerance of ' + maxAllowedErrorDetail + '.\n';
+
+        // Print out actual, expected, absolute error, and relative error.
+        let counter = 0;
+        failDetail += '\tIndex\tActual\t\t\tExpected\t\tAbsError' +
+            '\t\tRelError\t\tTest threshold';
+        let printedIndices = [];
+        for (let index in errors) {
+          failDetail +=
+              '\n' +
+              _formatFailureEntry(
+                  index, actual[index], expected[index], errors[index],
+                  _closeToThreshold(
+                      absErrorThreshold, relErrorThreshold, expected[index]));
+
+          printedIndices.push(index);
+          if (++counter > this._options.numberOfErrors) {
+            failDetail +=
+                '\n\t...and ' + (numberOfErrors - counter) + ' more errors.';
+            break;
+          }
+        }
+
+        // Finalize the error log: print out the location of both the maxAbs
+        // error and the maxRel error so we can adjust thresholds appropriately
+        // in the test.
+        failDetail += '\n' +
+            '\tMax AbsError of ' + maxAbsError.toExponential(16) +
+            ' at index of ' + maxAbsErrorIndex + '.\n';
+        if (printedIndices.find(element => {
+              return element == maxAbsErrorIndex;
+            }) === undefined) {
+          // Print an entry for this index if we haven't already.
+          failDetail +=
+              _formatFailureEntry(
+                  maxAbsErrorIndex, actual[maxAbsErrorIndex],
+                  expected[maxAbsErrorIndex], errors[maxAbsErrorIndex],
+                  _closeToThreshold(
+                      absErrorThreshold, relErrorThreshold,
+                      expected[maxAbsErrorIndex])) +
+              '\n';
+        }
+        failDetail += '\tMax RelError of ' + maxRelError.toExponential(16) +
+            ' at index of ' + maxRelErrorIndex + '.\n';
+        if (printedIndices.find(element => {
+              return element == maxRelErrorIndex;
+            }) === undefined) {
+          // Print an entry for this index if we haven't already.
+          failDetail +=
+              _formatFailureEntry(
+                  maxRelErrorIndex, actual[maxRelErrorIndex],
+                  expected[maxRelErrorIndex], errors[maxRelErrorIndex],
+                  _closeToThreshold(
+                      absErrorThreshold, relErrorThreshold,
+                      expected[maxRelErrorIndex])) +
+              '\n';
+        }
+      }
+
+      return this._assert(passed, passDetail, failDetail);
+    }
+
+    /**
+     * A temporary escape hat for printing an in-task message. The description
+     * for the |actual| is required to get the message printed properly.
+     *
+     * TODO(hongchan): remove this method when the transition from the old Audit
+     * to the new Audit is completed.
+     * @example
+     *   should(true, 'The message is').message('truthful!', 'false!');
+     *
+     * @result
+     *   "PASS   The message is truthful!"
+     */
+    message(passDetail, failDetail) {
+      return this._assert(
+          this._actual, '${actual} ' + passDetail, '${actual} ' + failDetail);
+    }
+
+    /**
+     * Check if |expected| property is truly owned by |actual| object.
+     *
+     * @example
+     *   should(BaseAudioContext.prototype,
+     *          'BaseAudioContext.prototype').haveOwnProperty('createGain');
+     *
+     * @result
+     *   "PASS   BaseAudioContext.prototype has an own property of
+     *       'createGain'."
+     */
+    haveOwnProperty() {
+      this._processArguments(arguments);
+
+      return this._assert(
+          this._actual.hasOwnProperty(this._expected),
+          '${actual} has an own property of "${expected}".',
+          '${actual} does not own the property of "${expected}".');
+    }
+
+
+    /**
+     * Check if |expected| property is not owned by |actual| object.
+     *
+     * @example
+     *   should(BaseAudioContext.prototype,
+     *          'BaseAudioContext.prototype')
+     *       .notHaveOwnProperty('startRendering');
+     *
+     * @result
+     *   "PASS   BaseAudioContext.prototype does not have an own property of
+     *       'startRendering'."
+     */
+    notHaveOwnProperty() {
+      this._processArguments(arguments);
+
+      return this._assert(
+          !this._actual.hasOwnProperty(this._expected),
+          '${actual} does not have an own property of "${expected}".',
+          '${actual} has an own the property of "${expected}".')
+    }
+
+
+    /**
+     * Check if an object is inherited from a class. This looks up the entire
+     * prototype chain of a given object and tries to find a match.
+     *
+     * @example
+     *   should(sourceNode, 'A buffer source node')
+     *       .inheritFrom('AudioScheduledSourceNode');
+     *
+     * @result
+     *   "PASS   A buffer source node inherits from 'AudioScheduledSourceNode'."
+     */
+    inheritFrom() {
+      this._processArguments(arguments);
+
+      let prototypes = [];
+      let currentPrototype = Object.getPrototypeOf(this._actual);
+      while (currentPrototype) {
+        prototypes.push(currentPrototype.constructor.name);
+        currentPrototype = Object.getPrototypeOf(currentPrototype);
+      }
+
+      return this._assert(
+          prototypes.includes(this._expected),
+          '${actual} inherits from "${expected}".',
+          '${actual} does not inherit from "${expected}".');
+    }
+  }
+
+
+  // Task Class state enum.
+  const TaskState = {PENDING: 0, STARTED: 1, FINISHED: 2};
+
+
+  /**
+   * @class Task
+   * @description WebAudio testing task. Managed by TaskRunner.
+   */
+  class Task {
+    /**
+     * Task constructor.
+     * @param  {Object} taskRunner Reference of associated task runner.
+     * @param  {String||Object} taskLabel Task label if a string is given. This
+     *                                    parameter can be a dictionary with the
+     *                                    following fields.
+     * @param  {String} taskLabel.label Task label.
+     * @param  {String} taskLabel.description Description of task.
+     * @param  {Function} taskFunction Task function to be performed.
+     * @return {Object} Task object.
+     */
+    constructor(taskRunner, taskLabel, taskFunction) {
+      this._taskRunner = taskRunner;
+      this._taskFunction = taskFunction;
+
+      if (typeof taskLabel === 'string') {
+        this._label = taskLabel;
+        this._description = null;
+      } else if (typeof taskLabel === 'object') {
+        if (typeof taskLabel.label !== 'string') {
+          _throwException('Task.constructor:: task label must be string.');
+        }
+        this._label = taskLabel.label;
+        this._description = (typeof taskLabel.description === 'string') ?
+            taskLabel.description :
+            null;
+      } else {
+        _throwException(
+            'Task.constructor:: task label must be a string or ' +
+            'a dictionary.');
+      }
+
+      this._state = TaskState.PENDING;
+      this._result = true;
+
+      this._totalAssertions = 0;
+      this._failedAssertions = 0;
+    }
+
+    get label() {
+      return this._label;
+    }
+
+    get state() {
+      return this._state;
+    }
+
+    get result() {
+      return this._result;
+    }
+
+    // Start the assertion chain.
+    should(actual, actualDescription) {
+      // If no argument is given, we cannot proceed. Halt.
+      if (arguments.length === 0)
+        _throwException('Task.should:: requires at least 1 argument.');
+
+      return new Should(this, actual, actualDescription);
+    }
+
+    // Run this task. |this| task will be passed into the user-supplied test
+    // task function.
+    run(harnessTest) {
+      this._state = TaskState.STARTED;
+      this._harnessTest = harnessTest;
+      // Print out the task entry with label and description.
+      _logPassed(
+          '> [' + this._label + '] ' +
+          (this._description ? this._description : ''));
+
+      return new Promise((resolve, reject) => {
+        this._resolve = resolve;
+        this._reject = reject;
+        let result = this._taskFunction(this, this.should.bind(this));
+        if (result && typeof result.then === "function") {
+          result.then(() => this.done()).catch(reject);
+        }
+      });
+    }
+
+    // Update the task success based on the individual assertion/test inside.
+    update(subTask) {
+      // After one of tests fails within a task, the result is irreversible.
+      if (subTask.result === false) {
+        this._result = false;
+        this._failedAssertions++;
+      }
+
+      this._totalAssertions++;
+    }
+
+    // Finish the current task and start the next one if available.
+    done() {
+      assert_equals(this._state, TaskState.STARTED)
+      this._state = TaskState.FINISHED;
+
+      let message = '< [' + this._label + '] ';
+
+      if (this._result) {
+        message += 'All assertions passed. (total ' + this._totalAssertions +
+            ' assertions)';
+        _logPassed(message);
+      } else {
+        message += this._failedAssertions + ' out of ' + this._totalAssertions +
+            ' assertions were failed.'
+        _logFailed(message);
+      }
+
+      this._resolve();
+    }
+
+    // Runs |subTask| |time| milliseconds later. |setTimeout| is not allowed in
+    // WPT linter, so a thin wrapper around the harness's |step_timeout| is
+    // used here.  Returns a Promise which is resolved after |subTask| runs.
+    timeout(subTask, time) {
+      return new Promise(resolve => {
+        this._harnessTest.step_timeout(() => {
+          let result = subTask();
+          if (result && typeof result.then === "function") {
+            // Chain rejection directly to the harness test Promise, to report
+            // the rejection against the subtest even when the caller of
+            // timeout does not handle the rejection.
+            result.then(resolve, this._reject());
+          } else {
+            resolve();
+          }
+        }, time);
+      });
+    }
+
+    isPassed() {
+      return this._state === TaskState.FINISHED && this._result;
+    }
+
+    toString() {
+      return '"' + this._label + '": ' + this._description;
+    }
+  }
+
+
+  /**
+   * @class TaskRunner
+   * @description WebAudio testing task runner. Manages tasks.
+   */
+  class TaskRunner {
+    constructor() {
+      this._tasks = {};
+      this._taskSequence = [];
+
+      // Configure testharness.js for the async operation.
+      setup(new Function(), {explicit_done: true});
+    }
+
+    _finish() {
+      let numberOfFailures = 0;
+      for (let taskIndex in this._taskSequence) {
+        let task = this._tasks[this._taskSequence[taskIndex]];
+        numberOfFailures += task.result ? 0 : 1;
+      }
+
+      let prefix = '# AUDIT TASK RUNNER FINISHED: ';
+      if (numberOfFailures > 0) {
+        _logFailed(
+            prefix + numberOfFailures + ' out of ' + this._taskSequence.length +
+            ' tasks were failed.');
+      } else {
+        _logPassed(
+            prefix + this._taskSequence.length + ' tasks ran successfully.');
+      }
+
+      return Promise.resolve();
+    }
+
+    // |taskLabel| can be either a string or a dictionary. See Task constructor
+    // for the detail.  If |taskFunction| returns a thenable, then the task
+    // is considered complete when the thenable is fulfilled; otherwise the
+    // task must be completed with an explicit call to |task.done()|.
+    define(taskLabel, taskFunction) {
+      let task = new Task(this, taskLabel, taskFunction);
+      if (this._tasks.hasOwnProperty(task.label)) {
+        _throwException('Audit.define:: Duplicate task definition.');
+        return;
+      }
+      this._tasks[task.label] = task;
+      this._taskSequence.push(task.label);
+    }
+
+    // Start running all the tasks scheduled. Multiple task names can be passed
+    // to execute them sequentially. Zero argument will perform all defined
+    // tasks in the order of definition.
+    run() {
+      // Display the beginning of the test suite.
+      _logPassed('# AUDIT TASK RUNNER STARTED.');
+
+      // If the argument is specified, override the default task sequence with
+      // the specified one.
+      if (arguments.length > 0) {
+        this._taskSequence = [];
+        for (let i = 0; i < arguments.length; i++) {
+          let taskLabel = arguments[i];
+          if (!this._tasks.hasOwnProperty(taskLabel)) {
+            _throwException('Audit.run:: undefined task.');
+          } else if (this._taskSequence.includes(taskLabel)) {
+            _throwException('Audit.run:: duplicate task request.');
+          } else {
+            this._taskSequence.push(taskLabel);
+          }
+        }
+      }
+
+      if (this._taskSequence.length === 0) {
+        _throwException('Audit.run:: no task to run.');
+        return;
+      }
+
+      for (let taskIndex in this._taskSequence) {
+        let task = this._tasks[this._taskSequence[taskIndex]];
+        // Some tests assume that tasks run in sequence, which is provided by
+        // promise_test().
+        promise_test((t) => task.run(t), `Executing "${task.label}"`);
+      }
+
+      // Schedule a summary report on completion.
+      promise_test(() => this._finish(), "Audit report");
+
+      // From testharness.js. The harness now need not wait for more subtests
+      // to be added.
+      _testharnessDone();
+    }
+  }
+
+  /**
+   * Load file from a given URL and pass ArrayBuffer to the following promise.
+   * @param  {String} fileUrl file URL.
+   * @return {Promise}
+   *
+   * @example
+   *   Audit.loadFileFromUrl('resources/my-sound.ogg').then((response) => {
+   *       audioContext.decodeAudioData(response).then((audioBuffer) => {
+   *           // Do something with AudioBuffer.
+   *       });
+   *   });
+   */
+  function loadFileFromUrl(fileUrl) {
+    return new Promise((resolve, reject) => {
+      let xhr = new XMLHttpRequest();
+      xhr.open('GET', fileUrl, true);
+      xhr.responseType = 'arraybuffer';
+
+      xhr.onload = () => {
+        // |status = 0| is a workaround for the run_web_test.py server. We are
+        // speculating the server quits the transaction prematurely without
+        // completing the request.
+        if (xhr.status === 200 || xhr.status === 0) {
+          resolve(xhr.response);
+        } else {
+          let errorMessage = 'loadFile: Request failed when loading ' +
+              fileUrl + '. ' + xhr.statusText + '. (status = ' + xhr.status +
+              ')';
+          if (reject) {
+            reject(errorMessage);
+          } else {
+            new Error(errorMessage);
+          }
+        }
+      };
+
+      xhr.onerror = (event) => {
+        let errorMessage =
+            'loadFile: Network failure when loading ' + fileUrl + '.';
+        if (reject) {
+          reject(errorMessage);
+        } else {
+          new Error(errorMessage);
+        }
+      };
+
+      xhr.send();
+    });
+  }
+
+  /**
+   * @class Audit
+   * @description A WebAudio layout test task manager.
+   * @example
+   *   let audit = Audit.createTaskRunner();
+   *   audit.define('first-task', function (task, should) {
+   *     should(someValue).beEqualTo(someValue);
+   *     task.done();
+   *   });
+   *   audit.run();
+   */
+  return {
+
+    /**
+     * Creates an instance of Audit task runner.
+     * @param {Object}  options                     Options for task runner.
+     * @param {Boolean} options.requireResultFile   True if the test suite
+     *                                              requires explicit text
+     *                                              comparison with the expected
+     *                                              result file.
+     */
+    createTaskRunner: function(options) {
+      if (options && options.requireResultFile == true) {
+        _logError(
+            'this test requires the explicit comparison with the ' +
+            'expected result when it runs with run_web_tests.py.');
+      }
+
+      return new TaskRunner();
+    },
+
+    /**
+     * Load file from a given URL and pass ArrayBuffer to the following promise.
+     * See |loadFileFromUrl| method for the detail.
+     */
+    loadFileFromUrl: loadFileFromUrl
+
+  };
+
+})();
diff --git a/common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/testharness.js b/common/tct-webaudio-w3c-tests/webaudio/w3c/the-audio-api/resources/testharness.js
new file mode 100644 (file)
index 0000000..e183e8c
--- /dev/null
@@ -0,0 +1,3963 @@
+/*global self*/
+/*jshint latedef: nofunc*/
+/*
+Distributed under both the W3C Test Suite License [1] and the W3C
+3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
+policies and contribution forms [3].
+
+[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
+[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
+[3] http://www.w3.org/2004/10/27-testcases
+*/
+
+/* Documentation: https://web-platform-tests.org/writing-tests/testharness-api.html
+ * (../docs/_writing-tests/testharness-api.md) */
+
+(function (global_scope)
+{
+    var debug = false;
+    // default timeout is 10 seconds, test can override if needed
+    var settings = {
+        output:true,
+        harness_timeout:{
+            "normal":10000,
+            "long":60000
+        },
+        test_timeout:null,
+        message_events: ["start", "test_state", "result", "completion"]
+    };
+
+    var xhtml_ns = "http://www.w3.org/1999/xhtml";
+
+    /*
+     * TestEnvironment is an abstraction for the environment in which the test
+     * harness is used. Each implementation of a test environment has to provide
+     * the following interface:
+     *
+     * interface TestEnvironment {
+     *   // Invoked after the global 'tests' object has been created and it's
+     *   // safe to call add_*_callback() to register event handlers.
+     *   void on_tests_ready();
+     *
+     *   // Invoked after setup() has been called to notify the test environment
+     *   // of changes to the test harness properties.
+     *   void on_new_harness_properties(object properties);
+     *
+     *   // Should return a new unique default test name.
+     *   DOMString next_default_test_name();
+     *
+     *   // Should return the test harness timeout duration in milliseconds.
+     *   float test_timeout();
+     * };
+     */
+
+    /*
+     * A test environment with a DOM. The global object is 'window'. By default
+     * test results are displayed in a table. Any parent windows receive
+     * callbacks or messages via postMessage() when test events occur. See
+     * apisample11.html and apisample12.html.
+     */
+    function WindowTestEnvironment() {
+        this.name_counter = 0;
+        this.window_cache = null;
+        this.output_handler = null;
+        this.all_loaded = false;
+        var this_obj = this;
+        this.message_events = [];
+        this.dispatched_messages = [];
+
+        this.message_functions = {
+            start: [add_start_callback, remove_start_callback,
+                    function (properties) {
+                        this_obj._dispatch("start_callback", [properties],
+                                           {type: "start", properties: properties});
+                    }],
+
+            test_state: [add_test_state_callback, remove_test_state_callback,
+                         function(test) {
+                             this_obj._dispatch("test_state_callback", [test],
+                                                {type: "test_state",
+                                                 test: test.structured_clone()});
+                         }],
+            result: [add_result_callback, remove_result_callback,
+                     function (test) {
+                         this_obj.output_handler.show_status();
+                         this_obj._dispatch("result_callback", [test],
+                                            {type: "result",
+                                             test: test.structured_clone()});
+                     }],
+            completion: [add_completion_callback, remove_completion_callback,
+                         function (tests, harness_status) {
+                             var cloned_tests = map(tests, function(test) {
+                                 return test.structured_clone();
+                             });
+                             this_obj._dispatch("completion_callback", [tests, harness_status],
+                                                {type: "complete",
+                                                 tests: cloned_tests,
+                                                 status: harness_status.structured_clone()});
+                         }]
+        }
+
+        on_event(window, 'load', function() {
+            this_obj.all_loaded = true;
+        });
+
+        on_event(window, 'message', function(event) {
+            if (event.data && event.data.type === "getmessages" && event.source) {
+                // A window can post "getmessages" to receive a duplicate of every
+                // message posted by this environment so far. This allows subscribers
+                // from fetch_tests_from_window to 'catch up' to the current state of
+                // this environment.
+                for (var i = 0; i < this_obj.dispatched_messages.length; ++i)
+                {
+                    event.source.postMessage(this_obj.dispatched_messages[i], "*");
+                }
+            }
+        });
+    }
+
+    WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
+        this.dispatched_messages.push(message_arg);
+        this._forEach_windows(
+                function(w, same_origin) {
+                    if (same_origin) {
+                        try {
+                            var has_selector = selector in w;
+                        } catch(e) {
+                            // If document.domain was set at some point same_origin can be
+                            // wrong and the above will fail.
+                            has_selector = false;
+                        }
+                        if (has_selector) {
+                            try {
+                                w[selector].apply(undefined, callback_args);
+                            } catch (e) {
+                                if (debug) {
+                                    throw e;
+                                }
+                            }
+                        }
+                    }
+                    if (supports_post_message(w) && w !== self) {
+                        w.postMessage(message_arg, "*");
+                    }
+                });
+    };
+
+    WindowTestEnvironment.prototype._forEach_windows = function(callback) {
+        // Iterate over the windows [self ... top, opener]. The callback is passed
+        // two objects, the first one is the window object itself, the second one
+        // is a boolean indicating whether or not it's on the same origin as the
+        // current window.
+        var cache = this.window_cache;
+        if (!cache) {
+            cache = [[self, true]];
+            var w = self;
+            var i = 0;
+            var so;
+            while (w != w.parent) {
+                w = w.parent;
+                so = is_same_origin(w);
+                cache.push([w, so]);
+                i++;
+            }
+            w = window.opener;
+            if (w) {
+                cache.push([w, is_same_origin(w)]);
+            }
+            this.window_cache = cache;
+        }
+
+        forEach(cache,
+                function(a) {
+                    callback.apply(null, a);
+                });
+    };
+
+    WindowTestEnvironment.prototype.on_tests_ready = function() {
+        var output = new Output();
+        this.output_handler = output;
+
+        var this_obj = this;
+
+        add_start_callback(function (properties) {
+            this_obj.output_handler.init(properties);
+        });
+
+        add_test_state_callback(function(test) {
+            this_obj.output_handler.show_status();
+        });
+
+        add_result_callback(function (test) {
+            this_obj.output_handler.show_status();
+        });
+
+        add_completion_callback(function (tests, harness_status) {
+            this_obj.output_handler.show_results(tests, harness_status);
+        });
+        this.setup_messages(settings.message_events);
+    };
+
+    WindowTestEnvironment.prototype.setup_messages = function(new_events) {
+        var this_obj = this;
+        forEach(settings.message_events, function(x) {
+            var current_dispatch = this_obj.message_events.indexOf(x) !== -1;
+            var new_dispatch = new_events.indexOf(x) !== -1;
+            if (!current_dispatch && new_dispatch) {
+                this_obj.message_functions[x][0](this_obj.message_functions[x][2]);
+            } else if (current_dispatch && !new_dispatch) {
+                this_obj.message_functions[x][1](this_obj.message_functions[x][2]);
+            }
+        });
+        this.message_events = new_events;
+    }
+
+    WindowTestEnvironment.prototype.next_default_test_name = function() {
+        var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+        this.name_counter++;
+        return get_title() + suffix;
+    };
+
+    WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
+        this.output_handler.setup(properties);
+        if (properties.hasOwnProperty("message_events")) {
+            this.setup_messages(properties.message_events);
+        }
+    };
+
+    WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+        on_event(window, 'load', callback);
+    };
+
+    WindowTestEnvironment.prototype.test_timeout = function() {
+        var metas = document.getElementsByTagName("meta");
+        for (var i = 0; i < metas.length; i++) {
+            if (metas[i].name == "timeout") {
+                if (metas[i].content == "long") {
+                    return settings.harness_timeout.long;
+                }
+                break;
+            }
+        }
+        return settings.harness_timeout.normal;
+    };
+
+    /*
+     * Base TestEnvironment implementation for a generic web worker.
+     *
+     * Workers accumulate test results. One or more clients can connect and
+     * retrieve results from a worker at any time.
+     *
+     * WorkerTestEnvironment supports communicating with a client via a
+     * MessagePort.  The mechanism for determining the appropriate MessagePort
+     * for communicating with a client depends on the type of worker and is
+     * implemented by the various specializations of WorkerTestEnvironment
+     * below.
+     *
+     * A client document using testharness can use fetch_tests_from_worker() to
+     * retrieve results from a worker. See apisample16.html.
+     */
+    function WorkerTestEnvironment() {
+        this.name_counter = 0;
+        this.all_loaded = true;
+        this.message_list = [];
+        this.message_ports = [];
+    }
+
+    WorkerTestEnvironment.prototype._dispatch = function(message) {
+        this.message_list.push(message);
+        for (var i = 0; i < this.message_ports.length; ++i)
+        {
+            this.message_ports[i].postMessage(message);
+        }
+    };
+
+    // The only requirement is that port has a postMessage() method. It doesn't
+    // have to be an instance of a MessagePort, and often isn't.
+    WorkerTestEnvironment.prototype._add_message_port = function(port) {
+        this.message_ports.push(port);
+        for (var i = 0; i < this.message_list.length; ++i)
+        {
+            port.postMessage(this.message_list[i]);
+        }
+    };
+
+    WorkerTestEnvironment.prototype.next_default_test_name = function() {
+        var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+        this.name_counter++;
+        return get_title() + suffix;
+    };
+
+    WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+    WorkerTestEnvironment.prototype.on_tests_ready = function() {
+        var this_obj = this;
+        add_start_callback(
+                function(properties) {
+                    this_obj._dispatch({
+                        type: "start",
+                        properties: properties,
+                    });
+                });
+        add_test_state_callback(
+                function(test) {
+                    this_obj._dispatch({
+                        type: "test_state",
+                        test: test.structured_clone()
+                    });
+                });
+        add_result_callback(
+                function(test) {
+                    this_obj._dispatch({
+                        type: "result",
+                        test: test.structured_clone()
+                    });
+                });
+        add_completion_callback(
+                function(tests, harness_status) {
+                    this_obj._dispatch({
+                        type: "complete",
+                        tests: map(tests,
+                            function(test) {
+                                return test.structured_clone();
+                            }),
+                        status: harness_status.structured_clone()
+                    });
+                });
+    };
+
+    WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {};
+
+    WorkerTestEnvironment.prototype.test_timeout = function() {
+        // Tests running in a worker don't have a default timeout. I.e. all
+        // worker tests behave as if settings.explicit_timeout is true.
+        return null;
+    };
+
+    /*
+     * Dedicated web workers.
+     * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
+     *
+     * This class is used as the test_environment when testharness is running
+     * inside a dedicated worker.
+     */
+    function DedicatedWorkerTestEnvironment() {
+        WorkerTestEnvironment.call(this);
+        // self is an instance of DedicatedWorkerGlobalScope which exposes
+        // a postMessage() method for communicating via the message channel
+        // established when the worker is created.
+        this._add_message_port(self);
+    }
+    DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+    DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+        WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+        // In the absence of an onload notification, we a require dedicated
+        // workers to explicitly signal when the tests are done.
+        tests.wait_for_finish = true;
+    };
+
+    /*
+     * Shared web workers.
+     * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
+     *
+     * This class is used as the test_environment when testharness is running
+     * inside a shared web worker.
+     */
+    function SharedWorkerTestEnvironment() {
+        WorkerTestEnvironment.call(this);
+        var this_obj = this;
+        // Shared workers receive message ports via the 'onconnect' event for
+        // each connection.
+        self.addEventListener("connect",
+                function(message_event) {
+                    this_obj._add_message_port(message_event.source);
+                }, false);
+    }
+    SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+    SharedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+        WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+        // In the absence of an onload notification, we a require shared
+        // workers to explicitly signal when the tests are done.
+        tests.wait_for_finish = true;
+    };
+
+    /*
+     * Service workers.
+     * http://www.w3.org/TR/service-workers/
+     *
+     * This class is used as the test_environment when testharness is running
+     * inside a service worker.
+     */
+    function ServiceWorkerTestEnvironment() {
+        WorkerTestEnvironment.call(this);
+        this.all_loaded = false;
+        this.on_loaded_callback = null;
+        var this_obj = this;
+        self.addEventListener("message",
+                function(event) {
+                    if (event.data && event.data.type && event.data.type === "connect") {
+                        if (event.ports && event.ports[0]) {
+                            // If a MessageChannel was passed, then use it to
+                            // send results back to the main window.  This
+                            // allows the tests to work even if the browser
+                            // does not fully support MessageEvent.source in
+                            // ServiceWorkers yet.
+                            this_obj._add_message_port(event.ports[0]);
+                            event.ports[0].start();
+                        } else {
+                            // If there is no MessageChannel, then attempt to
+                            // use the MessageEvent.source to send results
+                            // back to the main window.
+                            this_obj._add_message_port(event.source);
+                        }
+                    }
+                }, false);
+
+        // The oninstall event is received after the service worker script and
+        // all imported scripts have been fetched and executed. It's the
+        // equivalent of an onload event for a document. All tests should have
+        // been added by the time this event is received, thus it's not
+        // necessary to wait until the onactivate event. However, tests for
+        // installed service workers need another event which is equivalent to
+        // the onload event because oninstall is fired only on installation. The
+        // onmessage event is used for that purpose since tests using
+        // testharness.js should ask the result to its service worker by
+        // PostMessage. If the onmessage event is triggered on the service
+        // worker's context, that means the worker's script has been evaluated.
+        on_event(self, "install", on_all_loaded);
+        on_event(self, "message", on_all_loaded);
+        function on_all_loaded() {
+            if (this_obj.all_loaded)
+                return;
+            this_obj.all_loaded = true;
+            if (this_obj.on_loaded_callback) {
+              this_obj.on_loaded_callback();
+            }
+        }
+    }
+
+    ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+    ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+        if (this.all_loaded) {
+            callback();
+        } else {
+            this.on_loaded_callback = callback;
+        }
+    };
+
+    /*
+     * JavaScript shells.
+     *
+     * This class is used as the test_environment when testharness is running
+     * inside a JavaScript shell.
+     */
+    function ShellTestEnvironment() {
+        this.name_counter = 0;
+        this.all_loaded = false;
+        this.on_loaded_callback = null;
+        Promise.resolve().then(function() {
+            this.all_loaded = true
+            if (this.on_loaded_callback) {
+                this.on_loaded_callback();
+            }
+        }.bind(this));
+        this.message_list = [];
+        this.message_ports = [];
+    }
+
+    ShellTestEnvironment.prototype.next_default_test_name = function() {
+        var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+        this.name_counter++;
+        return "Untitled" + suffix;
+    };
+
+    ShellTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+    ShellTestEnvironment.prototype.on_tests_ready = function() {};
+
+    ShellTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+        if (this.all_loaded) {
+            callback();
+        } else {
+            this.on_loaded_callback = callback;
+        }
+    };
+
+    ShellTestEnvironment.prototype.test_timeout = function() {
+        // Tests running in a shell don't have a default timeout, so behave as
+        // if settings.explicit_timeout is true.
+        return null;
+    };
+
+    function create_test_environment() {
+        if ('document' in global_scope) {
+            return new WindowTestEnvironment();
+        }
+        if ('DedicatedWorkerGlobalScope' in global_scope &&
+            global_scope instanceof DedicatedWorkerGlobalScope) {
+            return new DedicatedWorkerTestEnvironment();
+        }
+        if ('SharedWorkerGlobalScope' in global_scope &&
+            global_scope instanceof SharedWorkerGlobalScope) {
+            return new SharedWorkerTestEnvironment();
+        }
+        if ('ServiceWorkerGlobalScope' in global_scope &&
+            global_scope instanceof ServiceWorkerGlobalScope) {
+            return new ServiceWorkerTestEnvironment();
+        }
+        if ('WorkerGlobalScope' in global_scope &&
+            global_scope instanceof WorkerGlobalScope) {
+            return new DedicatedWorkerTestEnvironment();
+        }
+
+        if (!('location' in global_scope)) {
+            return new ShellTestEnvironment();
+        }
+
+        throw new Error("Unsupported test environment");
+    }
+
+    var test_environment = create_test_environment();
+
+    function is_shared_worker(worker) {
+        return 'SharedWorker' in global_scope && worker instanceof SharedWorker;
+    }
+
+    function is_service_worker(worker) {
+        // The worker object may be from another execution context,
+        // so do not use instanceof here.
+        return 'ServiceWorker' in global_scope &&
+            Object.prototype.toString.call(worker) == '[object ServiceWorker]';
+    }
+
+    /*
+     * API functions
+     */
+    function test(func, name, properties)
+    {
+        if (tests.promise_setup_called) {
+            tests.status.status = tests.status.ERROR;
+            tests.status.message = '`test` invoked after `promise_setup`';
+            tests.complete();
+        }
+        var test_name = name ? name : test_environment.next_default_test_name();
+        var test_obj = new Test(test_name, properties);
+        var value = test_obj.step(func, test_obj, test_obj);
+
+        if (value !== undefined) {
+            var msg = "Test named \"" + test_name +
+                "\" inappropriately returned a value";
+
+            try {
+                if (value && value.hasOwnProperty("then")) {
+                    msg += ", consider using `promise_test` instead";
+                }
+            } catch (err) {}
+
+            tests.status.status = tests.status.ERROR;
+            tests.status.message = msg;
+        }
+
+        if (test_obj.phase === test_obj.phases.STARTED) {
+            test_obj.done();
+        }
+    }
+
+    function async_test(func, name, properties)
+    {
+        if (tests.promise_setup_called) {
+            tests.status.status = tests.status.ERROR;
+            tests.status.message = '`async_test` invoked after `promise_setup`';
+            tests.complete();
+        }
+        if (typeof func !== "function") {
+            properties = name;
+            name = func;
+            func = null;
+        }
+        var test_name = name ? name : test_environment.next_default_test_name();
+        var test_obj = new Test(test_name, properties);
+        if (func) {
+            test_obj.step(func, test_obj, test_obj);
+        }
+        return test_obj;
+    }
+
+    function promise_test(func, name, properties) {
+        if (typeof func !== "function") {
+            properties = name;
+            name = func;
+            func = null;
+        }
+        var test_name = name ? name : test_environment.next_default_test_name();
+        var test = new Test(test_name, properties);
+        test._is_promise_test = true;
+
+        // If there is no promise tests queue make one.
+        if (!tests.promise_tests) {
+            tests.promise_tests = Promise.resolve();
+        }
+        tests.promise_tests = tests.promise_tests.then(function() {
+            return new Promise(function(resolve) {
+                var promise = test.step(func, test, test);
+
+                test.step(function() {
+                    assert(!!promise, "promise_test", null,
+                           "test body must return a 'thenable' object (received ${value})",
+                           {value:promise});
+                    assert(typeof promise.then === "function", "promise_test", null,
+                           "test body must return a 'thenable' object (received an object with no `then` method)",
+                           null);
+                });
+
+                // Test authors may use the `step` method within a
+                // `promise_test` even though this reflects a mixture of
+                // asynchronous control flow paradigms. The "done" callback
+                // should be registered prior to the resolution of the
+                // user-provided Promise to avoid timeouts in cases where the
+                // Promise does not settle but a `step` function has thrown an
+                // error.
+                add_test_done_callback(test, resolve);
+
+                Promise.resolve(promise)
+                    .catch(test.step_func(
+                        function(value) {
+                            if (value instanceof AssertionError) {
+                                throw value;
+                            }
+                            assert(false, "promise_test", null,
+                                   "Unhandled rejection with value: ${value}", {value:value});
+                        }))
+                    .then(function() {
+                        test.done();
+                    });
+                });
+        });
+    }
+
+    function promise_rejects(test, expected, promise, description) {
+        return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) {
+            assert_throws(expected, function() { throw e }, description);
+        });
+    }
+
+    function promise_rejects_js(test, expected, promise, description) {
+        return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) {
+            assert_throws_js_impl(expected, function() { throw e },
+                                  description, "promise_reject_js");
+        });
+    }
+
+    function promise_rejects_dom(test, expected, promise, description) {
+        return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) {
+            assert_throws_dom_impl(expected, function() { throw e },
+                                   description, "promise_rejects_dom");
+        });
+    }
+
+    function promise_rejects_exactly(test, expected, promise, description) {
+        return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) {
+            assert_throws_exactly_impl(expected, function() { throw e },
+                                       description, "promise_rejects_exactly");
+        });
+    }
+
+    /**
+     * This constructor helper allows DOM events to be handled using Promises,
+     * which can make it a lot easier to test a very specific series of events,
+     * including ensuring that unexpected events are not fired at any point.
+     */
+    function EventWatcher(test, watchedNode, eventTypes, timeoutPromise)
+    {
+        if (typeof eventTypes == 'string') {
+            eventTypes = [eventTypes];
+        }
+
+        var waitingFor = null;
+
+        // This is null unless we are recording all events, in which case it
+        // will be an Array object.
+        var recordedEvents = null;
+
+        var eventHandler = test.step_func(function(evt) {
+            assert_true(!!waitingFor,
+                        'Not expecting event, but got ' + evt.type + ' event');
+            assert_equals(evt.type, waitingFor.types[0],
+                          'Expected ' + waitingFor.types[0] + ' event, but got ' +
+                          evt.type + ' event instead');
+
+            if (Array.isArray(recordedEvents)) {
+                recordedEvents.push(evt);
+            }
+
+            if (waitingFor.types.length > 1) {
+                // Pop first event from array
+                waitingFor.types.shift();
+                return;
+            }
+            // We need to null out waitingFor before calling the resolve function
+            // since the Promise's resolve handlers may call wait_for() which will
+            // need to set waitingFor.
+            var resolveFunc = waitingFor.resolve;
+            waitingFor = null;
+            // Likewise, we should reset the state of recordedEvents.
+            var result = recordedEvents || evt;
+            recordedEvents = null;
+            resolveFunc(result);
+        });
+
+        for (var i = 0; i < eventTypes.length; i++) {
+            watchedNode.addEventListener(eventTypes[i], eventHandler, false);
+        }
+
+        /**
+         * Returns a Promise that will resolve after the specified event or
+         * series of events has occurred.
+         *
+         * @param options An optional options object. If the 'record' property
+         *                on this object has the value 'all', when the Promise
+         *                returned by this function is resolved,  *all* Event
+         *                objects that were waited for will be returned as an
+         *                array.
+         *
+         * For example,
+         *
+         * ```js
+         * const watcher = new EventWatcher(t, div, [ 'animationstart',
+         *                                            'animationiteration',
+         *                                            'animationend' ]);
+         * return watcher.wait_for([ 'animationstart', 'animationend' ],
+         *                         { record: 'all' }).then(evts => {
+         *   assert_equals(evts[0].elapsedTime, 0.0);
+         *   assert_equals(evts[1].elapsedTime, 2.0);
+         * });
+         * ```
+         */
+        this.wait_for = function(types, options) {
+            if (waitingFor) {
+                return Promise.reject('Already waiting for an event or events');
+            }
+            if (typeof types == 'string') {
+                types = [types];
+            }
+            if (options && options.record && options.record === 'all') {
+                recordedEvents = [];
+            }
+            return new Promise(function(resolve, reject) {
+                var timeout = test.step_func(function() {
+                    // If the timeout fires after the events have been received
+                    // or during a subsequent call to wait_for, ignore it.
+                    if (!waitingFor || waitingFor.resolve !== resolve)
+                        return;
+
+                    // This should always fail, otherwise we should have
+                    // resolved the promise.
+                    assert_true(waitingFor.types.length == 0,
+                                'Timed out waiting for ' + waitingFor.types.join(', '));
+                    var result = recordedEvents;
+                    recordedEvents = null;
+                    var resolveFunc = waitingFor.resolve;
+                    waitingFor = null;
+                    resolveFunc(result);
+                });
+
+                if (timeoutPromise) {
+                    timeoutPromise().then(timeout);
+                }
+
+                waitingFor = {
+                    types: types,
+                    resolve: resolve,
+                    reject: reject
+                };
+            });
+        };
+
+        function stop_watching() {
+            for (var i = 0; i < eventTypes.length; i++) {
+                watchedNode.removeEventListener(eventTypes[i], eventHandler, false);
+            }
+        };
+
+        test._add_cleanup(stop_watching);
+
+        return this;
+    }
+    expose(EventWatcher, 'EventWatcher');
+
+    function setup(func_or_properties, maybe_properties)
+    {
+        var func = null;
+        var properties = {};
+        if (arguments.length === 2) {
+            func = func_or_properties;
+            properties = maybe_properties;
+        } else if (func_or_properties instanceof Function) {
+            func = func_or_properties;
+        } else {
+            properties = func_or_properties;
+        }
+        tests.setup(func, properties);
+        test_environment.on_new_harness_properties(properties);
+    }
+
+    function promise_setup(func, maybe_properties)
+    {
+        if (typeof func !== "function") {
+            tests.set_status(tests.status.ERROR,
+                             "promise_test invoked without a function");
+            tests.complete();
+            return;
+        }
+        tests.promise_setup_called = true;
+
+        if (!tests.promise_tests) {
+            tests.promise_tests = Promise.resolve();
+        }
+
+        tests.promise_tests = tests.promise_tests
+            .then(function()
+                  {
+                      var properties = maybe_properties || {};
+                      var result;
+
+                      tests.setup(null, properties);
+                      result = func();
+                      test_environment.on_new_harness_properties(properties);
+
+                      if (!result || typeof result.then !== "function") {
+                          throw "Non-thenable returned by function passed to `promise_setup`";
+                      }
+                      return result;
+                  })
+            .catch(function(e)
+                   {
+                       tests.set_status(tests.status.ERROR,
+                                        String(e),
+                                        e && e.stack);
+                       tests.complete();
+                   });
+    }
+
+    function done() {
+        if (tests.tests.length === 0) {
+            // `done` is invoked after handling uncaught exceptions, so if the
+            // harness status is already set, the corresponding message is more
+            // descriptive than the generic message defined here.
+            if (tests.status.status === null) {
+                tests.status.status = tests.status.ERROR;
+                tests.status.message = "done() was called without first defining any tests: " + new Error().stack;
+            }
+
+            tests.complete();
+            return;
+        }
+        if (tests.file_is_test) {
+            // file is test files never have asynchronous cleanup logic,
+            // meaning the fully-synchronous `done` function can be used here.
+            tests.tests[0].done();
+        }
+        tests.end_wait();
+    }
+
+    function generate_tests(func, args, properties) {
+        forEach(args, function(x, i)
+                {
+                    var name = x[0];
+                    test(function()
+                         {
+                             func.apply(this, x.slice(1));
+                         },
+                         name,
+                         Array.isArray(properties) ? properties[i] : properties);
+                });
+    }
+
+    /*
+     * Register a function as a DOM event listener to the given object for the
+     * event bubbling phase.
+     *
+     * This function was deprecated in November of 2019.
+     */
+    function on_event(object, event, callback)
+    {
+        object.addEventListener(event, callback, false);
+    }
+
+    function step_timeout(f, t) {
+        var outer_this = this;
+        var args = Array.prototype.slice.call(arguments, 2);
+        return setTimeout(function() {
+            f.apply(outer_this, args);
+        }, t * tests.timeout_multiplier);
+    }
+
+    expose(test, 'test');
+    expose(async_test, 'async_test');
+    expose(promise_test, 'promise_test');
+    expose(promise_rejects, 'promise_rejects');
+    expose(promise_rejects_js, 'promise_rejects_js');
+    expose(promise_rejects_dom, 'promise_rejects_dom');
+    expose(promise_rejects_exactly, 'promise_rejects_exactly');
+    expose(generate_tests, 'generate_tests');
+    expose(setup, 'setup');
+    expose(promise_setup, 'promise_setup');
+    expose(done, 'done');
+    expose(on_event, 'on_event');
+    expose(step_timeout, 'step_timeout');
+
+    /*
+     * Return a string truncated to the given length, with ... added at the end
+     * if it was longer.
+     */
+    function truncate(s, len)
+    {
+        if (s.length > len) {
+            return s.substring(0, len - 3) + "...";
+        }
+        return s;
+    }
+
+    /*
+     * Return true if object is probably a Node object.
+     */
+    function is_node(object)
+    {
+        // I use duck-typing instead of instanceof, because
+        // instanceof doesn't work if the node is from another window (like an
+        // iframe's contentWindow):
+        // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
+        try {
+            var has_node_properties = ("nodeType" in object &&
+                                       "nodeName" in object &&
+                                       "nodeValue" in object &&
+                                       "childNodes" in object);
+        } catch (e) {
+            // We're probably cross-origin, which means we aren't a node
+            return false;
+        }
+
+        if (has_node_properties) {
+            try {
+                object.nodeType;
+            } catch (e) {
+                // The object is probably Node.prototype or another prototype
+                // object that inherits from it, and not a Node instance.
+                return false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    var replacements = {
+        "0": "0",
+        "1": "x01",
+        "2": "x02",
+        "3": "x03",
+        "4": "x04",
+        "5": "x05",
+        "6": "x06",
+        "7": "x07",
+        "8": "b",
+        "9": "t",
+        "10": "n",
+        "11": "v",
+        "12": "f",
+        "13": "r",
+        "14": "x0e",
+        "15": "x0f",
+        "16": "x10",
+        "17": "x11",
+        "18": "x12",
+        "19": "x13",
+        "20": "x14",
+        "21": "x15",
+        "22": "x16",
+        "23": "x17",
+        "24": "x18",
+        "25": "x19",
+        "26": "x1a",
+        "27": "x1b",
+        "28": "x1c",
+        "29": "x1d",
+        "30": "x1e",
+        "31": "x1f",
+        "0xfffd": "ufffd",
+        "0xfffe": "ufffe",
+        "0xffff": "uffff",
+    };
+
+    /*
+     * Convert a value to a nice, human-readable string
+     */
+    function format_value(val, seen)
+    {
+        if (!seen) {
+            seen = [];
+        }
+        if (typeof val === "object" && val !== null) {
+            if (seen.indexOf(val) >= 0) {
+                return "[...]";
+            }
+            seen.push(val);
+        }
+        if (Array.isArray(val)) {
+            let output = "[";
+            if (val.beginEllipsis !== undefined) {
+                output += "…, ";
+            }
+            output += val.map(function(x) {return format_value(x, seen);}).join(", ");
+            if (val.endEllipsis !== undefined) {
+                output += ", â€¦";
+            }
+            return output + "]";
+        }
+
+        switch (typeof val) {
+        case "string":
+            val = val.replace(/\\/g, "\\\\");
+            for (var p in replacements) {
+                var replace = "\\" + replacements[p];
+                val = val.replace(RegExp(String.fromCharCode(p), "g"), replace);
+            }
+            return '"' + val.replace(/"/g, '\\"') + '"';
+        case "boolean":
+        case "undefined":
+            return String(val);
+        case "number":
+            // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
+            // special-case.
+            if (val === -0 && 1/val === -Infinity) {
+                return "-0";
+            }
+            return String(val);
+        case "object":
+            if (val === null) {
+                return "null";
+            }
+
+            // Special-case Node objects, since those come up a lot in my tests.  I
+            // ignore namespaces.
+            if (is_node(val)) {
+                switch (val.nodeType) {
+                case Node.ELEMENT_NODE:
+                    var ret = "<" + val.localName;
+                    for (var i = 0; i < val.attributes.length; i++) {
+                        ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
+                    }
+                    ret += ">" + val.innerHTML + "</" + val.localName + ">";
+                    return "Element node " + truncate(ret, 60);
+                case Node.TEXT_NODE:
+                    return 'Text node "' + truncate(val.data, 60) + '"';
+                case Node.PROCESSING_INSTRUCTION_NODE:
+                    return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
+                case Node.COMMENT_NODE:
+                    return "Comment node <!--" + truncate(val.data, 60) + "-->";
+                case Node.DOCUMENT_NODE:
+                    return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+                case Node.DOCUMENT_TYPE_NODE:
+                    return "DocumentType node";
+                case Node.DOCUMENT_FRAGMENT_NODE:
+                    return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+                default:
+                    return "Node object of unknown type";
+                }
+            }
+
+        /* falls through */
+        default:
+            try {
+                return typeof val + ' "' + truncate(String(val), 1000) + '"';
+            } catch(e) {
+                return ("[stringifying object threw " + String(e) +
+                        " with type " + String(typeof e) + "]");
+            }
+        }
+    }
+    expose(format_value, "format_value");
+
+    /*
+     * Assertions
+     */
+
+    function assert_true(actual, description)
+    {
+        assert(actual === true, "assert_true", description,
+                                "expected true got ${actual}", {actual:actual});
+    }
+    expose(assert_true, "assert_true");
+
+    function assert_false(actual, description)
+    {
+        assert(actual === false, "assert_false", description,
+                                 "expected false got ${actual}", {actual:actual});
+    }
+    expose(assert_false, "assert_false");
+
+    function same_value(x, y) {
+        if (y !== y) {
+            //NaN case
+            return x !== x;
+        }
+        if (x === 0 && y === 0) {
+            //Distinguish +0 and -0
+            return 1/x === 1/y;
+        }
+        return x === y;
+    }
+
+    function assert_equals(actual, expected, description)
+    {
+         /*
+          * Test if two primitives are equal or two objects
+          * are the same object
+          */
+        if (typeof actual != typeof expected) {
+            assert(false, "assert_equals", description,
+                          "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
+                          {expected:expected, actual:actual});
+            return;
+        }
+        assert(same_value(actual, expected), "assert_equals", description,
+                                             "expected ${expected} but got ${actual}",
+                                             {expected:expected, actual:actual});
+    }
+    expose(assert_equals, "assert_equals");
+
+    function assert_not_equals(actual, expected, description)
+    {
+         /*
+          * Test if two primitives are unequal or two objects
+          * are different objects
+          */
+        assert(!same_value(actual, expected), "assert_not_equals", description,
+                                              "got disallowed value ${actual}",
+                                              {actual:actual});
+    }
+    expose(assert_not_equals, "assert_not_equals");
+
+    function assert_in_array(actual, expected, description)
+    {
+        assert(expected.indexOf(actual) != -1, "assert_in_array", description,
+                                               "value ${actual} not in array ${expected}",
+                                               {actual:actual, expected:expected});
+    }
+    expose(assert_in_array, "assert_in_array");
+
+    function assert_object_equals(actual, expected, description)
+    {
+         assert(typeof actual === "object" && actual !== null, "assert_object_equals", description,
+                                                               "value is ${actual}, expected object",
+                                                               {actual: actual});
+         //This needs to be improved a great deal
+         function check_equal(actual, expected, stack)
+         {
+             stack.push(actual);
+
+             var p;
+             for (p in actual) {
+                 assert(expected.hasOwnProperty(p), "assert_object_equals", description,
+                                                    "unexpected property ${p}", {p:p});
+
+                 if (typeof actual[p] === "object" && actual[p] !== null) {
+                     if (stack.indexOf(actual[p]) === -1) {
+                         check_equal(actual[p], expected[p], stack);
+                     }
+                 } else {
+                     assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
+                                                       "property ${p} expected ${expected} got ${actual}",
+                                                       {p:p, expected:expected, actual:actual});
+                 }
+             }
+             for (p in expected) {
+                 assert(actual.hasOwnProperty(p),
+                        "assert_object_equals", description,
+                        "expected property ${p} missing", {p:p});
+             }
+             stack.pop();
+         }
+         check_equal(actual, expected, []);
+    }
+    expose(assert_object_equals, "assert_object_equals");
+
+    function assert_array_equals(actual, expected, description)
+    {
+        const max_array_length = 20;
+        function shorten_array(arr, offset = 0) {
+            // Make ", â€¦" only show up when it would likely reduce the length, not accounting for
+            // fonts.
+            if (arr.length < max_array_length + 2) {
+                return arr;
+            }
+            // By default we want half the elements after the offset and half before
+            // But if that takes us past the end of the array, we have more before, and
+            // if it takes us before the start we have more after.
+            const length_after_offset = Math.floor(max_array_length / 2);
+            let upper_bound = Math.min(length_after_offset + offset, arr.length);
+            const lower_bound = Math.max(upper_bound - max_array_length, 0);
+
+            if (lower_bound === 0) {
+                upper_bound = max_array_length;
+            }
+
+            const output = arr.slice(lower_bound, upper_bound);
+            if (lower_bound > 0) {
+                output.beginEllipsis = true;
+            }
+            if (upper_bound < arr.length) {
+                output.endEllipsis = true;
+            }
+            return output;
+        }
+
+        assert(typeof actual === "object" && actual !== null && "length" in actual,
+               "assert_array_equals", description,
+               "value is ${actual}, expected array",
+               {actual:actual});
+        assert(actual.length === expected.length,
+               "assert_array_equals", description,
+               "lengths differ, expected array ${expected} length ${expectedLength}, got ${actual} length ${actualLength}",
+               {expected:shorten_array(expected, expected.length - 1), expectedLength:expected.length,
+                actual:shorten_array(actual, actual.length - 1), actualLength:actual.length
+               });
+
+        for (var i = 0; i < actual.length; i++) {
+            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+                   "assert_array_equals", description,
+                   "expected property ${i} to be ${expected} but was ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+                   {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+                    actual:actual.hasOwnProperty(i) ? "present" : "missing",
+                    arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+            assert(same_value(expected[i], actual[i]),
+                   "assert_array_equals", description,
+                   "expected property ${i} to be ${expected} but got ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+                   {i:i, expected:expected[i], actual:actual[i],
+                    arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+        }
+    }
+    expose(assert_array_equals, "assert_array_equals");
+
+    function assert_array_approx_equals(actual, expected, epsilon, description)
+    {
+        /*
+         * Test if two primitive arrays are equal within +/- epsilon
+         */
+        assert(actual.length === expected.length,
+               "assert_array_approx_equals", description,
+               "lengths differ, expected ${expected} got ${actual}",
+               {expected:expected.length, actual:actual.length});
+
+        for (var i = 0; i < actual.length; i++) {
+            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+                   "assert_array_approx_equals", description,
+                   "property ${i}, property expected to be ${expected} but was ${actual}",
+                   {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+                    actual:actual.hasOwnProperty(i) ? "present" : "missing"});
+            assert(typeof actual[i] === "number",
+                   "assert_array_approx_equals", description,
+                   "property ${i}, expected a number but got a ${type_actual}",
+                   {i:i, type_actual:typeof actual[i]});
+            assert(Math.abs(actual[i] - expected[i]) <= epsilon,
+                   "assert_array_approx_equals", description,
+                   "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}",
+                   {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon});
+        }
+    }
+    expose(assert_array_approx_equals, "assert_array_approx_equals");
+
+    function assert_approx_equals(actual, expected, epsilon, description)
+    {
+        /*
+         * Test if two primitive numbers are equal within +/- epsilon
+         */
+        assert(typeof actual === "number",
+               "assert_approx_equals", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(Math.abs(actual - expected) <= epsilon,
+               "assert_approx_equals", description,
+               "expected ${expected} +/- ${epsilon} but got ${actual}",
+               {expected:expected, actual:actual, epsilon:epsilon});
+    }
+    expose(assert_approx_equals, "assert_approx_equals");
+
+    function assert_less_than(actual, expected, description)
+    {
+        /*
+         * Test if a primitive number is less than another
+         */
+        assert(typeof actual === "number",
+               "assert_less_than", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual < expected,
+               "assert_less_than", description,
+               "expected a number less than ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    }
+    expose(assert_less_than, "assert_less_than");
+
+    function assert_greater_than(actual, expected, description)
+    {
+        /*
+         * Test if a primitive number is greater than another
+         */
+        assert(typeof actual === "number",
+               "assert_greater_than", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual > expected,
+               "assert_greater_than", description,
+               "expected a number greater than ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    }
+    expose(assert_greater_than, "assert_greater_than");
+
+    function assert_between_exclusive(actual, lower, upper, description)
+    {
+        /*
+         * Test if a primitive number is between two others
+         */
+        assert(typeof actual === "number",
+               "assert_between_exclusive", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual > lower && actual < upper,
+               "assert_between_exclusive", description,
+               "expected a number greater than ${lower} " +
+               "and less than ${upper} but got ${actual}",
+               {lower:lower, upper:upper, actual:actual});
+    }
+    expose(assert_between_exclusive, "assert_between_exclusive");
+
+    function assert_less_than_equal(actual, expected, description)
+    {
+        /*
+         * Test if a primitive number is less than or equal to another
+         */
+        assert(typeof actual === "number",
+               "assert_less_than_equal", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual <= expected,
+               "assert_less_than_equal", description,
+               "expected a number less than or equal to ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    }
+    expose(assert_less_than_equal, "assert_less_than_equal");
+
+    function assert_greater_than_equal(actual, expected, description)
+    {
+        /*
+         * Test if a primitive number is greater than or equal to another
+         */
+        assert(typeof actual === "number",
+               "assert_greater_than_equal", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual >= expected,
+               "assert_greater_than_equal", description,
+               "expected a number greater than or equal to ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    }
+    expose(assert_greater_than_equal, "assert_greater_than_equal");
+
+    function assert_between_inclusive(actual, lower, upper, description)
+    {
+        /*
+         * Test if a primitive number is between to two others or equal to either of them
+         */
+        assert(typeof actual === "number",
+               "assert_between_inclusive", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual >= lower && actual <= upper,
+               "assert_between_inclusive", description,
+               "expected a number greater than or equal to ${lower} " +
+               "and less than or equal to ${upper} but got ${actual}",
+               {lower:lower, upper:upper, actual:actual});
+    }
+    expose(assert_between_inclusive, "assert_between_inclusive");
+
+    function assert_regexp_match(actual, expected, description) {
+        /*
+         * Test if a string (actual) matches a regexp (expected)
+         */
+        assert(expected.test(actual),
+               "assert_regexp_match", description,
+               "expected ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    }
+    expose(assert_regexp_match, "assert_regexp_match");
+
+    function assert_class_string(object, class_string, description) {
+        var actual = {}.toString.call(object);
+        var expected = "[object " + class_string + "]";
+        assert(same_value(actual, expected), "assert_class_string", description,
+                                             "expected ${expected} but got ${actual}",
+                                             {expected:expected, actual:actual});
+    }
+    expose(assert_class_string, "assert_class_string");
+
+
+    function assert_own_property(object, property_name, description) {
+        assert(object.hasOwnProperty(property_name),
+               "assert_own_property", description,
+               "expected property ${p} missing", {p:property_name});
+    }
+    expose(assert_own_property, "assert_own_property");
+
+    function assert_not_own_property(object, property_name, description) {
+        assert(!object.hasOwnProperty(property_name),
+               "assert_not_own_property", description,
+               "unexpected property ${p} is found on object", {p:property_name});
+    }
+    expose(assert_not_own_property, "assert_not_own_property");
+
+    function _assert_inherits(name) {
+        return function (object, property_name, description)
+        {
+            assert(typeof object === "object" || typeof object === "function",
+                   name, description,
+                   "provided value is not an object");
+
+            assert("hasOwnProperty" in object,
+                   name, description,
+                   "provided value is an object but has no hasOwnProperty method");
+
+            assert(!object.hasOwnProperty(property_name),
+                   name, description,
+                   "property ${p} found on object expected in prototype chain",
+                   {p:property_name});
+
+            assert(property_name in object,
+                   name, description,
+                   "property ${p} not found in prototype chain",
+                   {p:property_name});
+        };
+    }
+    expose(_assert_inherits("assert_inherits"), "assert_inherits");
+    expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
+
+    function assert_readonly(object, property_name, description)
+    {
+         var initial_value = object[property_name];
+         try {
+             //Note that this can have side effects in the case where
+             //the property has PutForwards
+             object[property_name] = initial_value + "a"; //XXX use some other value here?
+             assert(same_value(object[property_name], initial_value),
+                    "assert_readonly", description,
+                    "changing property ${p} succeeded",
+                    {p:property_name});
+         } finally {
+             object[property_name] = initial_value;
+         }
+    }
+    expose(assert_readonly, "assert_readonly");
+
+    /**
+     * Assert an Exception with the expected code is thrown.
+     *
+     * @param {object|number|string} code The expected exception code.
+     * @param {Function} func Function which should throw.
+     * @param {string} description Error description for the case that the error is not thrown.
+     */
+    function assert_throws(code, func, description)
+    {
+        try {
+            func.call(this);
+            assert(false, "assert_throws", description,
+                   "${func} did not throw", {func:func});
+        } catch (e) {
+            if (e instanceof AssertionError) {
+                throw e;
+            }
+
+            assert(typeof e === "object",
+                   "assert_throws", description,
+                   "${func} threw ${e} with type ${type}, not an object",
+                   {func:func, e:e, type:typeof e});
+
+            assert(e !== null,
+                   "assert_throws", description,
+                   "${func} threw null, not an object",
+                   {func:func});
+
+            if (code === null) {
+                throw new AssertionError('Test bug: need to pass exception to assert_throws()');
+            }
+            if (typeof code === "object") {
+                assert("name" in e && e.name == code.name,
+                       "assert_throws", description,
+                       "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
+                                    {func:func, actual:e, actual_name:e.name,
+                                     expected:code,
+                                     expected_name:code.name});
+                return;
+            }
+
+            var code_name_map = {
+                INDEX_SIZE_ERR: 'IndexSizeError',
+                HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+                WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+                INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+                NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+                NOT_FOUND_ERR: 'NotFoundError',
+                NOT_SUPPORTED_ERR: 'NotSupportedError',
+                INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
+                INVALID_STATE_ERR: 'InvalidStateError',
+                SYNTAX_ERR: 'SyntaxError',
+                INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+                NAMESPACE_ERR: 'NamespaceError',
+                INVALID_ACCESS_ERR: 'InvalidAccessError',
+                TYPE_MISMATCH_ERR: 'TypeMismatchError',
+                SECURITY_ERR: 'SecurityError',
+                NETWORK_ERR: 'NetworkError',
+                ABORT_ERR: 'AbortError',
+                URL_MISMATCH_ERR: 'URLMismatchError',
+                QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+                TIMEOUT_ERR: 'TimeoutError',
+                INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+                DATA_CLONE_ERR: 'DataCloneError'
+            };
+
+            var name = code in code_name_map ? code_name_map[code] : code;
+
+            var name_code_map = {
+                IndexSizeError: 1,
+                HierarchyRequestError: 3,
+                WrongDocumentError: 4,
+                InvalidCharacterError: 5,
+                NoModificationAllowedError: 7,
+                NotFoundError: 8,
+                NotSupportedError: 9,
+                InUseAttributeError: 10,
+                InvalidStateError: 11,
+                SyntaxError: 12,
+                InvalidModificationError: 13,
+                NamespaceError: 14,
+                InvalidAccessError: 15,
+                TypeMismatchError: 17,
+                SecurityError: 18,
+                NetworkError: 19,
+                AbortError: 20,
+                URLMismatchError: 21,
+                QuotaExceededError: 22,
+                TimeoutError: 23,
+                InvalidNodeTypeError: 24,
+                DataCloneError: 25,
+
+                EncodingError: 0,
+                NotReadableError: 0,
+                UnknownError: 0,
+                ConstraintError: 0,
+                DataError: 0,
+                TransactionInactiveError: 0,
+                ReadOnlyError: 0,
+                VersionError: 0,
+                OperationError: 0,
+                NotAllowedError: 0
+            };
+
+            var code_name_map = {};
+            for (var key in name_code_map) {
+                if (name_code_map[key] > 0) {
+                    code_name_map[name_code_map[key]] = key;
+                }
+            }
+
+            var required_props = { code: code };
+
+            if (typeof code === "number") {
+                if (code === 0) {
+                    throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws()');
+                } else if (!(code in code_name_map)) {
+                    throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
+                }
+                name = code_name_map[code];
+            } else if (typeof code === "string") {
+                if (!(name in name_code_map)) {
+                    throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
+                }
+                required_props.code = name_code_map[name];
+            }
+
+            if (required_props.code === 0 ||
+               ("name" in e &&
+                e.name !== e.name.toUpperCase() &&
+                e.name !== "DOMException")) {
+                // New style exception: also test the name property.
+                required_props.name = name;
+            }
+
+            //We'd like to test that e instanceof the appropriate interface,
+            //but we can't, because we don't know what window it was created
+            //in.  It might be an instanceof the appropriate interface on some
+            //unknown other window.  TODO: Work around this somehow?
+
+            for (var prop in required_props) {
+                assert(prop in e && e[prop] == required_props[prop],
+                       "assert_throws", description,
+                       "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}",
+                       {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+            }
+        }
+    }
+    expose(assert_throws, "assert_throws");
+
+    /**
+     * Assert a JS Error with the expected constructor is thrown.
+     *
+     * @param {object} constructor The expected exception constructor.
+     * @param {Function} func Function which should throw.
+     * @param {string} description Error description for the case that the error is not thrown.
+     */
+    function assert_throws_js(constructor, func, description)
+    {
+        assert_throws_js_impl(constructor, func, description,
+                              "assert_throws_js");
+    }
+    expose(assert_throws_js, "assert_throws_js");
+
+    /**
+     * Like assert_throws_js but allows specifying the assertion type
+     * (assert_throws_js or promise_rejects_js, in practice).
+     */
+    function assert_throws_js_impl(constructor, func, description,
+                                   assertion_type)
+    {
+        try {
+            func.call(this);
+            assert(false, assertion_type, description,
+                   "${func} did not throw", {func:func});
+        } catch (e) {
+            if (e instanceof AssertionError) {
+                throw e;
+            }
+
+            // Basic sanity-checks on the thrown exception.
+            assert(typeof e === "object",
+                   assertion_type, description,
+                   "${func} threw ${e} with type ${type}, not an object",
+                   {func:func, e:e, type:typeof e});
+
+            assert(e !== null,
+                   assertion_type, description,
+                   "${func} threw null, not an object",
+                   {func:func});
+
+            // Basic sanity-check on the passed-in constructor
+            assert(typeof constructor == "function",
+                   assertion_type, description,
+                   "${constructor} is not a constructor",
+                   {constructor:constructor});
+            var obj = constructor;
+            while (obj) {
+                if (typeof obj === "function" &&
+                    obj.name === "Error") {
+                    break;
+                }
+                obj = Object.getPrototypeOf(obj);
+            }
+            assert(obj != null,
+                   assertion_type, description,
+                   "${constructor} is not an Error subtype",
+                   {constructor:constructor});
+
+            // And checking that our exception is reasonable
+            assert(e.constructor === constructor &&
+                   e.name === constructor.name,
+                   assertion_type, description,
+                   "${func} threw ${actual} (${actual_name}) expected instance of ${expected} (${expected_name})",
+                   {func:func, actual:e, actual_name:e.name,
+                    expected:constructor,
+                    expected_name:constructor.name});
+        }
+    }
+
+    /**
+     * Assert a DOMException with the expected type is thrown.
+     *
+     * @param {number|string} type The expected exception name or code.  See the
+     *        table of names and codes at
+     *        https://heycam.github.io/webidl/#dfn-error-names-table
+     *        If a number is passed it should be one of the numeric code values
+     *        in that table (e.g. 3, 4, etc).  If a string is passed it can
+     *        either be an exception name (e.g. "HierarchyRequestError",
+     *        "WrongDocumentError") or the name of the corresponding error code
+     *        (e.g. "HIERARCHY_REQUEST_ERR", "WRONG_DOCUMENT_ERR").
+     * @param {Function} func Function which should throw.
+     * @param {string} description Error description for the case that the error is not thrown.
+     */
+    function assert_throws_dom(type, func, description)
+    {
+        assert_throws_dom_impl(type, func, description, "assert_throws_dom")
+    }
+    expose(assert_throws_dom, "assert_throws_dom");
+
+    /**
+     * Like assert_throws_dom but allows specifying the assertion type
+     * (assert_throws_dom or promise_rejects_dom, in practice).
+     */
+    function assert_throws_dom_impl(type, func, description, assertion_type)
+    {
+        try {
+            func.call(this);
+            assert(false, assertion_type, description,
+                   "${func} did not throw", {func:func});
+        } catch (e) {
+            if (e instanceof AssertionError) {
+                throw e;
+            }
+
+            assert(typeof e === "object",
+                   assertion_type, description,
+                   "${func} threw ${e} with type ${type}, not an object",
+                   {func:func, e:e, type:typeof e});
+
+            assert(e !== null,
+                   assertion_type, description,
+                   "${func} threw null, not an object",
+                   {func:func});
+
+            // Sanity-check our type
+            assert(typeof type == "number" ||
+                   typeof type == "string",
+                   assertion_type, description,
+                   "${type} is not a number or string",
+                   {type:type});
+
+            var codename_name_map = {
+                INDEX_SIZE_ERR: 'IndexSizeError',
+                HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+                WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+                INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+                NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+                NOT_FOUND_ERR: 'NotFoundError',
+                NOT_SUPPORTED_ERR: 'NotSupportedError',
+                INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
+                INVALID_STATE_ERR: 'InvalidStateError',
+                SYNTAX_ERR: 'SyntaxError',
+                INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+                NAMESPACE_ERR: 'NamespaceError',
+                INVALID_ACCESS_ERR: 'InvalidAccessError',
+                TYPE_MISMATCH_ERR: 'TypeMismatchError',
+                SECURITY_ERR: 'SecurityError',
+                NETWORK_ERR: 'NetworkError',
+                ABORT_ERR: 'AbortError',
+                URL_MISMATCH_ERR: 'URLMismatchError',
+                QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+                TIMEOUT_ERR: 'TimeoutError',
+                INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+                DATA_CLONE_ERR: 'DataCloneError'
+            };
+
+            var name_code_map = {
+                IndexSizeError: 1,
+                HierarchyRequestError: 3,
+                WrongDocumentError: 4,
+                InvalidCharacterError: 5,
+                NoModificationAllowedError: 7,
+                NotFoundError: 8,
+                NotSupportedError: 9,
+                InUseAttributeError: 10,
+                InvalidStateError: 11,
+                SyntaxError: 12,
+                InvalidModificationError: 13,
+                NamespaceError: 14,
+                InvalidAccessError: 15,
+                TypeMismatchError: 17,
+                SecurityError: 18,
+                NetworkError: 19,
+                AbortError: 20,
+                URLMismatchError: 21,
+                QuotaExceededError: 22,
+                TimeoutError: 23,
+                InvalidNodeTypeError: 24,
+                DataCloneError: 25,
+
+                EncodingError: 0,
+                NotReadableError: 0,
+                UnknownError: 0,
+                ConstraintError: 0,
+                DataError: 0,
+                TransactionInactiveError: 0,
+                ReadOnlyError: 0,
+                VersionError: 0,
+                OperationError: 0,
+                NotAllowedError: 0
+            };
+
+            var code_name_map = {};
+            for (var key in name_code_map) {
+                if (name_code_map[key] > 0) {
+                    code_name_map[name_code_map[key]] = key;
+                }
+            }
+
+            var required_props = {};
+            var name;
+
+            if (typeof type === "number") {
+                if (type === 0) {
+                    throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()');
+                } else if (!(type in code_name_map)) {
+                    throw new AssertionError('Test bug: unrecognized DOMException code "' + type + '" passed to assert_throws_dom()');
+                }
+                name = code_name_map[type];
+                required_props.code = type;
+            } else if (typeof type === "string") {
+                name = type in codename_name_map ? codename_name_map[type] : type;
+                if (!(name in name_code_map)) {
+                    throw new AssertionError('Test bug: unrecognized DOMException code name or name "' + type + '" passed to assert_throws_dom()');
+                }
+
+                required_props.code = name_code_map[name];
+            }
+
+            if (required_props.code === 0 ||
+               ("name" in e &&
+                e.name !== e.name.toUpperCase() &&
+                e.name !== "DOMException")) {
+                // New style exception: also test the name property.
+                required_props.name = name;
+            }
+
+            //We'd like to test that e instanceof the appropriate interface,
+            //but we can't, because we don't know what window it was created
+            //in.  It might be an instanceof the appropriate interface on some
+            //unknown other window.  TODO: Work around this somehow?  Maybe have
+            //the first arg just be a DOMException with the right name instead
+            //of the string-or-code thing we have now?
+
+            for (var prop in required_props) {
+                assert(prop in e && e[prop] == required_props[prop],
+                       assertion_type, description,
+                       "${func} threw ${e} that is not a DOMException " + type + ": property ${prop} is equal to ${actual}, expected ${expected}",
+                       {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+            }
+        }
+    }
+
+    /**
+     * Assert the provided value is thrown.
+     *
+     * @param {value} exception The expected exception.
+     * @param {Function} func Function which should throw.
+     * @param {string} description Error description for the case that the error is not thrown.
+     */
+    function assert_throws_exactly(exception, func, description)
+    {
+        assert_throws_exactly_impl(exception, func, description,
+                                   "assert_throws_exactly");
+    }
+    expose(assert_throws_exactly, "assert_throws_exactly");
+
+    /**
+     * Like assert_throws_exactly but allows specifying the assertion type
+     * (assert_throws_exactly or promise_rejects_exactly, in practice).
+     */
+    function assert_throws_exactly_impl(exception, func, description,
+                                        assertion_type)
+    {
+        try {
+            func.call(this);
+            assert(false, assertion_type, description,
+                   "${func} did not throw", {func:func});
+        } catch (e) {
+            if (e instanceof AssertionError) {
+                throw e;
+            }
+
+            assert(same_value(e, exception), assertion_type, description,
+                   "${func} threw ${e} but we expected it to throw ${exception}",
+                   {func:func, e:e, exception:exception});
+        }
+    }
+
+    function assert_unreached(description) {
+         assert(false, "assert_unreached", description,
+                "Reached unreachable code");
+    }
+    expose(assert_unreached, "assert_unreached");
+
+    function assert_any(assert_func, actual, expected_array)
+    {
+        var args = [].slice.call(arguments, 3);
+        var errors = [];
+        var passed = false;
+        forEach(expected_array,
+                function(expected)
+                {
+                    try {
+                        assert_func.apply(this, [actual, expected].concat(args));
+                        passed = true;
+                    } catch (e) {
+                        errors.push(e.message);
+                    }
+                });
+        if (!passed) {
+            throw new AssertionError(errors.join("\n\n"));
+        }
+    }
+    expose(assert_any, "assert_any");
+
+    function assert_precondition(precondition, description) {
+        if (!precondition) {
+            throw new PreconditionFailedError(description);
+        }
+    }
+    expose(assert_precondition, "assert_precondition");
+
+    function Test(name, properties)
+    {
+        if (tests.file_is_test && tests.tests.length) {
+            throw new Error("Tried to create a test with file_is_test");
+        }
+        this.name = name;
+
+        this.phase = (tests.is_aborted || tests.phase === tests.phases.COMPLETE) ?
+            this.phases.COMPLETE : this.phases.INITIAL;
+
+        this.status = this.NOTRUN;
+        this.timeout_id = null;
+        this.index = null;
+
+        this.properties = properties || {};
+        this.timeout_length = settings.test_timeout;
+        if (this.timeout_length !== null) {
+            this.timeout_length *= tests.timeout_multiplier;
+        }
+
+        this.message = null;
+        this.stack = null;
+
+        this.steps = [];
+        this._is_promise_test = false;
+
+        this.cleanup_callbacks = [];
+        this._user_defined_cleanup_count = 0;
+        this._done_callbacks = [];
+
+        // Tests declared following harness completion are likely an indication
+        // of a programming error, but they cannot be reported
+        // deterministically.
+        if (tests.phase === tests.phases.COMPLETE) {
+            return;
+        }
+
+        tests.push(this);
+    }
+
+    Test.statuses = {
+        PASS:0,
+        FAIL:1,
+        TIMEOUT:2,
+        NOTRUN:3,
+        PRECONDITION_FAILED:4
+    };
+
+    Test.prototype = merge({}, Test.statuses);
+
+    Test.prototype.phases = {
+        INITIAL:0,
+        STARTED:1,
+        HAS_RESULT:2,
+        CLEANING:3,
+        COMPLETE:4
+    };
+
+    Test.prototype.structured_clone = function()
+    {
+        if (!this._structured_clone) {
+            var msg = this.message;
+            msg = msg ? String(msg) : msg;
+            this._structured_clone = merge({
+                name:String(this.name),
+                properties:merge({}, this.properties),
+                phases:merge({}, this.phases)
+            }, Test.statuses);
+        }
+        this._structured_clone.status = this.status;
+        this._structured_clone.message = this.message;
+        this._structured_clone.stack = this.stack;
+        this._structured_clone.index = this.index;
+        this._structured_clone.phase = this.phase;
+        return this._structured_clone;
+    };
+
+    Test.prototype.step = function(func, this_obj)
+    {
+        if (this.phase > this.phases.STARTED) {
+            return;
+        }
+        this.phase = this.phases.STARTED;
+        //If we don't get a result before the harness times out that will be a test timeout
+        this.set_status(this.TIMEOUT, "Test timed out");
+
+        tests.started = true;
+        tests.notify_test_state(this);
+
+        if (this.timeout_id === null) {
+            this.set_timeout();
+        }
+
+        this.steps.push(func);
+
+        if (arguments.length === 1) {
+            this_obj = this;
+        }
+
+        try {
+            return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+        } catch (e) {
+            if (this.phase >= this.phases.HAS_RESULT) {
+                return;
+            }
+            var status = e instanceof PreconditionFailedError ? this.PRECONDITION_FAILED : this.FAIL;
+            var message = String((typeof e === "object" && e !== null) ? e.message : e);
+            var stack = e.stack ? e.stack : null;
+
+            this.set_status(status, message, stack);
+            this.phase = this.phases.HAS_RESULT;
+            this.done();
+        }
+    };
+
+    Test.prototype.step_func = function(func, this_obj)
+    {
+        var test_this = this;
+
+        if (arguments.length === 1) {
+            this_obj = test_this;
+        }
+
+        return function()
+        {
+            return test_this.step.apply(test_this, [func, this_obj].concat(
+                Array.prototype.slice.call(arguments)));
+        };
+    };
+
+    Test.prototype.step_func_done = function(func, this_obj)
+    {
+        var test_this = this;
+
+        if (arguments.length === 1) {
+            this_obj = test_this;
+        }
+
+        return function()
+        {
+            if (func) {
+                test_this.step.apply(test_this, [func, this_obj].concat(
+                    Array.prototype.slice.call(arguments)));
+            }
+            test_this.done();
+        };
+    };
+
+    Test.prototype.unreached_func = function(description)
+    {
+        return this.step_func(function() {
+            assert_unreached(description);
+        });
+    };
+
+    Test.prototype.step_timeout = function(f, timeout) {
+        var test_this = this;
+        var args = Array.prototype.slice.call(arguments, 2);
+        return setTimeout(this.step_func(function() {
+            return f.apply(test_this, args);
+        }), timeout * tests.timeout_multiplier);
+    }
+
+    /*
+     * Private method for registering cleanup functions. `testharness.js`
+     * internals should use this method instead of the public `add_cleanup`
+     * method in order to hide implementation details from the harness status
+     * message in the case errors.
+     */
+    Test.prototype._add_cleanup = function(callback) {
+        this.cleanup_callbacks.push(callback);
+    };
+
+    /*
+     * Schedule a function to be run after the test result is known, regardless
+     * of passing or failing state. The behavior of this function will not
+     * influence the result of the test, but if an exception is thrown, the
+     * test harness will report an error.
+     */
+    Test.prototype.add_cleanup = function(callback) {
+        this._user_defined_cleanup_count += 1;
+        this._add_cleanup(callback);
+    };
+
+    Test.prototype.set_timeout = function()
+    {
+        if (this.timeout_length !== null) {
+            var this_obj = this;
+            this.timeout_id = setTimeout(function()
+                                         {
+                                             this_obj.timeout();
+                                         }, this.timeout_length);
+        }
+    };
+
+    Test.prototype.set_status = function(status, message, stack)
+    {
+        this.status = status;
+        this.message = message;
+        this.stack = stack ? stack : null;
+    };
+
+    Test.prototype.timeout = function()
+    {
+        this.timeout_id = null;
+        this.set_status(this.TIMEOUT, "Test timed out");
+        this.phase = this.phases.HAS_RESULT;
+        this.done();
+    };
+
+    Test.prototype.force_timeout = Test.prototype.timeout;
+
+    /**
+     * Update the test status, initiate "cleanup" functions, and signal test
+     * completion.
+     */
+    Test.prototype.done = function()
+    {
+        if (this.phase >= this.phases.CLEANING) {
+            return;
+        }
+
+        if (this.phase <= this.phases.STARTED) {
+            this.set_status(this.PASS, null);
+        }
+
+        if (global_scope.clearTimeout) {
+            clearTimeout(this.timeout_id);
+        }
+
+        this.cleanup();
+    };
+
+    function add_test_done_callback(test, callback)
+    {
+        if (test.phase === test.phases.COMPLETE) {
+            callback();
+            return;
+        }
+
+        test._done_callbacks.push(callback);
+    }
+
+    /*
+     * Invoke all specified cleanup functions. If one or more produce an error,
+     * the context is in an unpredictable state, so all further testing should
+     * be cancelled.
+     */
+    Test.prototype.cleanup = function() {
+        var error_count = 0;
+        var bad_value_count = 0;
+        function on_error() {
+            error_count += 1;
+            // Abort tests immediately so that tests declared within subsequent
+            // cleanup functions are not run.
+            tests.abort();
+        }
+        var this_obj = this;
+        var results = [];
+
+        this.phase = this.phases.CLEANING;
+
+        forEach(this.cleanup_callbacks,
+                function(cleanup_callback) {
+                    var result;
+
+                    try {
+                        result = cleanup_callback();
+                    } catch (e) {
+                        on_error();
+                        return;
+                    }
+
+                    if (!is_valid_cleanup_result(this_obj, result)) {
+                        bad_value_count += 1;
+                        // Abort tests immediately so that tests declared
+                        // within subsequent cleanup functions are not run.
+                        tests.abort();
+                    }
+
+                    results.push(result);
+                });
+
+        if (!this._is_promise_test) {
+            cleanup_done(this_obj, error_count, bad_value_count);
+        } else {
+            all_async(results,
+                      function(result, done) {
+                          if (result && typeof result.then === "function") {
+                              result
+                                  .then(null, on_error)
+                                  .then(done);
+                          } else {
+                              done();
+                          }
+                      },
+                      function() {
+                          cleanup_done(this_obj, error_count, bad_value_count);
+                      });
+        }
+    };
+
+    /**
+     * Determine if the return value of a cleanup function is valid for a given
+     * test. Any test may return the value `undefined`. Tests created with
+     * `promise_test` may alternatively return "thenable" object values.
+     */
+    function is_valid_cleanup_result(test, result) {
+        if (result === undefined) {
+            return true;
+        }
+
+        if (test._is_promise_test) {
+            return result && typeof result.then === "function";
+        }
+
+        return false;
+    }
+
+    function cleanup_done(test, error_count, bad_value_count) {
+        if (error_count || bad_value_count) {
+            var total = test._user_defined_cleanup_count;
+
+            tests.status.status = tests.status.ERROR;
+            tests.status.message = "Test named '" + test.name +
+                "' specified " + total +
+                " 'cleanup' function" + (total > 1 ? "s" : "");
+
+            if (error_count) {
+                tests.status.message += ", and " + error_count + " failed";
+            }
+
+            if (bad_value_count) {
+                var type = test._is_promise_test ?
+                   "non-thenable" : "non-undefined";
+                tests.status.message += ", and " + bad_value_count +
+                    " returned a " + type + " value";
+            }
+
+            tests.status.message += ".";
+
+            tests.status.stack = null;
+        }
+
+        test.phase = test.phases.COMPLETE;
+        tests.result(test);
+        forEach(test._done_callbacks,
+                function(callback) {
+                    callback();
+                });
+        test._done_callbacks.length = 0;
+    }
+
+    /*
+     * A RemoteTest object mirrors a Test object on a remote worker. The
+     * associated RemoteWorker updates the RemoteTest object in response to
+     * received events. In turn, the RemoteTest object replicates these events
+     * on the local document. This allows listeners (test result reporting
+     * etc..) to transparently handle local and remote events.
+     */
+    function RemoteTest(clone) {
+        var this_obj = this;
+        Object.keys(clone).forEach(
+                function(key) {
+                    this_obj[key] = clone[key];
+                });
+        this.index = null;
+        this.phase = this.phases.INITIAL;
+        this.update_state_from(clone);
+        this._done_callbacks = [];
+        tests.push(this);
+    }
+
+    RemoteTest.prototype.structured_clone = function() {
+        var clone = {};
+        Object.keys(this).forEach(
+                (function(key) {
+                    var value = this[key];
+                    // `RemoteTest` instances are responsible for managing
+                    // their own "done" callback functions, so those functions
+                    // are not relevant in other execution contexts. Because of
+                    // this (and because Function values cannot be serialized
+                    // for cross-realm transmittance), the property should not
+                    // be considered when cloning instances.
+                    if (key === '_done_callbacks' ) {
+                        return;
+                    }
+
+                    if (typeof value === "object" && value !== null) {
+                        clone[key] = merge({}, value);
+                    } else {
+                        clone[key] = value;
+                    }
+                }).bind(this));
+        clone.phases = merge({}, this.phases);
+        return clone;
+    };
+
+    /**
+     * `RemoteTest` instances are objects which represent tests running in
+     * another realm. They do not define "cleanup" functions (if necessary,
+     * such functions are defined on the associated `Test` instance within the
+     * external realm). However, `RemoteTests` may have "done" callbacks (e.g.
+     * as attached by the `Tests` instance responsible for tracking the overall
+     * test status in the parent realm). The `cleanup` method delegates to
+     * `done` in order to ensure that such callbacks are invoked following the
+     * completion of the `RemoteTest`.
+     */
+    RemoteTest.prototype.cleanup = function() {
+        this.done();
+    };
+    RemoteTest.prototype.phases = Test.prototype.phases;
+    RemoteTest.prototype.update_state_from = function(clone) {
+        this.status = clone.status;
+        this.message = clone.message;
+        this.stack = clone.stack;
+        if (this.phase === this.phases.INITIAL) {
+            this.phase = this.phases.STARTED;
+        }
+    };
+    RemoteTest.prototype.done = function() {
+        this.phase = this.phases.COMPLETE;
+
+        forEach(this._done_callbacks,
+                function(callback) {
+                    callback();
+                });
+    }
+
+    /*
+     * A RemoteContext listens for test events from a remote test context, such
+     * as another window or a worker. These events are then used to construct
+     * and maintain RemoteTest objects that mirror the tests running in the
+     * remote context.
+     *
+     * An optional third parameter can be used as a predicate to filter incoming
+     * MessageEvents.
+     */
+    function RemoteContext(remote, message_target, message_filter) {
+        this.running = true;
+        this.started = false;
+        this.tests = new Array();
+        this.early_exception = null;
+
+        var this_obj = this;
+        // If remote context is cross origin assigning to onerror is not
+        // possible, so silently catch those errors.
+        try {
+          remote.onerror = function(error) { this_obj.remote_error(error); };
+        } catch (e) {
+          // Ignore.
+        }
+
+        // Keeping a reference to the remote object and the message handler until
+        // remote_done() is seen prevents the remote object and its message channel
+        // from going away before all the messages are dispatched.
+        this.remote = remote;
+        this.message_target = message_target;
+        this.message_handler = function(message) {
+            var passesFilter = !message_filter || message_filter(message);
+            // The reference to the `running` property in the following
+            // condition is unnecessary because that value is only set to
+            // `false` after the `message_handler` function has been
+            // unsubscribed.
+            // TODO: Simplify the condition by removing the reference.
+            if (this_obj.running && message.data && passesFilter &&
+                (message.data.type in this_obj.message_handlers)) {
+                this_obj.message_handlers[message.data.type].call(this_obj, message.data);
+            }
+        };
+
+        if (self.Promise) {
+            this.done = new Promise(function(resolve) {
+                this_obj.doneResolve = resolve;
+            });
+        }
+
+        this.message_target.addEventListener("message", this.message_handler);
+    }
+
+    RemoteContext.prototype.remote_error = function(error) {
+        if (error.preventDefault) {
+            error.preventDefault();
+        }
+
+        // Defer interpretation of errors until the testing protocol has
+        // started and the remote test's `allow_uncaught_exception` property
+        // is available.
+        if (!this.started) {
+            this.early_exception = error;
+        } else if (!this.allow_uncaught_exception) {
+            this.report_uncaught(error);
+        }
+    };
+
+    RemoteContext.prototype.report_uncaught = function(error) {
+        var message = error.message || String(error);
+        var filename = (error.filename ? " " + error.filename: "");
+        // FIXME: Display remote error states separately from main document
+        // error state.
+        tests.set_status(tests.status.ERROR,
+                         "Error in remote" + filename + ": " + message,
+                         error.stack);
+    };
+
+    RemoteContext.prototype.start = function(data) {
+        this.started = true;
+        this.allow_uncaught_exception = data.properties.allow_uncaught_exception;
+
+        if (this.early_exception && !this.allow_uncaught_exception) {
+            this.report_uncaught(this.early_exception);
+        }
+    };
+
+    RemoteContext.prototype.test_state = function(data) {
+        var remote_test = this.tests[data.test.index];
+        if (!remote_test) {
+            remote_test = new RemoteTest(data.test);
+            this.tests[data.test.index] = remote_test;
+        }
+        remote_test.update_state_from(data.test);
+        tests.notify_test_state(remote_test);
+    };
+
+    RemoteContext.prototype.test_done = function(data) {
+        var remote_test = this.tests[data.test.index];
+        remote_test.update_state_from(data.test);
+        remote_test.done();
+        tests.result(remote_test);
+    };
+
+    RemoteContext.prototype.remote_done = function(data) {
+        if (tests.status.status === null &&
+            data.status.status !== data.status.OK) {
+            tests.set_status(data.status.status, data.status.message, data.status.sack);
+        }
+
+        this.message_target.removeEventListener("message", this.message_handler);
+        this.running = false;
+
+        // If remote context is cross origin assigning to onerror is not
+        // possible, so silently catch those errors.
+        try {
+          this.remote.onerror = null;
+        } catch (e) {
+          // Ignore.
+        }
+
+        this.remote = null;
+        this.message_target = null;
+        if (this.doneResolve) {
+            this.doneResolve();
+        }
+
+        if (tests.all_done()) {
+            tests.complete();
+        }
+    };
+
+    RemoteContext.prototype.message_handlers = {
+        start: RemoteContext.prototype.start,
+        test_state: RemoteContext.prototype.test_state,
+        result: RemoteContext.prototype.test_done,
+        complete: RemoteContext.prototype.remote_done
+    };
+
+    /*
+     * Harness
+     */
+
+    function TestsStatus()
+    {
+        this.status = null;
+        this.message = null;
+        this.stack = null;
+    }
+
+    TestsStatus.statuses = {
+        OK:0,
+        ERROR:1,
+        TIMEOUT:2,
+        PRECONDITION_FAILED:3
+    };
+
+    TestsStatus.prototype = merge({}, TestsStatus.statuses);
+
+    TestsStatus.prototype.structured_clone = function()
+    {
+        if (!this._structured_clone) {
+            var msg = this.message;
+            msg = msg ? String(msg) : msg;
+            this._structured_clone = merge({
+                status:this.status,
+                message:msg,
+                stack:this.stack
+            }, TestsStatus.statuses);
+        }
+        return this._structured_clone;
+    };
+
+    function Tests()
+    {
+        this.tests = [];
+        this.num_pending = 0;
+
+        this.phases = {
+            INITIAL:0,
+            SETUP:1,
+            HAVE_TESTS:2,
+            HAVE_RESULTS:3,
+            COMPLETE:4
+        };
+        this.phase = this.phases.INITIAL;
+
+        this.properties = {};
+
+        this.wait_for_finish = false;
+        this.processing_callbacks = false;
+
+        this.allow_uncaught_exception = false;
+
+        this.file_is_test = false;
+        // This value is lazily initialized in order to avoid introducing a
+        // dependency on ECMAScript 2015 Promises to all tests.
+        this.promise_tests = null;
+        this.promise_setup_called = false;
+
+        this.timeout_multiplier = 1;
+        this.timeout_length = test_environment.test_timeout();
+        this.timeout_id = null;
+
+        this.start_callbacks = [];
+        this.test_state_callbacks = [];
+        this.test_done_callbacks = [];
+        this.all_done_callbacks = [];
+
+        this.pending_remotes = [];
+
+        this.status = new TestsStatus();
+
+        var this_obj = this;
+
+        test_environment.add_on_loaded_callback(function() {
+            if (this_obj.all_done()) {
+                this_obj.complete();
+            }
+        });
+
+        this.set_timeout();
+    }
+
+    Tests.prototype.setup = function(func, properties)
+    {
+        if (this.phase >= this.phases.HAVE_RESULTS) {
+            return;
+        }
+
+        if (this.phase < this.phases.SETUP) {
+            this.phase = this.phases.SETUP;
+        }
+
+        this.properties = properties;
+
+        for (var p in properties) {
+            if (properties.hasOwnProperty(p)) {
+                var value = properties[p];
+                if (p == "allow_uncaught_exception") {
+                    this.allow_uncaught_exception = value;
+                } else if (p == "explicit_done" && value) {
+                    this.wait_for_finish = true;
+                } else if (p == "explicit_timeout" && value) {
+                    this.timeout_length = null;
+                    if (this.timeout_id)
+                    {
+                        clearTimeout(this.timeout_id);
+                    }
+                } else if (p == "single_test" && value) {
+                    this.set_file_is_test();
+                } else if (p == "timeout_multiplier") {
+                    this.timeout_multiplier = value;
+                    if (this.timeout_length) {
+                         this.timeout_length *= this.timeout_multiplier;
+                    }
+                }
+            }
+        }
+
+        if (func) {
+            try {
+                func();
+            } catch (e) {
+                this.status.status = e instanceof PreconditionFailedError ? this.status.PRECONDITION_FAILED : this.status.ERROR;
+                this.status.message = String(e);
+                this.status.stack = e.stack ? e.stack : null;
+                this.complete();
+            }
+        }
+        this.set_timeout();
+    };
+
+    Tests.prototype.set_file_is_test = function() {
+        if (this.tests.length > 0) {
+            throw new Error("Tried to set file as test after creating a test");
+        }
+        this.wait_for_finish = true;
+        this.file_is_test = true;
+        // Create the test, which will add it to the list of tests
+        async_test();
+    };
+
+    Tests.prototype.set_status = function(status, message, stack)
+    {
+        this.status.status = status;
+        this.status.message = message;
+        this.status.stack = stack ? stack : null;
+    };
+
+    Tests.prototype.set_timeout = function() {
+        if (global_scope.clearTimeout) {
+            var this_obj = this;
+            clearTimeout(this.timeout_id);
+            if (this.timeout_length !== null) {
+                this.timeout_id = setTimeout(function() {
+                                                 this_obj.timeout();
+                                             }, this.timeout_length);
+            }
+        }
+    };
+
+    Tests.prototype.timeout = function() {
+        var test_in_cleanup = null;
+
+        if (this.status.status === null) {
+            forEach(this.tests,
+                    function(test) {
+                        // No more than one test is expected to be in the
+                        // "CLEANUP" phase at any time
+                        if (test.phase === test.phases.CLEANING) {
+                            test_in_cleanup = test;
+                        }
+
+                        test.phase = test.phases.COMPLETE;
+                    });
+
+            // Timeouts that occur while a test is in the "cleanup" phase
+            // indicate that some global state was not properly reverted. This
+            // invalidates the overall test execution, so the timeout should be
+            // reported as an error and cancel the execution of any remaining
+            // tests.
+            if (test_in_cleanup) {
+                this.status.status = this.status.ERROR;
+                this.status.message = "Timeout while running cleanup for " +
+                    "test named \"" + test_in_cleanup.name + "\".";
+                tests.status.stack = null;
+            } else {
+                this.status.status = this.status.TIMEOUT;
+            }
+        }
+
+        this.complete();
+    };
+
+    Tests.prototype.end_wait = function()
+    {
+        this.wait_for_finish = false;
+        if (this.all_done()) {
+            this.complete();
+        }
+    };
+
+    Tests.prototype.push = function(test)
+    {
+        if (this.phase < this.phases.HAVE_TESTS) {
+            this.start();
+        }
+        this.num_pending++;
+        test.index = this.tests.push(test);
+        this.notify_test_state(test);
+    };
+
+    Tests.prototype.notify_test_state = function(test) {
+        var this_obj = this;
+        forEach(this.test_state_callbacks,
+                function(callback) {
+                    callback(test, this_obj);
+                });
+    };
+
+    Tests.prototype.all_done = function() {
+        return this.tests.length > 0 && test_environment.all_loaded &&
+                (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish &&
+                !this.processing_callbacks &&
+                !this.pending_remotes.some(function(w) { return w.running; });
+    };
+
+    Tests.prototype.start = function() {
+        this.phase = this.phases.HAVE_TESTS;
+        this.notify_start();
+    };
+
+    Tests.prototype.notify_start = function() {
+        var this_obj = this;
+        forEach (this.start_callbacks,
+                 function(callback)
+                 {
+                     callback(this_obj.properties);
+                 });
+    };
+
+    Tests.prototype.result = function(test)
+    {
+        // If the harness has already transitioned beyond the `HAVE_RESULTS`
+        // phase, subsequent tests should not cause it to revert.
+        if (this.phase <= this.phases.HAVE_RESULTS) {
+            this.phase = this.phases.HAVE_RESULTS;
+        }
+        this.num_pending--;
+        this.notify_result(test);
+    };
+
+    Tests.prototype.notify_result = function(test) {
+        var this_obj = this;
+        this.processing_callbacks = true;
+        forEach(this.test_done_callbacks,
+                function(callback)
+                {
+                    callback(test, this_obj);
+                });
+        this.processing_callbacks = false;
+        if (this_obj.all_done()) {
+            this_obj.complete();
+        }
+    };
+
+    Tests.prototype.complete = function() {
+        if (this.phase === this.phases.COMPLETE) {
+            return;
+        }
+        var this_obj = this;
+        var all_complete = function() {
+            this_obj.phase = this_obj.phases.COMPLETE;
+            this_obj.notify_complete();
+        };
+        var incomplete = filter(this.tests,
+                                function(test) {
+                                    return test.phase < test.phases.COMPLETE;
+                                });
+
+        /**
+         * To preserve legacy behavior, overall test completion must be
+         * signaled synchronously.
+         */
+        if (incomplete.length === 0) {
+            all_complete();
+            return;
+        }
+
+        all_async(incomplete,
+                  function(test, testDone)
+                  {
+                      if (test.phase === test.phases.INITIAL) {
+                          test.phase = test.phases.COMPLETE;
+                          testDone();
+                      } else {
+                          add_test_done_callback(test, testDone);
+                          test.cleanup();
+                      }
+                  },
+                  all_complete);
+    };
+
+    /**
+     * Update the harness status to reflect an unrecoverable harness error that
+     * should cancel all further testing. Update all previously-defined tests
+     * which have not yet started to indicate that they will not be executed.
+     */
+    Tests.prototype.abort = function() {
+        this.status.status = this.status.ERROR;
+        this.is_aborted = true;
+
+        forEach(this.tests,
+                function(test) {
+                    if (test.phase === test.phases.INITIAL) {
+                        test.phase = test.phases.COMPLETE;
+                    }
+                });
+    };
+
+    /*
+     * Determine if any tests share the same `name` property. Return an array
+     * containing the names of any such duplicates.
+     */
+    Tests.prototype.find_duplicates = function() {
+        var names = Object.create(null);
+        var duplicates = [];
+
+        forEach (this.tests,
+                 function(test)
+                 {
+                     if (test.name in names && duplicates.indexOf(test.name) === -1) {
+                        duplicates.push(test.name);
+                     }
+                     names[test.name] = true;
+                 });
+
+        return duplicates;
+    };
+
+    function code_unit_str(char) {
+        return 'U+' + char.charCodeAt(0).toString(16);
+    }
+
+    function sanitize_unpaired_surrogates(str) {
+        return str.replace(/([\ud800-\udbff])(?![\udc00-\udfff])/g,
+                           function(_, unpaired)
+                           {
+                               return code_unit_str(unpaired);
+                           })
+                  // This replacement is intentionally implemented without an
+                  // ES2018 negative lookbehind assertion to support runtimes
+                  // which do not yet implement that language feature.
+                  .replace(/(^|[^\ud800-\udbff])([\udc00-\udfff])/g,
+                           function(_, previous, unpaired) {
+                              if (/[\udc00-\udfff]/.test(previous)) {
+                                  previous = code_unit_str(previous);
+                              }
+
+                              return previous + code_unit_str(unpaired);
+                           });
+    }
+
+    function sanitize_all_unpaired_surrogates(tests) {
+        forEach (tests,
+                 function (test)
+                 {
+                     var sanitized = sanitize_unpaired_surrogates(test.name);
+
+                     if (test.name !== sanitized) {
+                         test.name = sanitized;
+                         delete test._structured_clone;
+                     }
+                 });
+    }
+
+    Tests.prototype.notify_complete = function() {
+        var this_obj = this;
+        var duplicates;
+
+        if (this.status.status === null) {
+            duplicates = this.find_duplicates();
+
+            // Some transports adhere to UTF-8's restriction on unpaired
+            // surrogates. Sanitize the titles so that the results can be
+            // consistently sent via all transports.
+            sanitize_all_unpaired_surrogates(this.tests);
+
+            // Test names are presumed to be unique within test files--this
+            // allows consumers to use them for identification purposes.
+            // Duplicated names violate this expectation and should therefore
+            // be reported as an error.
+            if (duplicates.length) {
+                this.status.status = this.status.ERROR;
+                this.status.message =
+                   duplicates.length + ' duplicate test name' +
+                   (duplicates.length > 1 ? 's' : '') + ': "' +
+                   duplicates.join('", "') + '"';
+            } else {
+                this.status.status = this.status.OK;
+            }
+        }
+
+        forEach (this.all_done_callbacks,
+                 function(callback)
+                 {
+                     callback(this_obj.tests, this_obj.status);
+                 });
+    };
+
+    /*
+     * Constructs a RemoteContext that tracks tests from a specific worker.
+     */
+    Tests.prototype.create_remote_worker = function(worker) {
+        var message_port;
+
+        if (is_service_worker(worker)) {
+            if (window.MessageChannel) {
+                // The ServiceWorker's implicit MessagePort is currently not
+                // reliably accessible from the ServiceWorkerGlobalScope due to
+                // Blink setting MessageEvent.source to null for messages sent
+                // via ServiceWorker.postMessage(). Until that's resolved,
+                // create an explicit MessageChannel and pass one end to the
+                // worker.
+                var message_channel = new MessageChannel();
+                message_port = message_channel.port1;
+                message_port.start();
+                worker.postMessage({type: "connect"}, [message_channel.port2]);
+            } else {
+                // If MessageChannel is not available, then try the
+                // ServiceWorker.postMessage() approach using MessageEvent.source
+                // on the other end.
+                message_port = navigator.serviceWorker;
+                worker.postMessage({type: "connect"});
+            }
+        } else if (is_shared_worker(worker)) {
+            message_port = worker.port;
+            message_port.start();
+        } else {
+            message_port = worker;
+        }
+
+        return new RemoteContext(worker, message_port);
+    };
+
+    /*
+     * Constructs a RemoteContext that tracks tests from a specific window.
+     */
+    Tests.prototype.create_remote_window = function(remote) {
+        remote.postMessage({type: "getmessages"}, "*");
+        return new RemoteContext(
+            remote,
+            window,
+            function(msg) {
+                return msg.source === remote;
+            }
+        );
+    };
+
+    Tests.prototype.fetch_tests_from_worker = function(worker) {
+        if (this.phase >= this.phases.COMPLETE) {
+            return;
+        }
+
+        var remoteContext = this.create_remote_worker(worker);
+        this.pending_remotes.push(remoteContext);
+        return remoteContext.done;
+    };
+
+    function fetch_tests_from_worker(port) {
+        return tests.fetch_tests_from_worker(port);
+    }
+    expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
+
+    Tests.prototype.fetch_tests_from_window = function(remote) {
+        if (this.phase >= this.phases.COMPLETE) {
+            return;
+        }
+
+        this.pending_remotes.push(this.create_remote_window(remote));
+    };
+
+    function fetch_tests_from_window(window) {
+        tests.fetch_tests_from_window(window);
+    }
+    expose(fetch_tests_from_window, 'fetch_tests_from_window');
+
+    function timeout() {
+        if (tests.timeout_length === null) {
+            tests.timeout();
+        }
+    }
+    expose(timeout, 'timeout');
+
+    function add_start_callback(callback) {
+        tests.start_callbacks.push(callback);
+    }
+
+    function add_test_state_callback(callback) {
+        tests.test_state_callbacks.push(callback);
+    }
+
+    function add_result_callback(callback) {
+        tests.test_done_callbacks.push(callback);
+    }
+
+    function add_completion_callback(callback) {
+        tests.all_done_callbacks.push(callback);
+    }
+
+    expose(add_start_callback, 'add_start_callback');
+    expose(add_test_state_callback, 'add_test_state_callback');
+    expose(add_result_callback, 'add_result_callback');
+    expose(add_completion_callback, 'add_completion_callback');
+
+    function remove(array, item) {
+        var index = array.indexOf(item);
+        if (index > -1) {
+            array.splice(index, 1);
+        }
+    }
+
+    function remove_start_callback(callback) {
+        remove(tests.start_callbacks, callback);
+    }
+
+    function remove_test_state_callback(callback) {
+        remove(tests.test_state_callbacks, callback);
+    }
+
+    function remove_result_callback(callback) {
+        remove(tests.test_done_callbacks, callback);
+    }
+
+    function remove_completion_callback(callback) {
+       remove(tests.all_done_callbacks, callback);
+    }
+
+    /*
+     * Output listener
+    */
+
+    function Output() {
+        this.output_document = document;
+        this.output_node = null;
+        this.enabled = settings.output;
+        this.phase = this.INITIAL;
+    }
+
+    Output.prototype.INITIAL = 0;
+    Output.prototype.STARTED = 1;
+    Output.prototype.HAVE_RESULTS = 2;
+    Output.prototype.COMPLETE = 3;
+
+    Output.prototype.setup = function(properties) {
+        if (this.phase > this.INITIAL) {
+            return;
+        }
+
+        //If output is disabled in testharnessreport.js the test shouldn't be
+        //able to override that
+        this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
+                                        properties.output : settings.output);
+    };
+
+    Output.prototype.init = function(properties) {
+        if (this.phase >= this.STARTED) {
+            return;
+        }
+        if (properties.output_document) {
+            this.output_document = properties.output_document;
+        } else {
+            this.output_document = document;
+        }
+        this.phase = this.STARTED;
+    };
+
+    Output.prototype.resolve_log = function() {
+        var output_document;
+        if (this.output_node) {
+            return;
+        }
+        if (typeof this.output_document === "function") {
+            output_document = this.output_document.apply(undefined);
+        } else {
+            output_document = this.output_document;
+        }
+        if (!output_document) {
+            return;
+        }
+        var node = output_document.getElementById("log");
+        if (!node) {
+            if (output_document.readyState === "loading") {
+                return;
+            }
+            node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+            node.id = "log";
+            if (output_document.body) {
+                output_document.body.appendChild(node);
+            } else {
+                var root = output_document.documentElement;
+                var is_html = (root &&
+                               root.namespaceURI == "http://www.w3.org/1999/xhtml" &&
+                               root.localName == "html");
+                var is_svg = (output_document.defaultView &&
+                              "SVGSVGElement" in output_document.defaultView &&
+                              root instanceof output_document.defaultView.SVGSVGElement);
+                if (is_svg) {
+                    var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
+                    foreignObject.setAttribute("width", "100%");
+                    foreignObject.setAttribute("height", "100%");
+                    root.appendChild(foreignObject);
+                    foreignObject.appendChild(node);
+                } else if (is_html) {
+                    root.appendChild(output_document.createElementNS("http://www.w3.org/1999/xhtml", "body"))
+                        .appendChild(node);
+                } else {
+                    root.appendChild(node);
+                }
+            }
+        }
+        this.output_document = output_document;
+        this.output_node = node;
+    };
+
+    Output.prototype.show_status = function() {
+        if (this.phase < this.STARTED) {
+            this.init();
+        }
+        if (!this.enabled || this.phase === this.COMPLETE) {
+            return;
+        }
+        this.resolve_log();
+        if (this.phase < this.HAVE_RESULTS) {
+            this.phase = this.HAVE_RESULTS;
+        }
+        var done_count = tests.tests.length - tests.num_pending;
+        if (this.output_node) {
+            if (done_count < 100 ||
+                (done_count < 1000 && done_count % 100 === 0) ||
+                done_count % 1000 === 0) {
+                this.output_node.textContent = "Running, " +
+                    done_count + " complete, " +
+                    tests.num_pending + " remain";
+            }
+        }
+    };
+
+    Output.prototype.show_results = function (tests, harness_status) {
+        if (this.phase >= this.COMPLETE) {
+            return;
+        }
+        if (!this.enabled) {
+            return;
+        }
+        if (!this.output_node) {
+            this.resolve_log();
+        }
+        this.phase = this.COMPLETE;
+
+        var log = this.output_node;
+        if (!log) {
+            return;
+        }
+        var output_document = this.output_document;
+
+        while (log.lastChild) {
+            log.removeChild(log.lastChild);
+        }
+
+        var stylesheet = output_document.createElementNS(xhtml_ns, "style");
+        stylesheet.textContent = stylesheetContent;
+        var heads = output_document.getElementsByTagName("head");
+        if (heads.length) {
+            heads[0].appendChild(stylesheet);
+        }
+
+        var status_text_harness = {};
+        status_text_harness[harness_status.OK] = "OK";
+        status_text_harness[harness_status.ERROR] = "Error";
+        status_text_harness[harness_status.TIMEOUT] = "Timeout";
+        status_text_harness[harness_status.PRECONDITION_FAILED] = "Precondition Failed";
+
+        var status_text = {};
+        status_text[Test.prototype.PASS] = "Pass";
+        status_text[Test.prototype.FAIL] = "Fail";
+        status_text[Test.prototype.TIMEOUT] = "Timeout";
+        status_text[Test.prototype.NOTRUN] = "Not Run";
+        status_text[Test.prototype.PRECONDITION_FAILED] = "Precondition Failed";
+
+        var status_number = {};
+        forEach(tests,
+                function(test) {
+                    var status = status_text[test.status];
+                    if (status_number.hasOwnProperty(status)) {
+                        status_number[status] += 1;
+                    } else {
+                        status_number[status] = 1;
+                    }
+                });
+
+        function status_class(status)
+        {
+            return status.replace(/\s/g, '').toLowerCase();
+        }
+
+        var summary_template = ["section", {"id":"summary"},
+                                ["h2", {}, "Summary"],
+                                function()
+                                {
+
+                                    var status = status_text_harness[harness_status.status];
+                                    var rv = [["section", {},
+                                               ["p", {},
+                                                "Harness status: ",
+                                                ["span", {"class":status_class(status)},
+                                                 status
+                                                ],
+                                               ]
+                                              ]];
+
+                                    if (harness_status.status === harness_status.ERROR) {
+                                        rv[0].push(["pre", {}, harness_status.message]);
+                                        if (harness_status.stack) {
+                                            rv[0].push(["pre", {}, harness_status.stack]);
+                                        }
+                                    }
+                                    return rv;
+                                },
+                                ["p", {}, "Found ${num_tests} tests"],
+                                function() {
+                                    var rv = [["div", {}]];
+                                    var i = 0;
+                                    while (status_text.hasOwnProperty(i)) {
+                                        if (status_number.hasOwnProperty(status_text[i])) {
+                                            var status = status_text[i];
+                                            rv[0].push(["div", {"class":status_class(status)},
+                                                        ["label", {},
+                                                         ["input", {type:"checkbox", checked:"checked"}],
+                                                         status_number[status] + " " + status]]);
+                                        }
+                                        i++;
+                                    }
+                                    return rv;
+                                },
+                               ];
+
+        log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
+
+        forEach(output_document.querySelectorAll("section#summary label"),
+                function(element)
+                {
+                    on_event(element, "click",
+                             function(e)
+                             {
+                                 if (output_document.getElementById("results") === null) {
+                                     e.preventDefault();
+                                     return;
+                                 }
+                                 var result_class = element.parentNode.getAttribute("class");
+                                 var style_element = output_document.querySelector("style#hide-" + result_class);
+                                 var input_element = element.querySelector("input");
+                                 if (!style_element && !input_element.checked) {
+                                     style_element = output_document.createElementNS(xhtml_ns, "style");
+                                     style_element.id = "hide-" + result_class;
+                                     style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}";
+                                     output_document.body.appendChild(style_element);
+                                 } else if (style_element && input_element.checked) {
+                                     style_element.parentNode.removeChild(style_element);
+                                 }
+                             });
+                });
+
+        // This use of innerHTML plus manual escaping is not recommended in
+        // general, but is necessary here for performance.  Using textContent
+        // on each individual <td> adds tens of seconds of execution time for
+        // large test suites (tens of thousands of tests).
+        function escape_html(s)
+        {
+            return s.replace(/\&/g, "&amp;")
+                .replace(/</g, "&lt;")
+                .replace(/"/g, "&quot;")
+                .replace(/'/g, "&#39;");
+        }
+
+        function has_assertions()
+        {
+            for (var i = 0; i < tests.length; i++) {
+                if (tests[i].properties.hasOwnProperty("assert")) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function get_assertion(test)
+        {
+            if (test.properties.hasOwnProperty("assert")) {
+                if (Array.isArray(test.properties.assert)) {
+                    return test.properties.assert.join(' ');
+                }
+                return test.properties.assert;
+            }
+            return '';
+        }
+
+        log.appendChild(document.createElementNS(xhtml_ns, "section"));
+        var assertions = has_assertions();
+        var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
+            "<thead><tr><th>Result</th><th>Test Name</th>" +
+            (assertions ? "<th>Assertion</th>" : "") +
+            "<th>Message</th></tr></thead>" +
+            "<tbody>";
+        for (var i = 0; i < tests.length; i++) {
+            html += '<tr class="' +
+                escape_html(status_class(status_text[tests[i].status])) +
+                '"><td>' +
+                escape_html(status_text[tests[i].status]) +
+                "</td><td>" +
+                escape_html(tests[i].name) +
+                "</td><td>" +
+                (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") +
+                escape_html(tests[i].message ? tests[i].message : " ") +
+                (tests[i].stack ? "<pre>" +
+                 escape_html(tests[i].stack) +
+                 "</pre>": "") +
+                "</td></tr>";
+        }
+        html += "</tbody></table>";
+        try {
+            log.lastChild.innerHTML = html;
+        } catch (e) {
+            log.appendChild(document.createElementNS(xhtml_ns, "p"))
+               .textContent = "Setting innerHTML for the log threw an exception.";
+            log.appendChild(document.createElementNS(xhtml_ns, "pre"))
+               .textContent = html;
+        }
+    };
+
+    /*
+     * Template code
+     *
+     * A template is just a JavaScript structure. An element is represented as:
+     *
+     * [tag_name, {attr_name:attr_value}, child1, child2]
+     *
+     * the children can either be strings (which act like text nodes), other templates or
+     * functions (see below)
+     *
+     * A text node is represented as
+     *
+     * ["{text}", value]
+     *
+     * String values have a simple substitution syntax; ${foo} represents a variable foo.
+     *
+     * It is possible to embed logic in templates by using a function in a place where a
+     * node would usually go. The function must either return part of a template or null.
+     *
+     * In cases where a set of nodes are required as output rather than a single node
+     * with children it is possible to just use a list
+     * [node1, node2, node3]
+     *
+     * Usage:
+     *
+     * render(template, substitutions) - take a template and an object mapping
+     * variable names to parameters and return either a DOM node or a list of DOM nodes
+     *
+     * substitute(template, substitutions) - take a template and variable mapping object,
+     * make the variable substitutions and return the substituted template
+     *
+     */
+
+    function is_single_node(template)
+    {
+        return typeof template[0] === "string";
+    }
+
+    function substitute(template, substitutions)
+    {
+        if (typeof template === "function") {
+            var replacement = template(substitutions);
+            if (!replacement) {
+                return null;
+            }
+
+            return substitute(replacement, substitutions);
+        }
+
+        if (is_single_node(template)) {
+            return substitute_single(template, substitutions);
+        }
+
+        return filter(map(template, function(x) {
+                              return substitute(x, substitutions);
+                          }), function(x) {return x !== null;});
+    }
+
+    function substitute_single(template, substitutions)
+    {
+        var substitution_re = /\$\{([^ }]*)\}/g;
+
+        function do_substitution(input) {
+            var components = input.split(substitution_re);
+            var rv = [];
+            for (var i = 0; i < components.length; i += 2) {
+                rv.push(components[i]);
+                if (components[i + 1]) {
+                    rv.push(String(substitutions[components[i + 1]]));
+                }
+            }
+            return rv;
+        }
+
+        function substitute_attrs(attrs, rv)
+        {
+            rv[1] = {};
+            for (var name in template[1]) {
+                if (attrs.hasOwnProperty(name)) {
+                    var new_name = do_substitution(name).join("");
+                    var new_value = do_substitution(attrs[name]).join("");
+                    rv[1][new_name] = new_value;
+                }
+            }
+        }
+
+        function substitute_children(children, rv)
+        {
+            for (var i = 0; i < children.length; i++) {
+                if (children[i] instanceof Object) {
+                    var replacement = substitute(children[i], substitutions);
+                    if (replacement !== null) {
+                        if (is_single_node(replacement)) {
+                            rv.push(replacement);
+                        } else {
+                            extend(rv, replacement);
+                        }
+                    }
+                } else {
+                    extend(rv, do_substitution(String(children[i])));
+                }
+            }
+            return rv;
+        }
+
+        var rv = [];
+        rv.push(do_substitution(String(template[0])).join(""));
+
+        if (template[0] === "{text}") {
+            substitute_children(template.slice(1), rv);
+        } else {
+            substitute_attrs(template[1], rv);
+            substitute_children(template.slice(2), rv);
+        }
+
+        return rv;
+    }
+
+    function make_dom_single(template, doc)
+    {
+        var output_document = doc || document;
+        var element;
+        if (template[0] === "{text}") {
+            element = output_document.createTextNode("");
+            for (var i = 1; i < template.length; i++) {
+                element.data += template[i];
+            }
+        } else {
+            element = output_document.createElementNS(xhtml_ns, template[0]);
+            for (var name in template[1]) {
+                if (template[1].hasOwnProperty(name)) {
+                    element.setAttribute(name, template[1][name]);
+                }
+            }
+            for (var i = 2; i < template.length; i++) {
+                if (template[i] instanceof Object) {
+                    var sub_element = make_dom(template[i]);
+                    element.appendChild(sub_element);
+                } else {
+                    var text_node = output_document.createTextNode(template[i]);
+                    element.appendChild(text_node);
+                }
+            }
+        }
+
+        return element;
+    }
+
+    function make_dom(template, substitutions, output_document)
+    {
+        if (is_single_node(template)) {
+            return make_dom_single(template, output_document);
+        }
+
+        return map(template, function(x) {
+                       return make_dom_single(x, output_document);
+                   });
+    }
+
+    function render(template, substitutions, output_document)
+    {
+        return make_dom(substitute(template, substitutions), output_document);
+    }
+
+    /*
+     * Utility functions
+     */
+    function assert(expected_true, function_name, description, error, substitutions)
+    {
+        if (expected_true !== true) {
+            var msg = make_message(function_name, description,
+                                   error, substitutions);
+            throw new AssertionError(msg);
+        }
+    }
+
+    function AssertionError(message)
+    {
+        this.message = message;
+        this.stack = this.get_stack();
+    }
+    expose(AssertionError, "AssertionError");
+
+    AssertionError.prototype = Object.create(Error.prototype);
+
+    AssertionError.prototype.get_stack = function() {
+        var stack = new Error().stack;
+        // IE11 does not initialize 'Error.stack' until the object is thrown.
+        if (!stack) {
+            try {
+                throw new Error();
+            } catch (e) {
+                stack = e.stack;
+            }
+        }
+
+        // 'Error.stack' is not supported in all browsers/versions
+        if (!stack) {
+            return "(Stack trace unavailable)";
+        }
+
+        var lines = stack.split("\n");
+
+        // Create a pattern to match stack frames originating within testharness.js.  These include the
+        // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21').
+        // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+        // in case it contains RegExp characters.
+        var script_url = get_script_url();
+        var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js";
+        var re = new RegExp(re_text + ":\\d+:\\d+");
+
+        // Some browsers include a preamble that specifies the type of the error object.  Skip this by
+        // advancing until we find the first stack frame originating from testharness.js.
+        var i = 0;
+        while (!re.test(lines[i]) && i < lines.length) {
+            i++;
+        }
+
+        // Then skip the top frames originating from testharness.js to begin the stack at the test code.
+        while (re.test(lines[i]) && i < lines.length) {
+            i++;
+        }
+
+        // Paranoid check that we didn't skip all frames.  If so, return the original stack unmodified.
+        if (i >= lines.length) {
+            return stack;
+        }
+
+        return lines.slice(i).join("\n");
+    }
+
+    function PreconditionFailedError(message)
+    {
+        AssertionError.call(this, message);
+    }
+    PreconditionFailedError.prototype = Object.create(AssertionError.prototype);
+    expose(PreconditionFailedError, "PreconditionFailedError");
+
+    function make_message(function_name, description, error, substitutions)
+    {
+        for (var p in substitutions) {
+            if (substitutions.hasOwnProperty(p)) {
+                substitutions[p] = format_value(substitutions[p]);
+            }
+        }
+        var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
+                                   merge({function_name:function_name,
+                                          description:(description?description + " ":"")},
+                                          substitutions));
+        return node_form.slice(1).join("");
+    }
+
+    function filter(array, callable, thisObj) {
+        var rv = [];
+        for (var i = 0; i < array.length; i++) {
+            if (array.hasOwnProperty(i)) {
+                var pass = callable.call(thisObj, array[i], i, array);
+                if (pass) {
+                    rv.push(array[i]);
+                }
+            }
+        }
+        return rv;
+    }
+
+    function map(array, callable, thisObj)
+    {
+        var rv = [];
+        rv.length = array.length;
+        for (var i = 0; i < array.length; i++) {
+            if (array.hasOwnProperty(i)) {
+                rv[i] = callable.call(thisObj, array[i], i, array);
+            }
+        }
+        return rv;
+    }
+
+    function extend(array, items)
+    {
+        Array.prototype.push.apply(array, items);
+    }
+
+    function forEach(array, callback, thisObj)
+    {
+        for (var i = 0; i < array.length; i++) {
+            if (array.hasOwnProperty(i)) {
+                callback.call(thisObj, array[i], i, array);
+            }
+        }
+    }
+
+    /**
+     * Immediately invoke a "iteratee" function with a series of values in
+     * parallel and invoke a final "done" function when all of the "iteratee"
+     * invocations have signaled completion.
+     *
+     * If all callbacks complete synchronously (or if no callbacks are
+     * specified), the `done_callback` will be invoked synchronously. It is the
+     * responsibility of the caller to ensure asynchronicity in cases where
+     * that is desired.
+     *
+     * @param {array} value Zero or more values to use in the invocation of
+     *                      `iter_callback`
+     * @param {function} iter_callback A function that will be invoked once for
+     *                                 each of the provided `values`. Two
+     *                                 arguments will be available in each
+     *                                 invocation: the value from `values` and
+     *                                 a function that must be invoked to
+     *                                 signal completion
+     * @param {function} done_callback A function that will be invoked after
+     *                                 all operations initiated by the
+     *                                 `iter_callback` function have signaled
+     *                                 completion
+     */
+    function all_async(values, iter_callback, done_callback)
+    {
+        var remaining = values.length;
+
+        if (remaining === 0) {
+            done_callback();
+        }
+
+        forEach(values,
+                function(element) {
+                    var invoked = false;
+                    var elDone = function() {
+                        if (invoked) {
+                            return;
+                        }
+
+                        invoked = true;
+                        remaining -= 1;
+
+                        if (remaining === 0) {
+                            done_callback();
+                        }
+                    };
+
+                    iter_callback(element, elDone);
+                });
+    }
+
+    function merge(a,b)
+    {
+        var rv = {};
+        var p;
+        for (p in a) {
+            rv[p] = a[p];
+        }
+        for (p in b) {
+            rv[p] = b[p];
+        }
+        return rv;
+    }
+
+    function expose(object, name)
+    {
+        var components = name.split(".");
+        var target = global_scope;
+        for (var i = 0; i < components.length - 1; i++) {
+            if (!(components[i] in target)) {
+                target[components[i]] = {};
+            }
+            target = target[components[i]];
+        }
+        target[components[components.length - 1]] = object;
+    }
+
+    function is_same_origin(w) {
+        try {
+            'random_prop' in w;
+            return true;
+        } catch (e) {
+            return false;
+        }
+    }
+
+    /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */
+    function get_script_url()
+    {
+        if (!('document' in global_scope)) {
+            return undefined;
+        }
+
+        var scripts = document.getElementsByTagName("script");
+        for (var i = 0; i < scripts.length; i++) {
+            var src;
+            if (scripts[i].src) {
+                src = scripts[i].src;
+            } else if (scripts[i].href) {
+                //SVG case
+                src = scripts[i].href.baseVal;
+            }
+
+            var matches = src && src.match(/^(.*\/|)testharness\.js$/);
+            if (matches) {
+                return src;
+            }
+        }
+        return undefined;
+    }
+
+    /** Returns the <title> or filename or "Untitled" */
+    function get_title()
+    {
+        if ('document' in global_scope) {
+            //Don't use document.title to work around an Opera bug in XHTML documents
+            var title = document.getElementsByTagName("title")[0];
+            if (title && title.firstChild && title.firstChild.data) {
+                return title.firstChild.data;
+            }
+        }
+        if ('META_TITLE' in global_scope && META_TITLE) {
+            return META_TITLE;
+        }
+        if ('location' in global_scope) {
+            return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.'));
+        }
+        return "Untitled";
+    }
+
+    function supports_post_message(w)
+    {
+        var supports;
+        var type;
+        // Given IE implements postMessage across nested iframes but not across
+        // windows or tabs, you can't infer cross-origin communication from the presence
+        // of postMessage on the current window object only.
+        //
+        // Touching the postMessage prop on a window can throw if the window is
+        // not from the same origin AND post message is not supported in that
+        // browser. So just doing an existence test here won't do, you also need
+        // to wrap it in a try..catch block.
+        try {
+            type = typeof w.postMessage;
+            if (type === "function") {
+                supports = true;
+            }
+
+            // IE8 supports postMessage, but implements it as a host object which
+            // returns "object" as its `typeof`.
+            else if (type === "object") {
+                supports = true;
+            }
+
+            // This is the case where postMessage isn't supported AND accessing a
+            // window property across origins does NOT throw (e.g. old Safari browser).
+            else {
+                supports = false;
+            }
+        } catch (e) {
+            // This is the case where postMessage isn't supported AND accessing a
+            // window property across origins throws (e.g. old Firefox browser).
+            supports = false;
+        }
+        return supports;
+    }
+
+    /**
+     * Setup globals
+     */
+
+    var tests = new Tests();
+
+    if (global_scope.addEventListener) {
+        var error_handler = function(error, message, stack) {
+            var precondition_failed = error instanceof PreconditionFailedError;
+            if (tests.file_is_test) {
+                var test = tests.tests[0];
+                if (test.phase >= test.phases.HAS_RESULT) {
+                    return;
+                }
+                var status = precondition_failed ? test.PRECONDITION_FAILED : test.FAIL;
+                test.set_status(status, message, stack);
+                test.phase = test.phases.HAS_RESULT;
+            } else if (!tests.allow_uncaught_exception) {
+                var status = precondition_failed ? tests.status.PRECONDITION_FAILED : tests.status.ERROR;
+                tests.status.status = status;
+                tests.status.message = message;
+                tests.status.stack = stack;
+            }
+
+            // Do not transition to the "complete" phase if the test has been
+            // configured to allow uncaught exceptions. This gives the test an
+            // opportunity to define subtests based on the exception reporting
+            // behavior.
+            if (!tests.allow_uncaught_exception) {
+                done();
+            }
+        };
+
+        addEventListener("error", function(e) {
+            var message = e.message;
+            var stack;
+            if (e.error && e.error.stack) {
+                stack = e.error.stack;
+            } else {
+                stack = e.filename + ":" + e.lineno + ":" + e.colno;
+            }
+            error_handler(e.error, message, stack);
+        }, false);
+
+        addEventListener("unhandledrejection", function(e) {
+            var message;
+            if (e.reason && e.reason.message) {
+                message = "Unhandled rejection: " + e.reason.message;
+            } else {
+                message = "Unhandled rejection";
+            }
+            var stack;
+            if (e.reason && e.reason.stack) {
+                stack = e.reason.stack;
+            }
+            error_handler(e.reason, message, stack);
+        }, false);
+    }
+
+    test_environment.on_tests_ready();
+
+    /**
+     * Stylesheet
+     */
+     var stylesheetContent = "\
+html {\
+    font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;\
+}\
+\
+#log .warning,\
+#log .warning a {\
+  color: black;\
+  background: yellow;\
+}\
+\
+#log .error,\
+#log .error a {\
+  color: white;\
+  background: red;\
+}\
+\
+section#summary {\
+    margin-bottom:1em;\
+}\
+\
+table#results {\
+    border-collapse:collapse;\
+    table-layout:fixed;\
+    width:100%;\
+}\
+\
+table#results th:first-child,\
+table#results td:first-child {\
+    width:8em;\
+}\
+\
+table#results th:last-child,\
+table#results td:last-child {\
+    width:50%;\
+}\
+\
+table#results.assertions th:last-child,\
+table#results.assertions td:last-child {\
+    width:35%;\
+}\
+\
+table#results th {\
+    padding:0;\
+    padding-bottom:0.5em;\
+    border-bottom:medium solid black;\
+}\
+\
+table#results td {\
+    padding:1em;\
+    padding-bottom:0.5em;\
+    border-bottom:thin solid black;\
+}\
+\
+tr.pass > td:first-child {\
+    color:green;\
+}\
+\
+tr.fail > td:first-child {\
+    color:red;\
+}\
+\
+tr.timeout > td:first-child {\
+    color:red;\
+}\
+\
+tr.notrun > td:first-child {\
+    color:blue;\
+}\
+\
+tr.preconditionfailed > td:first-child {\
+    color:blue;\
+}\
+\
+.pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child, .preconditionfailed > td:first-child {\
+    font-variant:small-caps;\
+}\
+\
+table#results span {\
+    display:block;\
+}\
+\
+table#results span.expected {\
+    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
+    white-space:pre;\
+}\
+\
+table#results span.actual {\
+    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
+    white-space:pre;\
+}\
+\
+span.ok {\
+    color:green;\
+}\
+\
+tr.error {\
+    color:red;\
+}\
+\
+span.timeout {\
+    color:red;\
+}\
+\
+span.ok, span.timeout, span.error {\
+    font-variant:small-caps;\
+}\
+";
+
+})(this);
+// vim: set expandtab shiftwidth=4 tabstop=4:
index dc11af565507929a0492f172a8a8c89d925d4d1a..def2dc528187bbf4d1adb0d0b5eab45376095227 100755 (executable)
@@ -12,110 +12,159 @@ Based on a test from the WebKit test suite
 <html class="a">
  <head>
   <title>GainNode interface</title>
-  <script src="../../../../resources/testharness.js"></script>
+  <script src="../resources/testharness.js"></script>
   <script src="../../../../resources/testharnessreport.js"></script>
-  <script src="../../js/lodash.js"></script>
-  <script src="../../js/vendor-prefixes.js"></script>
-  <script src="../../js/helpers.js"></script>
-  <script src="../../js/buffer-loader.js"></script>
+  <script src="../resources/audit-util.js"></script>
+  <script src="../resources/audit.js"></script>
  </head>
- <body class="a">
-  <div id="log"></div>
-  <script>
-var gainNodeTest = async_test("GainNode");
-
-var sampleRate = 44100.0;
-var bufferDurationSeconds = 0.125;
-var numberOfNotes = 11;
-var noteSpacing = bufferDurationSeconds + 0.020; // leave 20ms of silence between each "note"
-var lengthInSeconds = numberOfNotes * noteSpacing;
-
-var context = 0;
-var expectedBuffer = 0;
-var actualBuffer = 0;
-var sinWaveBuffer = 0;
-
-function createSinWaveBuffer(lengthInSeconds, frequency) {
-  var audioBuffer = context.createBuffer(2, lengthInSeconds * sampleRate, sampleRate);
-
-  var n = audioBuffer.length;
-  var channelL = audioBuffer.getChannelData(0);
-  var channelR = audioBuffer.getChannelData(1);
-
-  for (var i = 0; i < n; ++i) {
-    channelL[i] = Math.sin(frequency * 2.0*Math.PI * i / sampleRate);
-    channelR[i] = channelL[i];
-  }
-
-  return audioBuffer;
-}
-
-function playNote(time, gain) {
-  var source = context.createBufferSource();
-  source.buffer = sinWaveBuffer;
-
-  var gainNode = context.createGain();
-  gainNode.gain.value = gain;
-
-  source.connect(gainNode);
-  gainNode.connect(context.destination);
-
-  source.start(time);
-}
-
-function loadExpectedBuffer(event) {
-  actualBuffer = event.renderedBuffer;
-
-  bufferLoader = new BufferLoader(
-    context,
-    ['./gain-expected.wav'],
-    bufferLoadCompleted
-  );
-  bufferLoader.load();
-};
-
-function bufferLoadCompleted(buffer) {
-  compareExpectedWithActualBuffer(buffer);
-};
-
-setup( function() {
-  // Create offline audio context.
-  context = new OfflineAudioContext(2, sampleRate * lengthInSeconds, sampleRate);
-
-  // Create a buffer for a short "note".
-  sinWaveBuffer = createSinWaveBuffer(bufferDurationSeconds, 880.0);
-
-  // Render 11 notes, starting at a gain of 1.0, decreasing in gain by 0.1.
-  // The last note will be of gain 0.0, so shouldn't be perceptible in the rendered output.
-  for (var i = 0; i < numberOfNotes; ++i) {
-    var time = i * noteSpacing;
-    var gain = 1.0 - i / (numberOfNotes - 1);
-    playNote(time, gain);
-  }
-
-  context.oncomplete = loadExpectedBuffer;
-  context.startRendering();
-}, {timeout: 10000});
-
-function compareExpectedWithActualBuffer(expected) {
-  var expectedBuffer = expected[0];
-
-  gainNodeTest.step(function() {
-    assert_array_approx_equals(expectedBuffer.getChannelData(0),
-                               actualBuffer.getChannelData(0),
-                               1e-4,
-                               "comparing expected and rendered buffers (channel 0)");
-  });
-
-  gainNodeTest.step(function() {
-    assert_array_approx_equals(expectedBuffer.getChannelData(1),
-                               actualBuffer.getChannelData(1),
-                               1e-4,
-                               "comparing expected and rendered buffers (channel 1)");
-  });
-
-  gainNodeTest.done();
-};
-  </script>
- </body>
+  <body>
+    <script id="layout-test-code">
+      // Tests that GainNode is properly scaling the gain.  We'll render 11
+      // notes, starting at a gain of 1.0, decreasing in gain by 0.1.  The 11th
+      // note will be of gain 0.0, so it should be silent (at the end in the
+      // rendered output).
+
+      let audit = Audit.createTaskRunner();
+
+      // Use a power of two to eliminate any round-off when converting frame to
+      // time.
+      let sampleRate = 32768;
+      // Make sure the buffer duration and spacing are all exact frame lengths
+      // so that the note spacing is also on frame boundaries to eliminate
+      // sub-sample accurate start of a ABSN.
+      let bufferDurationSeconds = Math.floor(0.125 * sampleRate) / sampleRate;
+      let numberOfNotes = 11;
+      // Leave about 20ms of silence, being sure this is an exact frame
+      // duration.
+      let noteSilence = Math.floor(0.020 * sampleRate) / sampleRate;
+      let noteSpacing = bufferDurationSeconds + noteSilence;
+
+      let lengthInSeconds = numberOfNotes * noteSpacing;
+
+      let context = 0;
+      let sinWaveBuffer = 0;
+
+      // Create a stereo AudioBuffer of duration |lengthInSeconds| consisting of
+      // a pure sine wave with the given |frequency|.  Both channels contain the
+      // same data.
+      function createSinWaveBuffer(lengthInSeconds, frequency) {
+        let audioBuffer =
+            context.createBuffer(2, lengthInSeconds * sampleRate, sampleRate);
+
+        let n = audioBuffer.length;
+        let channelL = audioBuffer.getChannelData(0);
+        let channelR = audioBuffer.getChannelData(1);
+
+        for (let i = 0; i < n; ++i) {
+          channelL[i] = Math.sin(frequency * 2.0 * Math.PI * i / sampleRate);
+          channelR[i] = channelL[i];
+        }
+
+        return audioBuffer;
+      }
+
+      function playNote(time, gain, merger) {
+        let source = context.createBufferSource();
+        source.buffer = sinWaveBuffer;
+
+        let gainNode = context.createGain();
+        gainNode.gain.value = gain;
+
+        let sourceSplitter = context.createChannelSplitter(2);
+        let gainSplitter = context.createChannelSplitter(2);
+
+        // Split the stereo channels from the source output and the gain output
+        // and merge them into the desired channels of the merger.
+        source.connect(gainNode).connect(gainSplitter);
+        source.connect(sourceSplitter);
+
+        gainSplitter.connect(merger, 0, 0);
+        gainSplitter.connect(merger, 1, 1);
+        sourceSplitter.connect(merger, 0, 2);
+        sourceSplitter.connect(merger, 1, 3);
+
+        source.start(time);
+      }
+
+      audit.define(
+          {label: 'create context', description: 'Create context for test'},
+          function(task, should) {
+            // Create offline audio context.
+            context = new OfflineAudioContext(
+                4, sampleRate * lengthInSeconds, sampleRate);
+            task.done();
+          });
+
+      audit.define(
+          {label: 'test', description: 'GainNode functionality'},
+          function(task, should) {
+            let merger = new ChannelMergerNode(
+                context, {numberOfInputs: context.destination.channelCount});
+            merger.connect(context.destination);
+
+            // Create a buffer for a short "note".
+            sinWaveBuffer = createSinWaveBuffer(bufferDurationSeconds, 880.0);
+
+            let startTimes = [];
+            let gainValues = [];
+
+            // Render 11 notes, starting at a gain of 1.0, decreasing in gain by
+            // 0.1. The last note will be of gain 0.0, so shouldn't be
+            // perceptible in the rendered output.
+            for (let i = 0; i < numberOfNotes; ++i) {
+              let time = i * noteSpacing;
+              let gain = 1.0 - i / (numberOfNotes - 1);
+              startTimes.push(time);
+              gainValues.push(gain);
+              playNote(time, gain, merger);
+            }
+
+            context.startRendering()
+                .then(buffer => {
+                  let actual0 = buffer.getChannelData(0);
+                  let actual1 = buffer.getChannelData(1);
+                  let reference0 = buffer.getChannelData(2);
+                  let reference1 = buffer.getChannelData(3);
+
+                  // It's ok to a frame too long since the sine pulses are
+                  // followed by silence.
+                  let bufferDurationFrames =
+                      Math.ceil(bufferDurationSeconds * context.sampleRate);
+
+                  // Apply the gains to the reference signal.
+                  for (let k = 0; k < startTimes.length; ++k) {
+                    // It's ok to be a frame early because the sine pulses are
+                    // preceded by silence.
+                    let startFrame =
+                        Math.floor(startTimes[k] * context.sampleRate);
+                    let gain = gainValues[k];
+                    for (let n = 0; n < bufferDurationFrames; ++n) {
+                      reference0[startFrame + n] *= gain;
+                      reference1[startFrame + n] *= gain;
+                    }
+                  }
+
+                  // Verify the channels are clsoe to the reference.
+                  should(actual0, 'Left output from gain node')
+                      .beCloseToArray(
+                          reference0, {relativeThreshold: 1.1877e-7});
+                  should(actual1, 'Right output from gain node')
+                      .beCloseToArray(
+                          reference1, {relativeThreshold: 1.1877e-7});
+
+                  // Test the SNR too for both channels.
+                  let snr0 = 10 * Math.log10(computeSNR(actual0, reference0));
+                  let snr1 = 10 * Math.log10(computeSNR(actual1, reference1));
+                  should(snr0, 'Left SNR (in dB)')
+                      .beGreaterThanOrEqualTo(148.71);
+                  should(snr1, 'Right SNR (in dB)')
+                      .beGreaterThanOrEqualTo(148.71);
+                })
+                .then(() => task.done());
+            ;
+          });
+
+      audit.run();
+    </script>
+  </body>
 </html>