diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c8e529a4f..74118d65de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -209,8 +209,9 @@ This key is for boolean properties associated with the test. - **raw** - execute the test without any modification (no harness files will be included); necessary to test the behavior of directive prologue; implies `noStrict` -- **async** - defer interpretation of test results until after the invocation - of the global `$DONE` function +- **async** - defer interpretation of test results until settlement of an + `asyncTest` callback promise or manual invocation of `$DONE`; refer to + [Writing Asynchronous Tests](#writing-asynchronous-tests) for details - **generated** - informative flag used to denote test files that were created procedurally using the project's test generation tool; refer to [Procedurally-generated tests](#procedurally-generated-tests) @@ -346,7 +347,7 @@ Consumers that violate the spec by throwing exceptions for parsing errors at run An asynchronous test is any test that include the `async` frontmatter flag. -For most asynchronous tests, the `asyncHelpers.js` harness file includes an `asyncTest` method that precludes needing to interact with the test runner via the `$DONE` function. `asyncTest` takes an async function and will ensure that `$DONE` is called properly if the async function returns or throws an exception. For example, a test written using `asyncTest` might look like: +Most asynchronous tests should include the `asyncHelpers.js` harness file and call its `asyncTest` function **exactly once**, with a callback returning a promise that indicates test failure via rejection and otherwise fulfills upon test conclusion (such as an async function). ```js /*--- diff --git a/harness/asyncHelpers.js b/harness/asyncHelpers.js index c8e58457fc..b2efdf8061 100644 --- a/harness/asyncHelpers.js +++ b/harness/asyncHelpers.js @@ -6,6 +6,14 @@ description: | defines: [asyncTest] ---*/ +/** + * Defines the **sole** asynchronous test of a file. + * @see {@link ../docs/rfcs/async-helpers.md} for background. + * + * @param {Function} testFunc a callback whose returned promise indicates test results + * (fulfillment for success, rejection for failure) + * @returns {void} + */ function asyncTest(testFunc) { if (!Object.hasOwn(globalThis, "$DONE")) { throw new Test262Error("asyncTest called without async flag"); @@ -28,86 +36,62 @@ function asyncTest(testFunc) { } } +/** + * Asserts that a callback asynchronously throws an instance of a particular + * error (i.e., returns a promise whose rejection value is an object referencing + * the constructor). + * + * @param {Function} expectedErrorConstructor the expected constructor of the + * rejection value + * @param {Function} func the callback + * @param {string} [message] the prefix to use for failure messages + * @returns {Promise} fulfills if the expected error is thrown, + * otherwise rejects + */ assert.throwsAsync = function (expectedErrorConstructor, func, message) { return new Promise(function (resolve) { - var innerThenable; - if (message === undefined) { - message = ""; - } else { - message += " "; - } - if (typeof func === "function") { - try { - innerThenable = func(); - if ( - innerThenable === null || - typeof innerThenable !== "object" || - typeof innerThenable.then !== "function" - ) { - message += - "Expected to obtain an inner promise that would reject with a" + - expectedErrorConstructor.name + - " but result was not a thenable"; - throw new Test262Error(message); - } - } catch (thrown) { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but an exception was thrown synchronously while obtaining the inner promise"; - throw new Test262Error(message); + var expectedName = expectedErrorConstructor.name; + var expectation = "Expected a " + expectedName + " to be thrown asynchronously"; + var fail = function (detail) { + if (message === undefined) { + throw new Test262Error(detail); } - } else { - message += - "assert.throwsAsync called with an argument that is not a function"; - throw new Test262Error(message); + throw new Test262Error(message + " " + detail); + }; + var res; + if (typeof func !== "function") { + fail("assert.throwsAsync called with an argument that is not a function"); + } + try { + res = func(); + } catch (thrown) { + fail(expectation + " but the function threw synchronously"); + } + if (res === null || typeof res !== "object" || typeof res.then !== "function") { + fail(expectation + " but result was not a thenable"); } try { - resolve(innerThenable.then( + resolve(res.then( function () { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but no exception was thrown at all"; - throw new Test262Error(message); + fail(expectation + " but no exception was thrown at all"); }, function (thrown) { - var expectedName, actualName; - if (typeof thrown !== "object" || thrown === null) { - message += "Thrown value was not an object!"; - throw new Test262Error(message); + var actualName; + if (thrown === null || typeof thrown !== "object") { + fail(expectation + " but thrown value was not an object"); } else if (thrown.constructor !== expectedErrorConstructor) { - expectedName = expectedErrorConstructor.name; actualName = thrown.constructor.name; if (expectedName === actualName) { - message += - "Expected a " + - expectedName + - " but got a different error constructor with the same name"; - } else { - message += - "Expected a " + expectedName + " but got a " + actualName; + fail(expectation + + " but got a different error constructor with the same name"); } - throw new Test262Error(message); + fail(expectation + " but got a " + actualName); } } )); } catch (thrown) { - if (typeof thrown !== "object" || thrown === null) { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but innerThenable synchronously threw a value that was not an object "; - } else { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but a " + - thrown.constructor.name + - " was thrown synchronously"; - } - throw new Test262Error(message); + fail(expectation + " but .then threw synchronously"); } }); };