Commit 0ded159ee15 for nodejs
commit 0ded159ee158f259c007c7afd340db4b593d6d17
Author: Filip Skokan <panva.ip@gmail.com>
Date: Sat Sep 26 13:22:26 2026 +0200
test: honor allowed WPT promise rejections
Honor allow_uncaught_exception for unhandled promise rejections in both
WPT worker paths. Apply setup settings before nested callbacks, preserve
settings after results start, and apply promise_setup settings when its
callback runs. Keep rejection failures in single_test mode.
Cover both runner backends and Web Workers, and remove the flaky
expectation for the Web Locks test that deliberately rejects.
Refs: https://github.com/nodejs/node/issues/59142
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Claude, Codex
PR-URL: https://github.com/nodejs/node/pull/66322
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
diff --git a/test/common/wpt/rejections.js b/test/common/wpt/rejections.js
new file mode 100644
index 00000000000..f0cda7a4c60
--- /dev/null
+++ b/test/common/wpt/rejections.js
@@ -0,0 +1,65 @@
+'use strict';
+
+// Node.js does not dispatch the global unhandledrejection event used by
+// testharness.js. Ignore rejections only when the harness settings allow them.
+module.exports = function honorAllowedRejections() {
+ const { setup, promise_setup, add_result_callback, add_completion_callback } = globalThis;
+ let configurable = true;
+ let allowed = false;
+ let singleTest = false;
+ const ignoreRejection = () => {};
+
+ // The harness ignores configuration changes once results have started.
+ add_result_callback(() => { configurable = false; });
+ add_completion_callback(() => { configurable = false; });
+
+ function apply(properties) {
+ if (properties) {
+ if (Object.prototype.propertyIsEnumerable.call(properties, 'allow_uncaught_exception')) {
+ allowed = properties.allow_uncaught_exception;
+ }
+ if (Object.prototype.propertyIsEnumerable.call(properties, 'single_test') && properties.single_test) {
+ singleTest = true;
+ }
+ }
+ process.removeListener('unhandledRejection', ignoreRejection);
+ // In single_test mode the harness still fails the implicit test when
+ // uncaught errors are allowed. Retain Node's failure path for that mode.
+ if (allowed && !singleTest) {
+ process.on('unhandledRejection', ignoreRejection);
+ }
+ }
+
+ globalThis.setup = function(funcOrProperties, maybeProperties) {
+ let func;
+ let properties;
+ if (arguments.length === 2) {
+ func = funcOrProperties;
+ properties = maybeProperties;
+ } else if (funcOrProperties instanceof Function) {
+ func = funcOrProperties;
+ properties = {};
+ } else {
+ properties = funcOrProperties;
+ }
+
+ // This callback runs only when setup was accepted, before any nested
+ // setup calls made by the test's callback can override its settings.
+ return setup.call(this, () => {
+ apply(properties);
+ if (func) func();
+ }, properties);
+ };
+
+ globalThis.promise_setup = function(func, properties) {
+ if (typeof func !== 'function') {
+ return promise_setup.call(this, func, properties);
+ }
+ return promise_setup.call(this, () => {
+ // promise_setup applies settings when its queued callback executes.
+ // Unlike setup, it still invokes that callback after results start.
+ if (configurable) apply(properties);
+ return func();
+ }, properties);
+ };
+};
diff --git a/test/common/wpt/webworker.js b/test/common/wpt/webworker.js
index e9a8ebe53d7..8a335c80869 100644
--- a/test/common/wpt/webworker.js
+++ b/test/common/wpt/webworker.js
@@ -4,6 +4,7 @@
// Refs: https://web-platform-tests.org/writing-tests/testharness.html
const { pathToFileURL } = require('url');
+const honorAllowedRejections = require('./rejections');
const {
runInThisContext,
constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER },
@@ -88,6 +89,7 @@ globalThis.onmessage = ({ data }) => {
const result = realImportScripts.apply(this, mapped);
if (mapped.includes(testharnessPath)) {
applySkips();
+ honorAllowedRejections();
}
return result;
};
diff --git a/test/common/wpt/worker.js b/test/common/wpt/worker.js
index ca4f4706f89..7d84f2ac1a3 100644
--- a/test/common/wpt/worker.js
+++ b/test/common/wpt/worker.js
@@ -9,6 +9,7 @@ const {
} = require('vm');
const { setFlagsFromString } = require('v8');
const { inspect } = require('util');
+const honorAllowedRejections = require('./rejections');
const {
isMainThread,
parentPort,
@@ -79,6 +80,7 @@ function run(workerData) {
filename: workerData.harness.filename,
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
});
+ honorAllowedRejections();
// If there are skip patterns, wrap test functions to prevent execution of
// matching tests. This must happen after testharness.js is loaded but before
diff --git a/test/parallel/test-common-wpt-allowed-rejections.js b/test/parallel/test-common-wpt-allowed-rejections.js
new file mode 100644
index 00000000000..e8f8ed113f3
--- /dev/null
+++ b/test/parallel/test-common-wpt-allowed-rejections.js
@@ -0,0 +1,189 @@
+'use strict';
+
+const common = require('../common');
+const fixtures = require('../common/fixtures');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { backends } = require('../common/wpt');
+
+const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
+const harness = {
+ code: fs.readFileSync(harnessPath, 'utf8'),
+ filename: harnessPath,
+};
+
+async function check(backend, webWorker, {
+ name,
+ code,
+ allowed,
+ passes = 1,
+ singleTest = false,
+ message = /uncaught rejection probe/,
+}) {
+ const label = `${backend}, ${webWorker}, ${name}`;
+ const script = {
+ filename: fixtures.path('wpt-rejection-probe.js'),
+ // Use a direct worker script for single_test: .any.js wrappers call
+ // done() automatically, completing the implicit test before its timers.
+ code: `${webWorker && singleTest ? 'importScripts("/resources/testharness.js");' : ''}\n${code}`,
+ };
+ const workerData = {
+ testRelativePath: 'rejection-probe.any.js',
+ wptRunner: path.join(__dirname, '../common/wpt.js'),
+ wptPath: 'compression',
+ harness,
+ scriptsToRun: webWorker ? [] : [script],
+ webWorker: webWorker ? {
+ path: script.filename,
+ modifiedScript: script,
+ isAnyTest: !singleTest,
+ variant: '',
+ scripts: [],
+ } : undefined,
+ };
+
+ let result;
+ const statuses = [];
+ const handle = backends[backend](['--experimental-web-worker'], workerData, {
+ message(message) {
+ if (message.type === 'result') {
+ statuses.push(message.result.status);
+ } else if (message.type === 'completion') {
+ // The process backend can complete before its uncaught-error handler
+ // exits. Preserve any failure it already reported.
+ result ||= message.status;
+ handle.kill();
+ } else {
+ assert.fail(`Unexpected message type: ${message.type}`);
+ }
+ },
+ failure(error) {
+ if (result) return false;
+ result = { status: 1, message: error.message };
+ return true;
+ },
+ });
+ const timeout = setTimeout(() => {
+ handle.kill();
+ assert.fail(`WPT rejection probe did not finish: ${label}`);
+ }, common.platformTimeout(10_000));
+
+ try {
+ await handle.finished;
+ } finally {
+ clearTimeout(timeout);
+ }
+
+ assert(result, label);
+ assert.strictEqual(result.status, allowed ? 0 : 1, label);
+ if (allowed) {
+ assert.deepStrictEqual(statuses, Array(passes).fill(0), label);
+ } else {
+ assert.match(result.message, message, label);
+ }
+}
+
+(async () => {
+ for (const backend of ['thread', 'process']) {
+ for (const webWorker of [false, true]) {
+ for (const [setup, allowed] of [
+ ['', false],
+ ['setup({ allow_uncaught_exception: false });', false],
+ ['setup({ allow_uncaught_exception: true });', true],
+ ['setup(() => {}, { allow_uncaught_exception: true });', true],
+ ['setup(null, { allow_uncaught_exception: true });', true],
+ ['setup({ allow_uncaught_exception: true }); setup({});', true],
+ ['setup({ allow_uncaught_exception: true }); setup({ allow_uncaught_exception: false });', false],
+ ['setup(Object.defineProperty({}, "allow_uncaught_exception", { value: true }));', false],
+ ]) {
+ await check(backend, webWorker, {
+ name: setup || 'default settings',
+ code: `${setup}
+ const t = async_test('rejection probe');
+ Promise.reject(new Error('uncaught rejection probe'));
+ setTimeout(() => t.done(), 0);`,
+ allowed,
+ });
+ }
+
+ for (const allowed of [false, true]) {
+ await check(backend, webWorker, {
+ name: `nested setup allows rejection: ${allowed}`,
+ code: `setup(() => setup({ allow_uncaught_exception: ${allowed} }),
+ { allow_uncaught_exception: ${!allowed} });
+ const t = async_test('rejection probe');
+ Promise.reject(new Error('uncaught rejection probe'));
+ setTimeout(() => t.done(), 0);`,
+ allowed,
+ });
+
+ await check(backend, webWorker, {
+ name: `late setup preserves allowance: ${allowed}`,
+ code: `setup({ allow_uncaught_exception: ${allowed} });
+ const t = async_test('rejection probe');
+ test(() => {}, 'first result');
+ setup(() => assert_unreached('late setup callback'),
+ { allow_uncaught_exception: ${!allowed} });
+ Promise.reject(new Error('uncaught rejection probe'));
+ setTimeout(() => t.done(), 0);`,
+ allowed,
+ passes: 2,
+ });
+
+ await check(backend, webWorker, {
+ name: `promise_setup applies deferred allowance: ${allowed}`,
+ code: `promise_setup(async () => {}, { allow_uncaught_exception: ${allowed} });
+ setup({ allow_uncaught_exception: ${!allowed} });
+ promise_test(async () => {
+ Promise.reject(new Error('uncaught rejection probe'));
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }, 'rejection probe');`,
+ allowed,
+ });
+
+ await check(backend, webWorker, {
+ name: `late promise_setup preserves allowance: ${allowed}`,
+ code: `setup({ allow_uncaught_exception: ${allowed} });
+ const t = async_test('rejection probe');
+ test(() => {}, 'first result');
+ promise_setup(async () => {
+ Promise.reject(new Error('uncaught rejection probe'));
+ setTimeout(() => t.done(), 0);
+ }, { allow_uncaught_exception: ${!allowed} });`,
+ allowed,
+ passes: 2,
+ });
+
+ await check(backend, webWorker, {
+ name: `single_test still fails with allowance: ${allowed}`,
+ code: `setup({ single_test: true, allow_uncaught_exception: ${allowed} });
+ setup({ single_test: false });
+ Promise.reject(new Error('uncaught rejection probe'));
+ setTimeout(done, 0);`,
+ allowed: false,
+ singleTest: true,
+ });
+ }
+
+ for (const setup of [
+ 'setup(() => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });',
+ 'promise_setup(async () => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });',
+ ]) {
+ await check(backend, webWorker, {
+ name: setup,
+ code: `${setup}\npromise_test(async () => {}, 'setup must succeed first');`,
+ allowed: false,
+ message: /setup failure/,
+ });
+ }
+
+ await check(backend, webWorker, {
+ name: 'unexpected rejection fails even with a permanently pending test',
+ code: `async_test('pending forever');
+ Promise.reject(new Error('uncaught rejection probe'));`,
+ allowed: false,
+ });
+ }
+ }
+})().then(common.mustCall());
diff --git a/test/wpt/status/web-locks.json b/test/wpt/status/web-locks.json
index 97a0a7900d6..f7774a38fa7 100644
--- a/test/wpt/status/web-locks.json
+++ b/test/wpt/status/web-locks.json
@@ -1,12 +1,4 @@
{
- "held.https.any.js": {
- "fail": {
- "note": "Flaky on ppc, linux x64 and s390x #59142",
- "flaky": [
- "Error: this uncaught rejection is expected"
- ]
- }
- },
"idlharness.https.any.js": {
"fail": {
"expected": [