зеркало из https://github.com/mozilla/gecko-dev.git
1415 строки
44 KiB
JavaScript
1415 строки
44 KiB
JavaScript
// 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 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', '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.
|
|
for (let j = 0; j < this._expected.length; j++) {
|
|
if (this._expected[j] !== indexedActual[j].value) {
|
|
firstErrorIndex = indexedActual[j].index;
|
|
passed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return this._assert(
|
|
passed,
|
|
'${actual} contains all the expected values in the correct order: ' +
|
|
'${expected}.',
|
|
'${actual} expected to have the value sequence of ${expected} but ' +
|
|
'got ' + this._actual[firstErrorIndex] + ' at index ' +
|
|
firstErrorIndex + '.');
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
this._state = TaskState.STARTED;
|
|
|
|
// Print out the task entry with label and description.
|
|
_logPassed(
|
|
'> [' + this._label + '] ' +
|
|
(this._description ? this._description : ''));
|
|
|
|
this._taskFunction(this, this.should.bind(this));
|
|
}
|
|
|
|
// 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() {
|
|
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._taskRunner._runNextTask();
|
|
}
|
|
|
|
// 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.
|
|
timeout(subTask, time) {
|
|
async_test((test) => {
|
|
test.step_timeout(() => {
|
|
subTask();
|
|
test.done();
|
|
}, 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 = [];
|
|
this._currentTaskIndex = -1;
|
|
|
|
// Configure testharness.js for the async operation.
|
|
setup(new Function(), {explicit_done: true});
|
|
}
|
|
|
|
_runNextTask() {
|
|
if (this._currentTaskIndex < this._taskSequence.length) {
|
|
this._tasks[this._taskSequence[this._currentTaskIndex++]].run();
|
|
} else {
|
|
this._finish();
|
|
}
|
|
}
|
|
|
|
_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.');
|
|
}
|
|
|
|
// From testharness.js, report back to the test infrastructure that
|
|
// the task runner completed all the tasks.
|
|
_testharnessDone();
|
|
}
|
|
|
|
// |taskLabel| can be either a string or a dictionary. See Task constructor
|
|
// for the detail.
|
|
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;
|
|
}
|
|
|
|
// Start the first task.
|
|
this._currentTaskIndex = 0;
|
|
this._runNextTask();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
|
|
};
|
|
|
|
})();
|