diff --git a/build/BUILD.wpt b/build/BUILD.wpt index 5084f448a45..757b227041c 100644 --- a/build/BUILD.wpt +++ b/build/BUILD.wpt @@ -2,21 +2,19 @@ # Licensed under the Apache 2.0 license found in the LICENSE file or at: # https://opensource.org/licenses/Apache-2.0 -load("@workerd//:build/wpt_test.bzl", "wpt_get_directories") - -[filegroup( - name = dir, - srcs = glob(["{}/**/*".format(dir)]), - visibility = ["//visibility:public"], -) for dir in wpt_get_directories( - excludes = [ - "dom", +directories = glob( + ["*"], + exclude = glob( + ["*"], + exclude_directories = 1, + ) + [ + ".*", ], - root = "", -)] + exclude_directories = 0, +) [filegroup( name = dir, srcs = glob(["{}/**/*".format(dir)]), visibility = ["//visibility:public"], -) for dir in wpt_get_directories(root = "dom")] +) for dir in directories] diff --git a/build/wpt_test.bzl b/build/wpt_test.bzl index 991e3ca2cf4..d2d5d5abd58 100644 --- a/build/wpt_test.bzl +++ b/build/wpt_test.bzl @@ -16,8 +16,6 @@ def wpt_test(name, wpt_directory, test_config): js_test_gen_rule = "{}@_wpt_js_test_gen".format(name) test_config_as_js = test_config.removesuffix(".ts") + ".js" - harness = "//src/wpt:wpt-test-harness" - compat_date = "//src/workerd/io:trimmed-supported-compatibility-date.txt" _wpt_js_test_gen( name = js_test_gen_rule, @@ -33,41 +31,22 @@ def wpt_test(name, wpt_directory, test_config): wpt_directory = wpt_directory, test_config = test_config_as_js, test_js_generated = js_test_gen_rule, - harness = harness, - compat_date = compat_date, ) wd_test( name = "{}".format(name), src = wd_test_gen_rule, args = ["--experimental"], - ts_deps = [harness], + ts_deps = ["//src/wpt:wpt-test-harness"], data = [ - harness, + "//src/wpt:wpt-test-harness", test_config, js_test_gen_rule, wpt_directory, - compat_date, + "//src/workerd/io:trimmed-supported-compatibility-date.txt", ], ) -def wpt_get_directories(root, excludes = []): - """ - Globs for files within a WPT directory structure, starting from root. - In addition to an explicitly provided excludes argument, hidden directories - and top-level files are also excluded as they don't contain test content. - """ - - root_pattern = "{}/*".format(root) if root else "*" - return native.glob( - [root_pattern], - exclude = native.glob( - [root_pattern], - exclude_directories = 1, - ) + [".*"] + excludes, - exclude_directories = 0, - ) - def _wpt_js_test_gen_impl(ctx): """ Generates a workerd test suite in JS. This contains the logic to run @@ -96,7 +75,7 @@ def generate_external_cases(files): for file in files.to_list(): if file.extension == "js": - entry = """export const {} = run('{}');""".format(test_case_name(file.basename), file.basename) + entry = """export const {} = run(config, '{}');""".format(test_case_name(file.basename), file.basename) result.append(entry) return "\n".join(result) @@ -118,11 +97,9 @@ def test_case_name(filename): WPT_JS_TEST_TEMPLATE = """// This file is autogenerated by wpt_test.bzl // DO NOT EDIT. -import {{ createRunner }} from 'wpt:harness'; +import {{ run }} from 'wpt:harness'; import config from '{test_config}'; -const run = createRunner(config); - {cases} """ @@ -132,16 +109,15 @@ def _wpt_wd_test_gen_impl(ctx): paths to modules needed to run the test: generated test suite, test config file, WPT test scripts, associated JSON resources. """ + src = ctx.actions.declare_file("{}.wd-test".format(ctx.attr.test_name)) ctx.actions.write( output = src, content = WPT_WD_TEST_TEMPLATE.format( test_name = ctx.attr.test_name, test_config = ctx.file.test_config.basename, - test_js_generated = ctx.file.test_js_generated.basename, - bindings = generate_external_bindings(ctx.file.test_config, ctx.attr.wpt_directory.files), - harness = wd_relative_path(ctx.file.test_config, ctx.file.harness), - compat_date = wd_relative_path(ctx.file.test_config, ctx.file.compat_date), + test_js_generated = wd_relative_path(ctx.file.test_js_generated), + bindings = generate_external_bindings(ctx.attr.wpt_directory.files), ), ) @@ -158,14 +134,14 @@ const unitTests :Workerd.Config = ( modules = [ (name = "worker", esModule = embed "{test_js_generated}"), (name = "{test_config}", esModule = embed "{test_config}"), - (name = "wpt:harness", esModule = embed "{harness}"), + (name = "wpt:harness", esModule = embed "../../../../../workerd/src/wpt/harness.js"), ], bindings = [ (name = "wpt", service = "wpt"), (name = "unsafe", unsafeEval = void), {bindings} ], - compatibilityDate = embed "{compat_date}", + compatibilityDate = embed "../../../../../workerd/src/workerd/io/trimmed-supported-compatibility-date.txt", compatibilityFlags = ["nodejs_compat", "experimental"], ) ), @@ -176,15 +152,15 @@ const unitTests :Workerd.Config = ( ], );""" -def wd_relative_path(origin, target): +def wd_relative_path(file): """ - Generates a relative path for use in wd-test files. - The origin is the directory containing the wd-test file, and the target is the file that should be referenced from it. + Returns a relative path which can be referenced in the .wd-test file. + This is four directories up from the bazel short_path """ - return "../" * origin.short_path.count("/") + target.short_path + return "../" * 4 + file.short_path -def generate_external_bindings(origin, files): +def generate_external_bindings(files): """ Generates appropriate bindings for each file in the WPT module: - JS files: text binding to allow code to be evaluated @@ -194,7 +170,7 @@ def generate_external_bindings(origin, files): result = [] for file in files.to_list(): - file_path = wd_relative_path(origin, file) + file_path = wd_relative_path(file) if file.extension == "js": entry = """(name = "{}", text = embed "{}")""".format(file.basename, file_path) elif file.extension == "json": @@ -219,10 +195,6 @@ _wpt_wd_test_gen = rule( "test_config": attr.label(allow_single_file = True), # An auto-generated JS file containing the test logic. "test_js_generated": attr.label(allow_single_file = True), - # Target specifying the location of the WPT test harness - "harness": attr.label(allow_single_file = True), - # Target specifying the location of the trimmed-supported-compatibility-date.txt file - "compat_date": attr.label(allow_single_file = True), }, ) diff --git a/src/workerd/api/wpt/BUILD.bazel b/src/workerd/api/wpt/BUILD.bazel index 190b0b015d1..e59b7d0375f 100644 --- a/src/workerd/api/wpt/BUILD.bazel +++ b/src/workerd/api/wpt/BUILD.bazel @@ -5,7 +5,7 @@ load("@npm//:eslint/package_json.bzl", eslint_bin = "bin") load("//:build/wpt_test.bzl", "wpt_test") -srcs = glob(["**/*-test.ts"]) +srcs = glob(["*-test.ts"]) [wpt_test( name = file.replace("-test.ts", ""), diff --git a/src/workerd/api/wpt/dom/abort-test.ts b/src/workerd/api/wpt/dom/abort-test.ts deleted file mode 100644 index 6010272f10a..00000000000 --- a/src/workerd/api/wpt/dom/abort-test.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2017-2022 Cloudflare, Inc. -// Licensed under the Apache 2.0 license found in the LICENSE file or at: -// https://opensource.org/licenses/Apache-2.0 - -import { type TestRunnerConfig } from 'wpt:harness'; - -export default { - 'AbortSignal.any.js': {}, - 'abort-signal-any-tests.js': {}, - 'abort-signal-any.any.js': { - comment: - '(1, 2) Target should be set to signal. (3) Should be investigated.', - expectedFailures: [ - 'AbortSignal.any() follows a single signal (using AbortController)', - 'AbortSignal.any() follows multiple signals (using AbortController)', - 'Abort events for AbortSignal.any() signals fire in the right order (using AbortController)', - ], - includeFile: 'abort-signal-any-tests.js', - }, - 'event.any.js': { - comment: 'Target should be set to signal', - expectedFailures: ['the abort event should have the right properties'], - }, - 'timeout-shadowrealm.any.js': { - comment: 'Enable when ShadowRealm is implemented', - skipAllTests: true, - }, - 'timeout.any.js': {}, -} satisfies TestRunnerConfig; diff --git a/src/workerd/api/wpt/eslint.config.mjs b/src/workerd/api/wpt/eslint.config.mjs index 8f313caa39c..666098b722b 100644 --- a/src/workerd/api/wpt/eslint.config.mjs +++ b/src/workerd/api/wpt/eslint.config.mjs @@ -3,7 +3,7 @@ import { baseConfig } from '../../../../tools/base.eslint.config.mjs'; export default [ ...baseConfig({ tsconfigRootDir: import.meta.dirname }), { - files: ['src/workerd/api/wpt/**/*-test.ts'], + files: ['src/workerd/api/wpt/*-test.js'], rules: { 'sort-keys': 'error', }, diff --git a/src/workerd/api/wpt/tsconfig.json b/src/workerd/api/wpt/tsconfig.json index 73003277b2f..a77d2f82c47 100644 --- a/src/workerd/api/wpt/tsconfig.json +++ b/src/workerd/api/wpt/tsconfig.json @@ -26,6 +26,6 @@ "wpt:*": ["../../../wpt/*"] } }, - "include": ["**/*.ts"], + "include": ["**/*.ts", "../../../wpt/*.ts"], "exclude": [] } diff --git a/src/workerd/api/wpt/url-test.ts b/src/workerd/api/wpt/url-test.ts index 0bccb33fb76..f4f525e21ae 100644 --- a/src/workerd/api/wpt/url-test.ts +++ b/src/workerd/api/wpt/url-test.ts @@ -5,7 +5,6 @@ import { type TestRunnerConfig } from 'wpt:harness'; export default { - 'IdnaTestV2.window.js': {}, 'a-element-origin.js': { comment: 'Implement globalThis.document', skipAllTests: true, @@ -17,6 +16,7 @@ export default { 'historical.any.js': { comment: 'Fix this eventually', expectedFailures: [ + 'Constructor only takes strings', 'URL: no structured serialize/deserialize support', 'URLSearchParams: no structured serialize/deserialize support', ], @@ -36,8 +36,7 @@ export default { ], }, 'percent-encoding.window.js': { - comment: - 'Implement test code modification feature to allow running this test without document', + comment: 'Implement `async_test`', skipAllTests: true, }, 'toascii.window.js': { @@ -50,17 +49,10 @@ export default { 'Parsing: without base', ], }, - 'url-origin.any.js': {}, - 'url-searchparams.any.js': {}, 'url-setters-a-area.window.js': { comment: 'Implement globalThis.document', skipAllTests: true, }, - 'url-setters-stripping.any.js': {}, - 'url-setters.any.js': {}, - 'url-statics-canparse.any.js': {}, - 'url-statics-parse.any.js': {}, - 'url-tojson.any.js': {}, 'urlencoded-parser.any.js': { comment: 'Requests fail due to HTTP method "LADIDA", responses fail due to shift_jis encoding', @@ -137,7 +129,6 @@ export default { 'response.formData() with input: b=%%2a', ], }, - 'urlsearchparams-append.any.js': {}, 'urlsearchparams-constructor.any.js': { comment: 'Fix this eventually', expectedFailures: [ @@ -147,16 +138,8 @@ export default { 'Construct with object with NULL, non-ASCII, and surrogate keys', ], }, - 'urlsearchparams-delete.any.js': {}, - 'urlsearchparams-foreach.any.js': {}, - 'urlsearchparams-get.any.js': {}, - 'urlsearchparams-getall.any.js': {}, - 'urlsearchparams-has.any.js': {}, - 'urlsearchparams-set.any.js': {}, - 'urlsearchparams-size.any.js': {}, 'urlsearchparams-sort.any.js': { comment: 'Investigate url_search_params::sort in ada-url', expectedFailures: ['Parse and sort: ffi&🌈', 'URL parse and sort: ffi&🌈'], }, - 'urlsearchparams-stringifier.any.js': {}, } satisfies TestRunnerConfig; diff --git a/src/workerd/api/wpt/urlpattern-test.ts b/src/workerd/api/wpt/urlpattern-test.ts index 41c66e73b20..3ff59a300f3 100644 --- a/src/workerd/api/wpt/urlpattern-test.ts +++ b/src/workerd/api/wpt/urlpattern-test.ts @@ -37,8 +37,6 @@ export default { 'Component: hash Left: {"hash":"a"} Right: {"hash":"b"}', ], }, - 'urlpattern-compare.tentative.any.js': {}, - 'urlpattern-compare.tentative.https.any.js': {}, 'urlpattern-hasregexpgroups-tests.js': { comment: 'urlpattern implementation will soon be replaced with ada-url', expectedFailures: [ @@ -47,9 +45,6 @@ export default { '', // This file consists of one unnamed subtest ], }, - 'urlpattern-hasregexpgroups.any.js': {}, - 'urlpattern.any.js': {}, - 'urlpattern.https.any.js': {}, 'urlpatterntests.js': { comment: 'urlpattern implementation will soon be replaced with ada-url', expectedFailures: [ diff --git a/src/wpt/harness.ts b/src/wpt/harness.ts index d2d3236031f..0f9b9591ada 100644 --- a/src/wpt/harness.ts +++ b/src/wpt/harness.ts @@ -29,14 +29,12 @@ import { deepStrictEqual, ok, throws, - fail, type AssertPredicate, } from 'node:assert'; type CommonOptions = { comment?: string; verbose?: boolean; - includeFile?: string; }; type SuccessOptions = { @@ -68,204 +66,9 @@ type TestCase = { test(_: unknown, env: Env): Promise; }; -type UnknownFunc = (...args: unknown[]) => unknown; - -/** - * A single subtest. A Test is not constructed directly but via the - * :js:func:`test`, :js:func:`async_test` or :js:func:`promise_test` functions. - * - * @param name - This must be unique in a given file and must be - * invariant between runs. - * - */ -/* eslint-disable @typescript-eslint/no-this-alias -- WPT allows for overriding the this environment for a step but defaults to the Test class */ -class Test { - public static Phases = { - INITIAL: 0, - STARTED: 1, - HAS_RESULT: 2, - CLEANING: 3, - COMPLETE: 4, - } as const; - - public name: string; - public properties: unknown; - public phase: (typeof Test.Phases)[keyof typeof Test.Phases]; - - public error?: Error; - - // For convenience, expose a promise that resolves once done() is called - public isDone: Promise; - private resolve: () => void; - - public constructor(name: string, properties: unknown) { - this.name = name; - this.properties = properties; - this.phase = Test.Phases.INITIAL; - - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- void is being used as a valid generic in this context - const { promise, resolve } = Promise.withResolvers(); - this.isDone = promise; - this.resolve = resolve; - } - - /** - * Run a single step of an ongoing test. - * - * @param func - Callback function to run as a step. If - * this throws an :js:func:`AssertionError`, or any other - * exception, the :js:class:`Test` status is set to ``FAIL``. - * @param [this_obj] - The object to use as the this - * value when calling ``func``. Defaults to the :js:class:`Test` object. - */ - public step( - func: UnknownFunc, - this_obj?: object, - ...rest: unknown[] - ): unknown { - if (this.phase > Test.Phases.STARTED) { - return undefined; - } - - if (arguments.length === 1) { - this_obj = this; - } - - try { - return func.call(this_obj, ...rest); - } catch (err) { - if (this.phase >= Test.Phases.HAS_RESULT) { - return undefined; - } - - this.error = new AggregateError([err], this.name); - this.done(); - } - - return undefined; - } - - /** - * Wrap a function so that it runs as a step of the current test. - * - * This allows creating a callback function that will run as a - * test step. - * - * @example - * let t = async_test("Example"); - * onload = t.step_func(e => { - * assert_equals(e.name, "load"); - * // Mark the test as complete. - * t.done(); - * }) - * - * @param func - Function to run as a step. If this - * throws an :js:func:`AssertionError`, or any other exception, - * the :js:class:`Test` status is set to ``FAIL``. - * @param [this_obj] - The object to use as the this - * value when calling ``func``. Defaults to the :js:class:`Test` object. - */ - public step_func(func: UnknownFunc, this_obj?: object): UnknownFunc { - const test_this = this; - - if (arguments.length === 1) { - this_obj = this; - } - - return function (...params: unknown[]) { - return test_this.step.call(test_this, func, this_obj, ...params); - }; - } - - /** - * Wrap a function so that it runs as a step of the current test, - * and automatically marks the test as complete if the function - * returns without error. - * - * @param func - Function to run as a step. If this - * throws an :js:func:`AssertionError`, or any other exception, - * the :js:class:`Test` status is set to ``FAIL``. If it returns - * without error the status is set to ``PASS``. - * @param [this_obj] - The object to use as the this - * value when calling `func`. Defaults to the :js:class:`Test` object. - */ - public step_func_done(func?: UnknownFunc, this_obj?: object): UnknownFunc { - const test_this = this; - - if (arguments.length === 1) { - this_obj = test_this; - } - - return function (...params: unknown[]) { - if (func) { - test_this.step.call(test_this, func, this_obj, ...params); - } - - test_this.done(); - }; - } - - /** - * Return a function that automatically sets the current test to - * ``FAIL`` if it's called. - * - * @param [description] - Error message to add to assert - * in case of failure. - * - */ - public unreached_func(description?: string): UnknownFunc { - return this.step_func(() => { - assert_unreached(description); - }); - } - - /** - * Run a function as a step of the test after a given timeout. - * - * In general it's encouraged to use :js:func:`Test.step_wait` or - * :js:func:`step_wait_func` in preference to this function where possible, - * as they provide better test performance. - * - * @param func - Function to run as a test - * step. - * @param timeout - Time in ms to wait before running the - * test step. - * - */ - public step_timeout( - func: UnknownFunc, - timeout: number, - ...rest: unknown[] - ): ReturnType { - const test_this = this; - - return setTimeout( - this.step_func(function () { - return func.call(test_this, ...rest); - }), - timeout - ); - } - - public done(): void { - if (this.phase >= Test.Phases.CLEANING) { - return; - } - - this.cleanup(); - } - - public cleanup(): void { - // Actual cleanup support is not yet needed for the WPT modules we support - this.phase = Test.Phases.COMPLETE; - this.resolve(); - } -} -/* eslint-enable @typescript-eslint/no-this-alias */ - type TestRunnerFn = (callback: TestFn | PromiseTestFn, message: string) => void; -type TestFn = UnknownFunc; -type PromiseTestFn = () => Promise; +type TestFn = () => void; +type PromiseTestFn = () => Promise; type ThrowingFn = () => unknown; declare global { @@ -274,10 +77,10 @@ declare global { var testOptions: TestRunnerOptions; var GLOBAL: { isWindow(): boolean }; var env: Env; - var promises: Promise[]; + var promises: { [name: string]: Promise }; /* eslint-enable no-var */ - function test(func: TestFn, name: string, properties?: unknown): void; + function test(func: TestFn, name: string): void; function done(): undefined; function subsetTestByKey( _key: string, @@ -290,7 +93,6 @@ declare global { name: string, properties?: unknown ): void; - function async_test(func: TestFn, name: string, properties?: unknown): void; function assert_equals(a: unknown, b: unknown, message?: string): void; function assert_not_equals(a: unknown, b: unknown, message?: string): void; function assert_true(val: unknown, message?: string): void; @@ -319,17 +121,13 @@ declare global { descriptionOrFunc: string | ThrowingFn, maybeDescription?: string ): void; - function assert_not_own_property( - object: object, - property_name: string, - description?: string - ): void; } /** + * @class * Exception type that represents a failing assert. * NOTE: This a custom error type defined by WPT - it's not the same as node:assert's AssertionError - * @param message - Error message. + * @param {string} message - Error message. */ declare class AssertionError extends Error {} function AssertionError(this: AssertionError, message: string): void { @@ -421,76 +219,15 @@ globalThis.subsetTestByKey = ( return testType(testCallback, testMessage); }; -globalThis.promise_test = (func, name, properties): void => { - if (!shouldRunTest(name)) { - return; - } - - const testCase = new Test(name, properties); - const promise = testCase.step(func, testCase, testCase); - - if (!(promise instanceof Promise)) { - // The functions passed to promise_test are expected to return a Promise, - // but are not required to be async functions. That means they could throw - // an error immediately when run. - - if (testCase.error) { - globalThis.errors.push(testCase.error); - } else { - globalThis.errors.push( - new Error('Unexpected value returned from promise_test') - ); - } - - return; - } - - globalThis.promises.push( - promise.catch((err: unknown) => { - globalThis.errors.push(new AggregateError([err], name)); - }) - ); -}; - -globalThis.async_test = (func, name, properties): void => { +globalThis.promise_test = (func, name, _properties): void => { if (!shouldRunTest(name)) { return; } - const testCase = new Test(name, properties); - testCase.step(func, testCase, testCase); - - globalThis.promises.push( - testCase.isDone.then(() => { - if (testCase.error) { - globalThis.errors.push(testCase.error); - } - }) - ); -}; - -/** - * Create a synchronous test - * - * @param func - Test function. This is executed - * immediately. If it returns without error, the test status is - * set to ``PASS``. If it throws an :js:class:`AssertionError`, or - * any other exception, the test status is set to ``FAIL`` - * (typically from an `assert` function). - * @param name - Test name. This must be unique in a - * given file and must be invariant between runs. - */ -globalThis.test = (func, name, properties): void => { - if (!shouldRunTest(name)) { - return; - } - - const testCase = new Test(name, properties); - testCase.step(func, testCase, testCase); - testCase.done(); - - if (testCase.error) { - globalThis.errors.push(testCase.error); + try { + globalThis.promises[name] = func.call(this); + } catch (err) { + globalThis.errors.push(new AggregateError([err], name)); } }; @@ -527,8 +264,8 @@ globalThis.assert_object_equals = (a, b, message): void => { * * assert_implements(window.Foo, 'Foo is not supported'); * - * @param condition The truthy value to test - * @param [description] Error description for the case that the condition is not truthy. + * @param {object} condition The truthy value to test + * @param {string} [description] Error description for the case that the condition is not truthy. */ globalThis.assert_implements = (condition, description): void => { ok(!!condition, description); @@ -544,8 +281,8 @@ globalThis.assert_implements = (condition, description): void => { * assert_implements_optional(video.canPlayType("video/webm"), * "webm video playback not supported"); * - * @param condition The truthy value to test - * @param [description] Error description for the case that the condition is not truthy. + * @param {object} condition The truthy value to test + * @param {string} [description] Error description for the case that the condition is not truthy. */ globalThis.assert_implements_optional = (condition, description): void => { if (!condition) { @@ -557,7 +294,7 @@ globalThis.assert_implements_optional = (condition, description): void => { * Asserts if called. Used to ensure that a specific code path is * not taken e.g. that an error event isn't fired. * - * @param [description] - Description of the condition being tested. + * @param {string} [description] - Description of the condition being tested. */ globalThis.assert_unreached = (description): void => { ok(false, `Reached unreachable code: ${description ?? 'undefined'}`); @@ -566,9 +303,9 @@ globalThis.assert_unreached = (description): void => { /** * Assert a JS Error with the expected constructor is thrown. * - * @param constructor The expected exception constructor. - * @param func Function which should throw. - * @param [description] Error description for the case that the error is not 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. */ globalThis.assert_throws_js = (constructor, func, description): void => { throws( @@ -583,23 +320,18 @@ globalThis.assert_throws_js = (constructor, func, description): void => { /** * Assert the provided value is thrown. * - * @param exception The expected exception. - * @param fn Function which should throw. - * @param [description] Error description for the case that the error is not thrown. + * @param {value} exception The expected exception. + * @param {Function} fn Function which should throw. + * @param {string} [description] Error description for the case that the error is not thrown. */ globalThis.assert_throws_exactly = (exception, fn, description): void => { - try { - fn.call(this); - } catch (err) { - strictEqual( - err, - exception, - description ?? "Thrown exception doesn't match expected value" - ); - return; - } - - fail(description ?? 'No exception was thrown'); + throws( + () => { + fn.call(this); + }, + exception, + description + ); }; /** @@ -616,7 +348,7 @@ globalThis.assert_throws_exactly = (exception, fn, description): void => { * the third argument the function expected to throw, and the fourth, optional, * argument the assertion description. * - * @param type - The expected exception name or + * @param {number|string} type - The expected exception name or * code. See the `table of names and codes * `_. If a * number is passed it should be one of the numeric code values in @@ -624,11 +356,11 @@ globalThis.assert_throws_exactly = (exception, fn, description): void => { * 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 descriptionOrFunc - The function expected to + * @param {Function} descriptionOrFunc - The function expected to * throw (if the exception comes from another global), or the * optional description of the condition being tested (if the * exception comes from the current global). - * @param [maybeDescription] - Description of the condition + * @param {string} [maybeDescription] - Description of the condition * being tested (if the exception comes from another global). * */ @@ -667,22 +399,26 @@ globalThis.assert_throws_dom = ( }; /** - * Assert that ``object`` does not have an own property with name ``property_name``. + * Create a synchronous test * - * @param object - Object that should not have the given property. - * @param property_name - Property name to test. - * @param [description] - Description of the condition being tested. + * @param {TestFn} func - Test function. This is executed + * immediately. If it returns without error, the test status is + * set to ``PASS``. If it throws an :js:class:`AssertionError`, or + * any other exception, the test status is set to ``FAIL`` + * (typically from an `assert` function). + * @param {String} name - Test name. This must be unique in a + * given file and must be invariant between runs. */ -globalThis.assert_not_own_property = ( - object, - property_name, - description -): void => { - ok( - !Object.prototype.hasOwnProperty.call(object, property_name), - `unexpected property ${property_name} is found on object: ` + - (description ?? '') - ); +globalThis.test = (func, name): void => { + if (!shouldRunTest(name)) { + return; + } + + try { + func.call(this); + } catch (err) { + globalThis.errors.push(new AggregateError([err], name)); + } }; globalThis.errors = []; @@ -703,15 +439,20 @@ function prepare(env: Env, options: TestRunnerOptions): void { globalThis.errors = []; globalThis.testOptions = options; globalThis.env = env; - globalThis.promises = []; + globalThis.promises = {}; } async function validate( testFileName: string, options: TestRunnerOptions ): Promise { - // Exception handling is set up on every promise in the test function that created it. - await Promise.all(globalThis.promises); + for (const [name, promise] of Object.entries(globalThis.promises)) { + try { + await promise; + } catch (err) { + globalThis.errors.push(new AggregateError([err], name)); + } + } const expectedFailures = new Set(options.expectedFailures ?? []); @@ -739,34 +480,22 @@ async function validate( } } -export function createRunner( - config: TestRunnerConfig -): (file: string) => TestCase { - return (file: string): TestCase => { - return { - async test(_: unknown, env: Env): Promise { - const options = config[file]; - if (!options) { - throw new Error( - `Missing test configuration for ${file}. Specify '${file}': {} for default options.` - ); - } - - if (options.skipAllTests) { - console.warn(`All tests in ${file} have been skipped.`); - return; - } - - prepare(env, options); - - if (options.includeFile) { - env.unsafe.eval(String(env[options.includeFile])); - } - - env.unsafe.eval(String(env[file])); - - await validate(file, options); - }, - }; +export function run(config: TestRunnerConfig, file: string): TestCase { + const options = config[file] ?? {}; + + return { + async test(_: unknown, env: Env): Promise { + if (options.skipAllTests) { + console.warn(`All tests in ${file} have been skipped.`); + return; + } + + prepare(env, options); + if (typeof env[file] !== 'string') { + throw new Error(`Unable to run ${file}. Code is not a string`); + } + env.unsafe.eval(env[file]); + await validate(file, options); + }, }; }