Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions INTERPRETING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ properties of the global scope prior to test execution.
Use this property to test that ECMAScript algorithms aren't mis-implemented to treat `document.all` as being `undefined` or of type Undefined (instead of Object).

**Tests using this function must be tagged with the `IsHTMLDDA` feature so that only hosts supporting this property will run them.**
- **`safeResolvePromise`** - (present only in implementations that can provide it) a function which takes as its first argument a promise and as its second argument a value, and uses the SafeResolve semantics provided by the [Thenable Curtailment](https://github.com/tc39/proposal-thenable-curtailment) proposal to resolve the provided promise with the value.
- **`agent`** - an ordinary object with the following properties:
- **`start`** - a function that takes a script source string and runs
the script in a concurrent agent. Will block until that agent is
Expand Down
5 changes: 5 additions & 0 deletions features.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ error-stack-accessor
# https://github.com/tc39/proposal-iterator-join
Iterator.prototype.join

# Thenable Curtailment
# https://github.com/tc39/proposal-thenable-curtailment
thenable-curtailment

## Standard language features
#
# Language features that have been included in a published version of the
Expand Down Expand Up @@ -273,3 +277,4 @@ __setter__

IsHTMLDDA
host-gc-required
safeResolvePromise
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (C) 2026 Mozilla Corporation. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-has-property-which-could-run-user-code
description: >
SafePromiseResolve defers resolution when the resolution is a module
namespace exotic object.
info: |
PropertyAccessCouldRunUserCode ( o, propertyKey, kind )

3. If _o_ has the [[GetPrototypeOf]] and [[GetOwnProperty]] internal methods
as defined in Module Namespace Exotic Objects, return *true*.
includes: [asyncHelpers.js, compareArray.js]
flags: [module, async]
features: [thenable-curtailment, safeResolvePromise, promise-with-resolvers]
---*/

import * as ns from "./deferred-module-namespace_FIXTURE.js";

var expected = [
// Being a module namespace object forces deferral without any lookup.
"start",

"tick 1",
"tick 2",

// No callable "then" is found, so the namespace object fulfills the promise
// from the deferred job, one microtask behind a synchronous resolution.
"settled",
];

var actual = [];

asyncTest(function() {
var ruler = Promise.resolve(0)
.then(() => actual.push("tick 1"))
.then(() => actual.push("tick 2"))
.then(() => {
assert.compareArray(
actual,
expected,
"Ticks for a module namespace object"
);
});

assert.sameValue(ns.then, undefined, "the fixture module does not export \"then\"");

var capability = Promise.withResolvers();
$262.safeResolvePromise(capability.promise, ns);
actual.push("start");

var settled = capability.promise.then(function(settledValue) {
actual.push("settled");
assert.sameValue(
settledValue,
ns,
"promise is fulfilled with the module namespace object itself"
);
});

return Promise.all([ruler, settled]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (C) 2026 Mozilla Corporation. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

export var x = 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (C) 2026 Mozilla Corporation. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-perform-promise-resolution
description: >
Resolving with a still-pending native promise via SafePromiseResolve takes
the same number of microtasks as an ordinary resolution.
info: |
PerformPromiseResolution ( promise, resolution, thenCallTiming )

9. If _thenCallTiming_ is ~deferred~, then
a. Perform ! PerformPromiseResolveThenable(_promise_, _resolution_,
_thenAction_).
b. Return ~unused~.
includes: [asyncHelpers.js, compareArray.js]
flags: [async]
features: [thenable-curtailment, safeResolvePromise, promise-with-resolvers]
---*/

// As for an already-fulfilled inner promise, the two paths settle in the same
// microtask, shown by the two settlements being adjacent and in the order their
// reactions were attached.
var expected = [
"start",

"tick 1",
"tick 2",
"tick 3",

// Were the safe path to take an extra microtask, these two would swap.
"settled safe",
"settled ordinary",
];

var actual = [];

asyncTest(function() {
var ruler = Promise.resolve(0)
.then(() => actual.push("tick 1"))
.then(() => actual.push("tick 2"))
.then(() => actual.push("tick 3"))
.then(() => {
assert.compareArray(
actual,
expected,
"Ticks for resolving with a pending promise"
);
});

var safeInner = Promise.withResolvers();
var safe = Promise.withResolvers();
$262.safeResolvePromise(safe.promise, safeInner.promise);
var safeSettled = safe.promise.then(function(settledValue) {
actual.push("settled safe");
assert.sameValue(settledValue, "inner", "fulfilled with the inner promise's value");
});
safeInner.resolve("inner");

var ordinaryInner = Promise.withResolvers();
var ordinary = Promise.withResolvers();
ordinary.resolve(ordinaryInner.promise);
var ordinarySettled = ordinary.promise.then(function(settledValue) {
actual.push("settled ordinary");
assert.sameValue(settledValue, "inner", "fulfilled with the inner promise's value");
});
ordinaryInner.resolve("inner");

actual.push("start");

return Promise.all([ruler, safeSettled, ordinarySettled]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (C) 2026 Mozilla Corporation. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-perform-promise-resolution
description: >
Resolving with an already-fulfilled native promise via SafePromiseResolve
takes the same number of microtasks as an ordinary resolution.
info: |
PerformPromiseResolution ( promise, resolution, thenCallTiming )

9. If _thenCallTiming_ is ~deferred~, then
a. Perform ! PerformPromiseResolveThenable(_promise_, _resolution_,
_thenAction_).
b. Return ~unused~.
10. Let _thenJobCallback_ be HostMakeJobCallback(_thenAction_).
11. Let _job_ be NewPromiseResolveThenableJob(_promise_, _resolution_,
_thenJobCallback_).
12. Perform HostEnqueuePromiseJob(_job_.[[Job]], _job_.[[Realm]]).
includes: [asyncHelpers.js, compareArray.js]
flags: [async]
features: [thenable-curtailment, safeResolvePromise, promise-with-resolvers]
---*/

// A native promise has a callable "then" reachable without running user code, so
// an ordinary resolution already enqueues a job for it. In the deferred job
// _thenCallTiming_ is ~deferred~, so the "then" call is performed inline rather
// than in a further job, and the two paths settle in the same microtask.
//
// The tick each settlement lands on is fully determined by the spec given this
// test's construction, but it is not a count of the resolution's own microtasks:
// it also reflects the interleaving with the ruler. What carries the claim here
// is that the two settlements are adjacent, in the order their reactions were
// attached.
var expected = [
"start",

"tick 1",
"tick 2",
"tick 3",

// Were the safe path to take an extra microtask, these two would swap.
"settled safe",
"settled ordinary",
];

var actual = [];

asyncTest(function() {
var ruler = Promise.resolve(0)
.then(() => actual.push("tick 1"))
.then(() => actual.push("tick 2"))
.then(() => actual.push("tick 3"))
.then(() => {
assert.compareArray(
actual,
expected,
"Ticks for resolving with an already-fulfilled promise"
);
});

var safe = Promise.withResolvers();
$262.safeResolvePromise(safe.promise, Promise.resolve("inner"));
var safeSettled = safe.promise.then(function(settledValue) {
actual.push("settled safe");
assert.sameValue(settledValue, "inner", "fulfilled with the inner promise's value");
});

var ordinary = Promise.withResolvers();
ordinary.resolve(Promise.resolve("inner"));
var ordinarySettled = ordinary.promise.then(function(settledValue) {
actual.push("settled ordinary");
assert.sameValue(settledValue, "inner", "fulfilled with the inner promise's value");
});

actual.push("start");

return Promise.all([ruler, safeSettled, ordinarySettled]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (C) 2026 Mozilla Corporation. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-safe-promise-resolve
description: >
A callable "then" on Object.prototype defers resolution of an ordinary
object.
info: |
PropertyAccessCouldRunUserCode ( o, propertyKey, kind )

7. Let _proto_ be _o_.[[GetPrototypeOf]]().
8. If _proto_ is *null*, return *false*.
9. Return PropertyAccessCouldRunUserCode(_proto_, _propertyKey_, _kind_).
includes: [asyncHelpers.js, compareArray.js]
flags: [async]
features: [thenable-curtailment, safeResolvePromise, promise-with-resolvers]
---*/

var expected = [
// Nothing has run yet: the inherited "then" was not called synchronously.
"start",

"tick 1",

// The deferred job calls the inherited "then".
"call Object.prototype.then",

"tick 2",

// Resolved from the deferred job, so its reaction runs after it.
"settled",
];

var actual = [];

var thenCalledWith;
var value = {};

Object.defineProperty(Object.prototype, "then", {
value: function(resolve) {
actual.push("call Object.prototype.then");
thenCalledWith = this;
resolve("from Object.prototype");
},
writable: true,
enumerable: false,
configurable: true,
});

asyncTest(function() {
var ruler = Promise.resolve(0)
.then(() => actual.push("tick 1"))
.then(() => actual.push("tick 2"))
.then(() => {
try {
assert.compareArray(
actual,
expected,
"Ticks for a callable Object.prototype.then"
);
assert.sameValue(
thenCalledWith,
value,
"\"then\" is called with the resolution as its this value"
);
} finally {
delete Object.prototype.then;
}
});

var capability = Promise.withResolvers();
$262.safeResolvePromise(capability.promise, value);
actual.push("start");

var settled = capability.promise.then(function(settledValue) {
actual.push("settled");
assert.sameValue(
settledValue,
"from Object.prototype",
"the inherited \"then\" resolves the promise"
);
});

return Promise.all([ruler, settled]);
});
Loading
Loading