Skip to content

Commit ddbf3e5

Browse files
committed
Temporal: Add tests for observable order of options get and cast
These tests cover the normative change approved in the July 2025 TC39 plenary. See tc39/proposal-temporal#3130. All properties of user-passed options objects should be read and cast before any "algorithmic validation" is done. This is a lot of tests, that cover every entry point where an options object is passed in, where subsequently an exception can be thrown or user code can be called. However, the normative change only affects 10 of these tests: - Instant.p.since,until,toString - PlainDate.p.since,until - PlainTime.p.since,until - PlainYearMonth.p.since,until - ZonedDateTime.p.toString The other 35 tests simply close a gap in coverage.
1 parent f4864f7 commit ddbf3e5

File tree

45 files changed

+1572
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1572
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.duration.compare
6+
description: All options properties are read before any algorithmic validation
7+
includes: [compareArray.js, temporalHelpers.js]
8+
features: [Temporal]
9+
---*/
10+
11+
const expected = ["get options.relativeTo"];
12+
const actual = [];
13+
14+
const options = TemporalHelpers.propertyBagObserver(actual, { relativeTo: undefined }, "options");
15+
16+
const d1 = new Temporal.Duration(0, 0, 0, /* days = */ 1);
17+
const d2 = new Temporal.Duration(1);
18+
19+
assert.throws(RangeError, function () {
20+
Temporal.Duration.compare(d1, d2, options);
21+
}, "exception thrown when calendar units provided without relativeTo");
22+
assert.compareArray(actual, expected, "all options should be read first");
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.duration.prototype.round
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.largestUnit",
14+
"get options.largestUnit.toString",
15+
"call options.largestUnit.toString",
16+
"get options.relativeTo",
17+
"get options.roundingIncrement",
18+
"get options.roundingIncrement.valueOf",
19+
"call options.roundingIncrement.valueOf",
20+
"get options.roundingMode",
21+
"get options.roundingMode.toString",
22+
"call options.roundingMode.toString",
23+
"get options.smallestUnit",
24+
"get options.smallestUnit.toString",
25+
"call options.smallestUnit.toString",
26+
];
27+
const actual = [];
28+
29+
const options = TemporalHelpers.propertyBagObserver(actual, {
30+
smallestUnit: "years",
31+
largestUnit: "nanoseconds",
32+
roundingIncrement: 1,
33+
roundingMode: "halfCeil",
34+
relativeTo: undefined,
35+
}, "options");
36+
37+
const instance = new Temporal.Duration(1);
38+
39+
assert.throws(RangeError, function () {
40+
instance.round(options);
41+
}, "exception thrown when smallestUnit > largestUnit");
42+
assert.compareArray(actual, expected, "all options should be read first");
43+
actual.splice(0); // clear
44+
45+
// Test again, with largestUnit and smallestUnit undefined. The error that's
46+
// thrown in that case is the earliest algorithmic validation that takes place,
47+
// but we can't test it simultaneously with testing that largestUnit and
48+
// smallestUnit are cast at the right time.
49+
50+
const expectedWithoutUnits = [
51+
"get options.largestUnit",
52+
"get options.relativeTo",
53+
"get options.roundingIncrement",
54+
"get options.roundingIncrement.valueOf",
55+
"call options.roundingIncrement.valueOf",
56+
"get options.roundingMode",
57+
"get options.roundingMode.toString",
58+
"call options.roundingMode.toString",
59+
"get options.smallestUnit",
60+
];
61+
62+
const optionsWithoutUnits = TemporalHelpers.propertyBagObserver(actual, {
63+
smallestUnit: undefined,
64+
largestUnit: undefined,
65+
roundingIncrement: 1,
66+
roundingMode: "halfFloor",
67+
relativeTo: undefined,
68+
}, "options");
69+
70+
assert.throws(RangeError, function () {
71+
instance.round(optionsWithoutUnits);
72+
}, "exception thrown when neither smallestUnit nor largestUnit present");
73+
assert.compareArray(actual, expectedWithoutUnits, "all options should be read first");
74+
actual.splice(0); // clear
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.duration.prototype.tostring
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.fractionalSecondDigits",
14+
"get options.fractionalSecondDigits.toString",
15+
"call options.fractionalSecondDigits.toString",
16+
"get options.roundingMode",
17+
"get options.roundingMode.toString",
18+
"call options.roundingMode.toString",
19+
"get options.smallestUnit",
20+
"get options.smallestUnit.toString",
21+
"call options.smallestUnit.toString",
22+
];
23+
const actual = [];
24+
25+
const options = TemporalHelpers.propertyBagObserver(actual, {
26+
smallestUnit: "seconds",
27+
fractionalSecondDigits: "auto",
28+
roundingMode: "expand",
29+
}, "options");
30+
31+
const instance = new Temporal.Duration(0, 0, 0, 0, 0, 0, /* seconds = */ Number.MAX_SAFE_INTEGER, 1);
32+
33+
assert.throws(RangeError, function () {
34+
instance.toString(options);
35+
}, "exception thrown when result is out of range");
36+
assert.compareArray(actual, expected, "all options should be read first");
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.duration.prototype.total
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.relativeTo",
14+
"get options.unit",
15+
"get options.unit.toString",
16+
"call options.unit.toString",
17+
];
18+
const actual = [];
19+
20+
const options = TemporalHelpers.propertyBagObserver(actual, {
21+
unit: "weeks",
22+
relativeTo: undefined,
23+
}, "options");
24+
25+
const instance = new Temporal.Duration(1);
26+
27+
assert.throws(RangeError, function () {
28+
instance.total(options);
29+
}, "exception thrown when total of calendar unit requested without relativeTo");
30+
assert.compareArray(actual, expected, "all options should be read first");
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.instant.prototype.round
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.roundingIncrement",
14+
"get options.roundingIncrement.valueOf",
15+
"call options.roundingIncrement.valueOf",
16+
"get options.roundingMode",
17+
"get options.roundingMode.toString",
18+
"call options.roundingMode.toString",
19+
"get options.smallestUnit",
20+
"get options.smallestUnit.toString",
21+
"call options.smallestUnit.toString",
22+
];
23+
const actual = [];
24+
25+
const options = TemporalHelpers.propertyBagObserver(actual, {
26+
smallestUnit: "hour",
27+
roundingIncrement: 25,
28+
roundingMode: "expand",
29+
}, "options");
30+
31+
const instance = new Temporal.Instant(1n);
32+
33+
assert.throws(RangeError, function () {
34+
instance.round(options);
35+
}, "exception thrown when roundingIncrement invalid for smallestUnit");
36+
assert.compareArray(actual, expected, "all options should be read first");
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.instant.prototype.since
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.largestUnit",
14+
"get options.largestUnit.toString",
15+
"call options.largestUnit.toString",
16+
"get options.roundingIncrement",
17+
"get options.roundingIncrement.valueOf",
18+
"call options.roundingIncrement.valueOf",
19+
"get options.roundingMode",
20+
"get options.roundingMode.toString",
21+
"call options.roundingMode.toString",
22+
"get options.smallestUnit",
23+
"get options.smallestUnit.toString",
24+
"call options.smallestUnit.toString",
25+
];
26+
const actual = [];
27+
28+
const options = TemporalHelpers.propertyBagObserver(actual, {
29+
smallestUnit: "nanosecond",
30+
largestUnit: "week",
31+
roundingIncrement: 1,
32+
roundingMode: "halfFloor",
33+
}, "options");
34+
35+
const instance = new Temporal.Instant(0n);
36+
const other = new Temporal.Instant(1000n);
37+
38+
assert.throws(RangeError, function () {
39+
instance.since(other, options);
40+
}, "exception thrown when largestUnit disallowed");
41+
assert.compareArray(actual, expected, "all options should be read first");
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.instant.prototype.tostring
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.fractionalSecondDigits",
14+
"get options.fractionalSecondDigits.toString",
15+
"call options.fractionalSecondDigits.toString",
16+
"get options.roundingMode",
17+
"get options.roundingMode.toString",
18+
"call options.roundingMode.toString",
19+
"get options.smallestUnit",
20+
"get options.smallestUnit.toString",
21+
"call options.smallestUnit.toString",
22+
"get options.timeZone",
23+
];
24+
const actual = [];
25+
26+
const options = TemporalHelpers.propertyBagObserver(actual, {
27+
smallestUnit: "month",
28+
fractionalSecondDigits: "auto",
29+
roundingMode: "expand",
30+
timeZone: undefined,
31+
}, "options");
32+
33+
const instance = new Temporal.Instant(0n);
34+
35+
assert.throws(RangeError, function () {
36+
instance.toString(options);
37+
}, "exception thrown when unit is a date unit");
38+
assert.compareArray(actual, expected, "all options should be read first");
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.instant.prototype.until
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.largestUnit",
14+
"get options.largestUnit.toString",
15+
"call options.largestUnit.toString",
16+
"get options.roundingIncrement",
17+
"get options.roundingIncrement.valueOf",
18+
"call options.roundingIncrement.valueOf",
19+
"get options.roundingMode",
20+
"get options.roundingMode.toString",
21+
"call options.roundingMode.toString",
22+
"get options.smallestUnit",
23+
"get options.smallestUnit.toString",
24+
"call options.smallestUnit.toString",
25+
];
26+
const actual = [];
27+
28+
const options = TemporalHelpers.propertyBagObserver(actual, {
29+
smallestUnit: "nanosecond",
30+
largestUnit: "week",
31+
roundingIncrement: 1,
32+
roundingMode: "halfFloor",
33+
}, "options");
34+
35+
const instance = new Temporal.Instant(0n);
36+
const other = new Temporal.Instant(1000n);
37+
38+
assert.throws(RangeError, function () {
39+
instance.until(other, options);
40+
}, "exception thrown when largestUnit disallowed");
41+
assert.compareArray(actual, expected, "all options should be read first");
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.plaindate.from
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.overflow",
14+
"get options.overflow.toString",
15+
"call options.overflow.toString",
16+
];
17+
const actual = [];
18+
19+
const options = TemporalHelpers.propertyBagObserver(actual, {
20+
overflow: "constrain",
21+
}, "options");
22+
23+
assert.throws(RangeError, function () {
24+
Temporal.PlainDate.from({ year: 2025, monthCode: "M08L", day: 14 }, options);
25+
}, "exception thrown when month code is invalid for calendar");
26+
assert.compareArray(actual, expected, "all options should be read first");
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (C) 2025 Igalia, S.L. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-temporal.plaindate.prototype.add
6+
description: >
7+
All options properties are read and cast before any algorithmic validation
8+
includes: [compareArray.js, temporalHelpers.js]
9+
features: [Temporal]
10+
---*/
11+
12+
const expected = [
13+
"get options.overflow",
14+
"get options.overflow.toString",
15+
"call options.overflow.toString",
16+
];
17+
const actual = [];
18+
19+
const options = TemporalHelpers.propertyBagObserver(actual, {
20+
overflow: "reject",
21+
}, "options");
22+
23+
const instance = new Temporal.PlainDate(2025, 8, 31);
24+
25+
assert.throws(RangeError, function () {
26+
instance.add(new Temporal.Duration(0, 1), options);
27+
}, "overflow reject exception thrown");
28+
assert.compareArray(actual, expected, "all options should be read first");

0 commit comments

Comments
 (0)