summaryrefslogtreecommitdiffstats
path: root/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /js/src/tests/test262/built-ins/Temporal/Duration/prototype/total
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/tests/test262/built-ins/Temporal/Duration/prototype/total')
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-negative-result.js24
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-subseconds.js17
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/branding.js27
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/browser.js0
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/builtin.js36
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-options-undefined.js43
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-plaindate-instance.js21
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateuntil-called-with-singular-largestunit.js68
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-fields-iterable.js34
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-possibly-required.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-temporal-object.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/constructor-in-calendar-fields.js17
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dateuntil-field.js37
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duplicate-calendar-fields.js19
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js28
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/length.js28
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/name.js26
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js80
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/not-a-constructor.js24
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/options-wrong-type.js27
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/order-of-operations.js396
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js99
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js101
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js121
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-4.js155
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-5.js61
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js143
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js122
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/prop-desc.js24
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/proto-in-calendar-fields.js17
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/read-time-fields-before-datefromfields.js24
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-infinity-throws-rangeerror.js26
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-leap-second.js45
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-number.js28
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-plaindate-add24hourdaystonormalizedtimeduration-out-of-range.js19
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-ambiguous-wall-clock-time.js92
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-builtin-calendar-no-array-iteration.js25
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-datefromfields-called-with-null-prototype-fields.js19
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-number.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-string.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-wrong-type.js48
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-getpossibleinstantsfor-called-with-iso8601-calendar.js59
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-invalid-offset-string.js32
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-no-time-units.js17
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-non-integer.js18
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-not-callable.js22
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-out-of-range.js18
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-wrong-type.js27
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-datetime.js66
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-leap-second.js22
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-year-zero.js24
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string.js40
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-wrong-type.js42
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-datetime.js40
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-invalid.js16
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime-invalid.js26
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime.js17
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime-wrong-offset.js19
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime.js22
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-sub-minute-offset.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-undefined-throw-on-calendar-units.js35
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-wrong-type.js50
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-negative-epochnanoseconds.js26
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js128
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-non-integer.js19
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-not-callable.js23
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-out-of-range.js19
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-wrong-type.js28
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days-different-sign.js38
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days.js27
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/shell.js24
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/timezone-getpossibleinstantsfor-iterable.js36
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-disallowed-units-string.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-invalid-string.js21
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted-string.js24
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted.js28
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-string-shorthand-string.js27
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-wrong-type.js25
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/year-zero.js20
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-day-length.js43
-rw-r--r--js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-year-month-week-length.js34
81 files changed, 3517 insertions, 0 deletions
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-negative-result.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-negative-result.js
new file mode 100644
index 0000000000..f4781725dd
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-negative-result.js
@@ -0,0 +1,24 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: A negative duration result is balanced correctly by the modulo operation in NanosecondsToDays
+info: |
+ sec-temporal-nanosecondstodays step 6:
+ 6. If Type(_relativeTo_) is not Object or _relativeTo_ does not have an [[InitializedTemporalZonedDateTime]] internal slot, then
+ a. Return the new Record { ..., [[Nanoseconds]]: abs(_nanoseconds_) modulo _dayLengthNs_ × _sign_, ... }.
+ sec-temporal-balanceduration step 4:
+ 4. If _largestUnit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then
+ a. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _relativeTo_).
+ sec-temporal.duration.prototype.round step 9:
+ 9. Let _balanceResult_ be ? BalanceDuration(_unbalanceResult_.[[Days]], _unbalanceResult_.[[Hours]], _unbalanceResult_.[[Minutes]], _unbalanceResult_.[[Seconds]], _unbalanceResult_.[[Milliseconds]], _unbalanceResult_.[[Microseconds]], _unbalanceResult_.[[Nanoseconds]], _unit_, _intermediate_).
+features: [Temporal]
+---*/
+
+const duration = new Temporal.Duration(0, 0, 0, 0, -60);
+const result = duration.total({ unit: "days" });
+assert.sameValue(result, -2.5);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-subseconds.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-subseconds.js
new file mode 100644
index 0000000000..9e37051c5d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-subseconds.js
@@ -0,0 +1,17 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Balancing from subsecond units to seconds happens correctly
+features: [Temporal]
+---*/
+
+const pos = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 999, 999999, 999999999);
+assert.sameValue(pos.total("seconds"), 2.998998999);
+
+const neg = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, -999, -999999, -999999999);
+assert.sameValue(neg.total("seconds"), -2.998998999);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/branding.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/branding.js
new file mode 100644
index 0000000000..0beb2f811a
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/branding.js
@@ -0,0 +1,27 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Throw a TypeError if the receiver is invalid
+features: [Symbol, Temporal]
+---*/
+
+const total = Temporal.Duration.prototype.total;
+
+assert.sameValue(typeof total, "function");
+
+const args = ["hour"];
+
+assert.throws(TypeError, () => total.apply(undefined, args), "undefined");
+assert.throws(TypeError, () => total.apply(null, args), "null");
+assert.throws(TypeError, () => total.apply(true, args), "true");
+assert.throws(TypeError, () => total.apply("", args), "empty string");
+assert.throws(TypeError, () => total.apply(Symbol(), args), "symbol");
+assert.throws(TypeError, () => total.apply(1, args), "1");
+assert.throws(TypeError, () => total.apply({}, args), "plain object");
+assert.throws(TypeError, () => total.apply(Temporal.Duration, args), "Temporal.Duration");
+assert.throws(TypeError, () => total.apply(Temporal.Duration.prototype, args), "Temporal.Duration.prototype");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/browser.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/browser.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/browser.js
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/builtin.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/builtin.js
new file mode 100644
index 0000000000..ca011bad96
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/builtin.js
@@ -0,0 +1,36 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Tests that Temporal.Duration.prototype.total
+ meets the requirements for built-in objects defined by the
+ introduction of chapter 17 of the ECMAScript Language Specification.
+info: |
+ Built-in functions that are not constructors do not have a "prototype" property unless
+ otherwise specified in the description of a particular function.
+
+ Unless specified otherwise, a built-in object that is callable as a function is a built-in
+ function object with the characteristics described in 10.3. Unless specified otherwise, the
+ [[Extensible]] internal slot of a built-in object initially has the value true.
+
+ Unless otherwise specified every built-in function and every built-in constructor has the
+ Function prototype object [...] as the value of its [[Prototype]] internal slot.
+features: [Temporal]
+---*/
+
+assert.sameValue(Object.isExtensible(Temporal.Duration.prototype.total),
+ true, "Built-in objects must be extensible.");
+
+assert.sameValue(Object.prototype.toString.call(Temporal.Duration.prototype.total),
+ "[object Function]", "Object.prototype.toString");
+
+assert.sameValue(Object.getPrototypeOf(Temporal.Duration.prototype.total),
+ Function.prototype, "prototype");
+
+assert.sameValue(Temporal.Duration.prototype.total.hasOwnProperty("prototype"),
+ false, "prototype property");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-options-undefined.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-options-undefined.js
new file mode 100644
index 0000000000..7185da66e6
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-options-undefined.js
@@ -0,0 +1,43 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ BuiltinTimeZoneGetInstantFor calls Calendar.dateAdd with undefined as the
+ options value
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const calendar = TemporalHelpers.calendarDateAddUndefinedOptions();
+const timeZone = TemporalHelpers.oneShiftTimeZone(new Temporal.Instant(0n), 3600e9);
+const relativeTo = new Temporal.ZonedDateTime(0n, timeZone, calendar);
+
+// Total of a calendar unit where larger calendar units have to be converted
+// down, to cover the path that goes through UnbalanceDateDurationRelative
+// The calls come from the path:
+// Duration.total() -> UnbalanceDateDurationRelative -> calendar.dateAdd()
+
+const instance1 = new Temporal.Duration(1, 1, 1, 1, 1);
+instance1.total({ unit: "days", relativeTo });
+assert.sameValue(calendar.dateAddCallCount, 1, "converting larger calendar units down");
+
+// Total of a calendar unit where smaller calendar units have to be converted
+// up, to cover the path that goes through MoveRelativeZonedDateTime
+// The calls come from these paths:
+// Duration.total() ->
+// MoveRelativeZonedDateTime -> AddZonedDateTime -> BuiltinTimeZoneGetInstantFor -> calendar.dateAdd()
+// BalanceDuration ->
+// AddZonedDateTime -> BuiltinTimeZoneGetInstantFor -> calendar.dateAdd()
+// RoundDuration ->
+// MoveRelativeDate -> calendar.dateAdd()
+
+calendar.dateAddCallCount = 0;
+
+const instance2 = new Temporal.Duration(0, 0, 1, 1);
+instance2.total({ unit: "weeks", relativeTo });
+assert.sameValue(calendar.dateAddCallCount, 3, "converting smaller calendar units up");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-plaindate-instance.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-plaindate-instance.js
new file mode 100644
index 0000000000..0afdda3671
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-plaindate-instance.js
@@ -0,0 +1,21 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ relativeTo parameters that are not ZonedDateTime or undefined, are always
+ converted to PlainDate for observable calendar calls
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const calendar = TemporalHelpers.calendarDateAddPlainDateInstance();
+const instance = new Temporal.Duration(1, 1, 1, 1);
+const relativeTo = new Temporal.PlainDate(2000, 1, 1, calendar);
+calendar.specificPlainDate = relativeTo;
+instance.total({ unit: "days", relativeTo });
+assert(calendar.dateAddCallCount > 0, "assertions in calendar.dateAdd() should have been tested");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateuntil-called-with-singular-largestunit.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateuntil-called-with-singular-largestunit.js
new file mode 100644
index 0000000000..0485054ae0
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateuntil-called-with-singular-largestunit.js
@@ -0,0 +1,68 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: The options object passed to calendar.dateUntil has a largestUnit property with its value in the singular form
+info: |
+ sec-temporal.duration.prototype.total steps 7–11:
+ 7. Let _unbalanceResult_ be ? UnbalanceDateDurationRelative(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _unit_, _relativeTo_).
+ ...
+ 10. Let _balanceResult_ be ? BalanceDuration(_unbalanceResult_.[[Days]], _unbalanceResult_.[[Hours]], _unbalanceResult_.[[Minutes]], _unbalanceResult_.[[Seconds]], _unbalanceResult_.[[Milliseconds]], _unbalanceResult_.[[Microseconds]], _unbalanceResult_.[[Nanoseconds]], _unit_, _intermediate_).
+ 11. Let _roundResult_ be ? RoundDuration(_unbalanceResult_.[[Years]], _unbalanceResult_.[[Months]], _unbalanceResult_.[[Weeks]], _balanceResult_.[[Days]], _balanceResult_.[[Hours]], _balanceResult_.[[Minutes]], _balanceResult_.[[Seconds]], _balanceResult_.[[Milliseconds]], _balanceResult_.[[Microseconds]], _balanceResult_.[[Nanoseconds]], 1, _unit_, *"trunc"*, _relativeTo_).
+ sec-temporal-unbalancedatedurationrelative step 3:
+ 3. If _largestUnit_ is *"month"*, then
+ ...
+ g. Let _untilOptions_ be ! OrdinaryObjectCreate(*null*).
+ h. Perform ! CreateDataPropertyOrThrow(_untilOptions_, *"largestUnit"*, *"month"*).
+ i. Let _untilResult_ be ? CalendarDateUntil(_calendarRec_.[[Receiver]], _plainRelativeTo_, _later_, _untilOptions_, _calendarRec_.[[DateUntil]]).
+ sec-temporal-balanceduration step 3.a:
+ 3. If _largestUnit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then
+ a. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _relativeTo_).
+ sec-temporal-roundduration steps 5.d and 8.n–p:
+ 5. If _unit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then
+ ...
+ d. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _intermediate_).
+ ...
+ 8. If _unit_ is *"year"*, then
+ ...
+ n. Let _untilOptions_ be ! OrdinaryObjectCreate(*null*).
+ o. Perform ! CreateDataPropertyOrThrow(_untilOptions_, *"largestUnit"*, *"year"*).
+ p. Let _timePassed_ be ? CalendarDateUntil(_calendar_, _relativeTo_, _daysLater_, _untilOptions_)
+ sec-temporal-nanosecondstodays step 11:
+ 11. 1. Let _dateDifference_ be ? DifferenceISODateTime(_startDateTime_.[[ISOYear]], _startDateTime_.[[ISOMonth]], _startDateTime_.[[ISODay]], _startDateTime_.[[ISOHour]], _startDateTime_.[[ISOMinute]], _startDateTime_.[[ISOSecond]], _startDateTime_.[[ISOMillisecond]], _startDateTime_.[[ISOMicrosecond]], _startDateTime_.[[ISONanosecond]], _endDateTime_.[[ISOYear]], _endDateTime_.[[ISOMonth]], _endDateTime_.[[ISODay]], _endDateTime_.[[ISOHour]], _endDateTime_.[[ISOMinute]], _endDateTime_.[[ISOSecond]], _endDateTime_.[[ISOMillisecond]], _endDateTime_.[[ISOMicrosecond]], _endDateTime_.[[ISONanosecond]], _relativeTo_.[[Calendar]], *"day"*).
+ sec-temporal-differenceisodatetime steps 9–11:
+ 9. Let _dateLargestUnit_ be ! LargerOfTwoTemporalUnits(*"day"*, _largestUnit_).
+ 10. Let _untilOptions_ be ? MergeLargestUnitOption(_options_, _dateLargestUnit_).
+ 11. Let _dateDifference_ be ? CalendarDateUntil(_calendar_, _date1_, _date2_, _untilOptions_).
+includes: [compareArray.js, temporalHelpers.js]
+features: [Temporal]
+---*/
+
+// Check the paths that go through NanosecondsToDays: only one call in
+// RoundDuration when the unit is a calendar unit. The others all
+// have largestUnit: "day" so the difference is taken in ISO calendar space.
+
+const duration = new Temporal.Duration(0, 1, 1, 1, 1, 1, 1, 1, 1, 1);
+
+TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
+ (calendar, unit) => {
+ const relativeTo = new Temporal.ZonedDateTime(0n, "UTC", calendar);
+ duration.total({ unit, relativeTo });
+ },
+ {
+ years: ["year"],
+ months: ["month"],
+ weeks: ["week"],
+ days: [],
+ hours: [],
+ minutes: [],
+ seconds: [],
+ milliseconds: [],
+ microseconds: [],
+ nanoseconds: []
+ }
+);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-fields-iterable.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-fields-iterable.js
new file mode 100644
index 0000000000..347d9a6a76
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-fields-iterable.js
@@ -0,0 +1,34 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Verify the result of calendar.fields() is treated correctly.
+info: |
+ sec-temporal.duration.prototype.total step 4:
+ 4. Let _relativeTo_ be ? ToRelativeTemporalObject(_options_).
+ sec-temporal-torelativetemporalobject step 4.c:
+ c. Let _fieldNames_ be ? CalendarFields(_calendar_, « *"day"*, *"month"*, *"monthCode"*, *"year"* »).
+ sec-temporal-calendarfields step 4:
+ 4. Let _result_ be ? IterableToList(_fieldsArray_).
+includes: [compareArray.js, temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const expected = [
+ "day",
+ "month",
+ "monthCode",
+ "year",
+];
+
+const calendar = TemporalHelpers.calendarFieldsIterable();
+const duration = new Temporal.Duration(1, 1, 1, 1, 1, 1, 1);
+duration.total({ unit: 'seconds', relativeTo: { year: 2000, month: 1, day: 1, calendar } });
+
+assert.sameValue(calendar.fieldsCallCount, 1, "fields() method called once");
+assert.compareArray(calendar.fieldsCalledWith[0], expected, "fields() method called with correct args");
+assert(calendar.iteratorExhausted[0], "iterated through the whole iterable");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-possibly-required.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-possibly-required.js
new file mode 100644
index 0000000000..154980346c
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-possibly-required.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Calendar required when days = 0 but years/months/weeks non-zero
+features: [Temporal]
+---*/
+
+const yearInstance = new Temporal.Duration(1999);
+const monthInstance = new Temporal.Duration(0, 49);
+const weekInstance = new Temporal.Duration(0, 0, 1);
+const dayInstance = new Temporal.Duration(0, 0, 0, 42);
+
+let relativeTo = new Temporal.PlainDate(2021, 12, 15);
+
+assert.throws(
+ RangeError,
+ () => { yearInstance.total({ unit: "days" }); },
+ "total a Duration with non-zero years fails without largest/smallest unit"
+);
+const yearResult = yearInstance.total({ unit: "days", relativeTo });
+assert.sameValue(yearResult, 730120, "year duration contains proper days");
+
+assert.throws(
+ RangeError,
+ () => { monthInstance.total({ unit: "days" }); },
+ "total a Duration with non-zero month fails without largest/smallest unit"
+);
+
+const monthResult = monthInstance.total({ unit: "days", relativeTo });
+assert.sameValue(monthResult, 1492, "month duration contains proper days");
+
+assert.throws(
+ RangeError,
+ () => { weekInstance.total({ unit: "days" }); },
+ "total a Duration with non-zero weeks fails without largest/smallest unit"
+);
+
+const weekResult = weekInstance.total({ unit: "days", relativeTo });
+assert.sameValue(weekResult, 7, "week duration contains proper days");
+
+const dayResultWithoutRelative = dayInstance.total({ unit: "days" });
+const dayResultWithRelative = dayInstance.total({ unit: "days", relativeTo });
+assert.sameValue(dayResultWithoutRelative, 42, "day duration without relative-to part contains proper days");
+assert.sameValue(dayResultWithRelative, 42, "day duration with relative-to part contains proper days");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-temporal-object.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-temporal-object.js
new file mode 100644
index 0000000000..12b0b55148
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-temporal-object.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Fast path for converting other Temporal objects to Temporal.Calendar by reading internal slots
+info: |
+ sec-temporal.duration.prototype.total step 4:
+ 4. Let _relativeTo_ be ? ToRelativeTemporalObject(_options_).
+ sec-temporal-torelativetemporalobject step 4.b:
+ b. Let _calendar_ be ? GetTemporalCalendarWithISODefault(_item_).
+ sec-temporal-gettemporalcalendarwithisodefault step 2:
+ 2. Return ? ToTemporalCalendarWithISODefault(_calendar_).
+ sec-temporal-totemporalcalendarwithisodefault step 2:
+ 3. Return ? ToTemporalCalendar(_temporalCalendarLike_).
+ sec-temporal-totemporalcalendar step 1.a:
+ a. If _temporalCalendarLike_ has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then
+ i. Return _temporalCalendarLike_.[[Calendar]].
+includes: [compareArray.js, temporalHelpers.js]
+features: [Temporal]
+---*/
+
+TemporalHelpers.checkToTemporalCalendarFastPath((temporalObject) => {
+ const duration = new Temporal.Duration(1, 1, 1, 1, 1, 1, 1);
+ duration.total({ unit: 'seconds', relativeTo: { year: 2000, month: 1, day: 1, calendar: temporalObject } });
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/constructor-in-calendar-fields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/constructor-in-calendar-fields.js
new file mode 100644
index 0000000000..4caf54a64c
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/constructor-in-calendar-fields.js
@@ -0,0 +1,17 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.duration.prototype.total
+description: If a calendar's fields() method returns a field named 'constructor', PrepareTemporalFields should throw a RangeError.
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const calendar = TemporalHelpers.calendarWithExtraFields(['constructor']);
+const relativeTo = { year: 2023, month: 5, monthCode: 'M05', day: 1, calendar: calendar, timeZone: 'Europe/Paris' };
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }));
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dateuntil-field.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dateuntil-field.js
new file mode 100644
index 0000000000..4ac47adda0
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dateuntil-field.js
@@ -0,0 +1,37 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ When consulting calendar.dateUntil() to calculate the number of months in a
+ year, the months property is not accessed on the result Duration
+includes: [compareArray.js, temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const actual = [];
+
+class CalendarDateUntilObservable extends Temporal.Calendar {
+ dateUntil(...args) {
+ actual.push("call dateUntil");
+ const returnValue = super.dateUntil(...args);
+ TemporalHelpers.observeProperty(actual, returnValue, "months", Infinity);
+ return returnValue;
+ }
+}
+
+const calendar = new CalendarDateUntilObservable("iso8601");
+const relativeTo = new Temporal.PlainDate(2018, 10, 12, calendar);
+
+const expected = [
+ "call dateUntil",
+];
+
+const years = new Temporal.Duration(2);
+const result = years.total({ unit: "months", relativeTo });
+assert.sameValue(result, 24, "result");
+assert.compareArray(actual, expected, "operations");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duplicate-calendar-fields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duplicate-calendar-fields.js
new file mode 100644
index 0000000000..516d4f56e8
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duplicate-calendar-fields.js
@@ -0,0 +1,19 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.duration.prototype.total
+description: If a calendar's fields() method returns duplicate field names, PrepareTemporalFields should throw a RangeError.
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+for (const extra_fields of [['foo', 'foo'], ['day'], ['month'], ['monthCode'], ['year']]) {
+ const calendar = TemporalHelpers.calendarWithExtraFields(extra_fields);
+ const relativeTo = { year: 2023, month: 5, monthCode: 'M05', day: 1, calendar: calendar, timeZone: 'Europe/Paris' };
+ const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+ assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }));
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js
new file mode 100644
index 0000000000..2abd469541
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js
@@ -0,0 +1,28 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: RangeError thrown when calendar part of duration added to relativeTo is out of range
+features: [Temporal]
+info: |
+ RoundDuration:
+ 8.k. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*).
+ l. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_).
+---*/
+
+// Based on a test case by André Bargull <andre.bargull@gmail.com>
+
+const instance = new Temporal.Duration(0, 0, 0, /* days = */ 500_000_000);
+const relativeTo = new Temporal.PlainDate(2000, 1, 1);
+assert.throws(RangeError, () => instance.total({relativeTo, unit: "years"}));
+assert.throws(RangeError, () => instance.total({relativeTo, unit: "months"}));
+assert.throws(RangeError, () => instance.total({relativeTo, unit: "weeks"}));
+
+const negInstance = new Temporal.Duration(0, 0, 0, /* days = */ -500_000_000);
+assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "years"}));
+assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "months"}));
+assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "weeks"}));
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/length.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/length.js
new file mode 100644
index 0000000000..e805ba32bc
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/length.js
@@ -0,0 +1,28 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Temporal.Duration.prototype.total.length is 1
+info: |
+ Every built-in function object, including constructors, has a "length" property whose value is
+ an integer. Unless otherwise specified, this value is equal to the largest number of named
+ arguments shown in the subclause headings for the function description. Optional parameters
+ (which are indicated with brackets: [ ]) or rest parameters (which are shown using the form
+ «...name») are not included in the default argument count.
+
+ Unless otherwise specified, the "length" property of a built-in function object has the
+ attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }.
+includes: [propertyHelper.js]
+features: [Temporal]
+---*/
+
+verifyProperty(Temporal.Duration.prototype.total, "length", {
+ value: 1,
+ writable: false,
+ enumerable: false,
+ configurable: true,
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/name.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/name.js
new file mode 100644
index 0000000000..9356d71761
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/name.js
@@ -0,0 +1,26 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Temporal.Duration.prototype.total.name is "total".
+info: |
+ Every built-in function object, including constructors, that is not identified as an anonymous
+ function has a "name" property whose value is a String. Unless otherwise specified, this value
+ is the name that is given to the function in this specification.
+
+ Unless otherwise specified, the "name" property of a built-in function object, if it exists,
+ has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }.
+includes: [propertyHelper.js]
+features: [Temporal]
+---*/
+
+verifyProperty(Temporal.Duration.prototype.total, "name", {
+ value: "total",
+ writable: false,
+ enumerable: false,
+ configurable: true,
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js
new file mode 100644
index 0000000000..72260abd68
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js
@@ -0,0 +1,80 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ NormalizedTimeDurationToDays can loop arbitrarily up to max safe integer
+info: |
+ NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] )
+ ...
+ 21. Repeat, while done is false,
+ a. Let oneDayFarther be ? AddDaysToZonedDateTime(relativeResult.[[Instant]],
+ relativeResult.[[DateTime]], timeZoneRec, zonedRelativeTo.[[Calendar]], sign).
+ b. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(oneDayFarther.[[EpochNanoseconds]],
+ relativeResult.[[EpochNanoseconds]]).
+ c. Let oneDayLess be ? SubtractNormalizedTimeDuration(norm, dayLengthNs).
+ c. If NormalizedTimeDurationSign(oneDayLess) × sign ≥ 0, then
+ i. Set norm to oneDayLess.
+ ii. Set relativeResult to oneDayFarther.
+ iii. Set days to days + sign.
+ d. Else,
+ i. Set done to true.
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const calls = [];
+const duration = Temporal.Duration.from({ days: 1 });
+
+function createRelativeTo(count) {
+ const dayLengthNs = 86400000000000n;
+ const dayInstant = new Temporal.Instant(dayLengthNs);
+ const substitutions = [];
+ const timeZone = new Temporal.TimeZone("UTC");
+ // Return constant value for first _count_ calls
+ TemporalHelpers.substituteMethod(
+ timeZone,
+ "getPossibleInstantsFor",
+ substitutions
+ );
+ substitutions.length = count;
+ let i = 0;
+ for (i = 0; i < substitutions.length; i++) {
+ // (this value)
+ substitutions[i] = [dayInstant];
+ }
+ // Record calls in calls[]
+ TemporalHelpers.observeMethod(calls, timeZone, "getPossibleInstantsFor");
+ return new Temporal.ZonedDateTime(0n, timeZone);
+}
+
+let zdt = createRelativeTo(50);
+calls.splice(0); // Reset calls list after ZonedDateTime construction
+duration.total({
+ unit: "day",
+ relativeTo: zdt,
+});
+assert.sameValue(
+ calls.length,
+ 50 + 2,
+ "Expected duration.total to call getPossibleInstantsFor correct number of times"
+);
+
+zdt = createRelativeTo(100);
+calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
+duration.total({
+ unit: "day",
+ relativeTo: zdt,
+});
+assert.sameValue(
+ calls.length,
+ 100 + 2,
+ "Expected duration.total to call getPossibleInstantsFor correct number of times"
+);
+
+zdt = createRelativeTo(106);
+assert.throws(RangeError, () => duration.total({ unit: "day", relativeTo: zdt }), "106-1 days > 2⁵³ ns");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/not-a-constructor.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/not-a-constructor.js
new file mode 100644
index 0000000000..193622f8c9
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/not-a-constructor.js
@@ -0,0 +1,24 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Temporal.Duration.prototype.total does not implement [[Construct]], is not new-able
+info: |
+ Built-in function objects that are not identified as constructors do not implement the
+ [[Construct]] internal method unless otherwise specified in the description of a particular
+ function.
+includes: [isConstructor.js]
+features: [Reflect.construct, Temporal]
+---*/
+
+assert.throws(TypeError, () => {
+ new Temporal.Duration.prototype.total();
+}, "Calling as constructor");
+
+assert.sameValue(isConstructor(Temporal.Duration.prototype.total), false,
+ "isConstructor(Temporal.Duration.prototype.total)");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/options-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/options-wrong-type.js
new file mode 100644
index 0000000000..3f0e77111a
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/options-wrong-type.js
@@ -0,0 +1,27 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: TypeError thrown when options argument is missing or a non-string primitive
+features: [BigInt, Symbol, Temporal]
+---*/
+
+const badOptions = [
+ undefined,
+ null,
+ true,
+ Symbol(),
+ 1,
+ 2n,
+];
+
+const instance = new Temporal.Duration(0, 0, 0, 0, 1);
+assert.throws(TypeError, () => instance.total(), "TypeError on missing options argument");
+for (const value of badOptions) {
+ assert.throws(TypeError, () => instance.total(value),
+ `TypeError on wrong options type ${typeof value}`);
+};
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/order-of-operations.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/order-of-operations.js
new file mode 100644
index 0000000000..24262e35f8
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/order-of-operations.js
@@ -0,0 +1,396 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Properties on objects passed to total() are accessed in the correct order
+includes: [compareArray.js, temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const expected = [
+ "get options.relativeTo",
+ "get options.unit",
+ "get options.unit.toString",
+ "call options.unit.toString",
+];
+const actual = [];
+
+function createOptionsObserver({ unit = "nanoseconds", roundingMode = "halfExpand", roundingIncrement = 1, relativeTo = undefined } = {}) {
+ return TemporalHelpers.propertyBagObserver(actual, {
+ unit,
+ roundingMode,
+ roundingIncrement,
+ relativeTo,
+ }, "options");
+}
+
+const instance = new Temporal.Duration(0, 0, 0, 0, 2400);
+
+// basic order of observable operations, with no relativeTo
+instance.total(createOptionsObserver({ unit: "nanoseconds" }));
+assert.compareArray(actual, expected, "order of operations");
+actual.splice(0); // clear
+
+const expectedOpsForPlainRelativeTo = [
+ // ToRelativeTemporalObject
+ "get options.relativeTo",
+ "get options.relativeTo.calendar",
+ "has options.relativeTo.calendar.dateAdd",
+ "has options.relativeTo.calendar.dateFromFields",
+ "has options.relativeTo.calendar.dateUntil",
+ "has options.relativeTo.calendar.day",
+ "has options.relativeTo.calendar.dayOfWeek",
+ "has options.relativeTo.calendar.dayOfYear",
+ "has options.relativeTo.calendar.daysInMonth",
+ "has options.relativeTo.calendar.daysInWeek",
+ "has options.relativeTo.calendar.daysInYear",
+ "has options.relativeTo.calendar.fields",
+ "has options.relativeTo.calendar.id",
+ "has options.relativeTo.calendar.inLeapYear",
+ "has options.relativeTo.calendar.mergeFields",
+ "has options.relativeTo.calendar.month",
+ "has options.relativeTo.calendar.monthCode",
+ "has options.relativeTo.calendar.monthDayFromFields",
+ "has options.relativeTo.calendar.monthsInYear",
+ "has options.relativeTo.calendar.weekOfYear",
+ "has options.relativeTo.calendar.year",
+ "has options.relativeTo.calendar.yearMonthFromFields",
+ "has options.relativeTo.calendar.yearOfWeek",
+ "get options.relativeTo.calendar.dateFromFields",
+ "get options.relativeTo.calendar.fields",
+ "call options.relativeTo.calendar.fields",
+ "get options.relativeTo.day",
+ "get options.relativeTo.day.valueOf",
+ "call options.relativeTo.day.valueOf",
+ "get options.relativeTo.hour",
+ "get options.relativeTo.microsecond",
+ "get options.relativeTo.millisecond",
+ "get options.relativeTo.minute",
+ "get options.relativeTo.month",
+ "get options.relativeTo.month.valueOf",
+ "call options.relativeTo.month.valueOf",
+ "get options.relativeTo.monthCode",
+ "get options.relativeTo.monthCode.toString",
+ "call options.relativeTo.monthCode.toString",
+ "get options.relativeTo.nanosecond",
+ "get options.relativeTo.offset",
+ "get options.relativeTo.second",
+ "get options.relativeTo.timeZone",
+ "get options.relativeTo.year",
+ "get options.relativeTo.year.valueOf",
+ "call options.relativeTo.year.valueOf",
+ "call options.relativeTo.calendar.dateFromFields",
+ // GetTemporalUnit
+ "get options.unit",
+ "get options.unit.toString",
+ "call options.unit.toString",
+ // lookup in Duration.p.total
+ "get options.relativeTo.calendar.dateAdd",
+ "get options.relativeTo.calendar.dateUntil",
+];
+
+const plainRelativeTo = TemporalHelpers.propertyBagObserver(actual, {
+ year: 2001,
+ month: 5,
+ monthCode: "M05",
+ day: 2,
+ calendar: TemporalHelpers.calendarObserver(actual, "options.relativeTo.calendar"),
+}, "options.relativeTo");
+
+// basic order of observable operations, without rounding:
+instance.total(createOptionsObserver({ unit: "nanoseconds", relativeTo: plainRelativeTo }));
+assert.compareArray(actual, expectedOpsForPlainRelativeTo, "order of operations for PlainDate relativeTo");
+actual.splice(0); // clear
+
+// code path through RoundDuration that rounds to the nearest year with minimal calendar calls:
+const expectedOpsForMinimalYearRounding = expectedOpsForPlainRelativeTo.concat([
+ // 12.d and 12.f not called because years, months, weeks are 0
+ "call options.relativeTo.calendar.dateUntil", // 12.n
+ // 12.r not called because years, months, weeks are 0
+ "call options.relativeTo.calendar.dateAdd", // 12.x MoveRelativeDate
+]);
+instance.total(createOptionsObserver({ unit: "years", relativeTo: plainRelativeTo }));
+assert.compareArray(actual, expectedOpsForMinimalYearRounding, "order of operations with years = 0 and unit = years");
+actual.splice(0); // clear
+
+// code path through RoundDuration that rounds to the nearest year:
+const expectedOpsForYearRounding = expectedOpsForPlainRelativeTo.concat([
+ "call options.relativeTo.calendar.dateAdd", // 12.d
+ "call options.relativeTo.calendar.dateAdd", // 12.f
+ "call options.relativeTo.calendar.dateUntil", // 12.n
+ "call options.relativeTo.calendar.dateAdd", // 12.r MoveRelativeDate
+ "call options.relativeTo.calendar.dateAdd", // 12.x MoveRelativeDate
+]);
+const instanceYears = new Temporal.Duration(1, 12, 0, 0, /* hours = */ 2400);
+instanceYears.total(createOptionsObserver({ unit: "years", relativeTo: plainRelativeTo }));
+assert.compareArray(actual, expectedOpsForYearRounding, "order of operations with unit = years");
+actual.splice(0); // clear
+
+// code path through Duration.prototype.total that rounds to the nearest month:
+const expectedOpsForMonthRounding = expectedOpsForPlainRelativeTo.concat([
+ // UnbalanceDateDurationRelative
+ "call options.relativeTo.calendar.dateAdd", // 3.f
+ "call options.relativeTo.calendar.dateUntil", // 3.i
+ // RoundDuration
+ "call options.relativeTo.calendar.dateAdd", // 13.c
+ "call options.relativeTo.calendar.dateAdd", // 13.e
+ "call options.relativeTo.calendar.dateUntil", // 13.m
+ "call options.relativeTo.calendar.dateAdd", // 13.q MoveRelativeDate
+ "call options.relativeTo.calendar.dateAdd", // 13.w MoveRelativeDate
+]);
+const instance2 = new Temporal.Duration(1, 0, 0, 62);
+instance2.total(createOptionsObserver({ unit: "months", relativeTo: plainRelativeTo }));
+assert.compareArray(actual, expectedOpsForMonthRounding, "order of operations with unit = months");
+actual.splice(0); // clear
+
+// code path through Duration.prototype.total that rounds to the nearest week:
+const expectedOpsForWeekRounding = expectedOpsForPlainRelativeTo.concat([
+ // UnbalanceDateDurationRelative
+ "call options.relativeTo.calendar.dateAdd", // 4.e
+ // RoundDuration
+ "call options.relativeTo.calendar.dateUntil", // 14.f
+ "call options.relativeTo.calendar.dateAdd", // 14.j MoveRelativeDate
+ "call options.relativeTo.calendar.dateAdd", // 14.p MoveRelativeDate
+]);
+const instance3 = new Temporal.Duration(1, 1, 0, 15);
+instance3.total(createOptionsObserver({ unit: "weeks", relativeTo: plainRelativeTo }));
+assert.compareArray(actual, expectedOpsForWeekRounding, "order of operations with unit = weeks");
+actual.splice(0); // clear
+
+// code path through UnbalanceDateDurationRelative that rounds to the nearest day:
+const expectedOpsForDayRounding = expectedOpsForPlainRelativeTo.concat([
+ "call options.relativeTo.calendar.dateAdd", // 10
+]);
+const instance4 = new Temporal.Duration(1, 1, 1)
+instance4.total(createOptionsObserver({ unit: "days", relativeTo: plainRelativeTo }));
+assert.compareArray(actual, expectedOpsForDayRounding, "order of operations with unit = days");
+actual.splice(0); // clear
+
+const expectedOpsForZonedRelativeTo = [
+ // ToRelativeTemporalObject
+ "get options.relativeTo",
+ "get options.relativeTo.calendar",
+ "has options.relativeTo.calendar.dateAdd",
+ "has options.relativeTo.calendar.dateFromFields",
+ "has options.relativeTo.calendar.dateUntil",
+ "has options.relativeTo.calendar.day",
+ "has options.relativeTo.calendar.dayOfWeek",
+ "has options.relativeTo.calendar.dayOfYear",
+ "has options.relativeTo.calendar.daysInMonth",
+ "has options.relativeTo.calendar.daysInWeek",
+ "has options.relativeTo.calendar.daysInYear",
+ "has options.relativeTo.calendar.fields",
+ "has options.relativeTo.calendar.id",
+ "has options.relativeTo.calendar.inLeapYear",
+ "has options.relativeTo.calendar.mergeFields",
+ "has options.relativeTo.calendar.month",
+ "has options.relativeTo.calendar.monthCode",
+ "has options.relativeTo.calendar.monthDayFromFields",
+ "has options.relativeTo.calendar.monthsInYear",
+ "has options.relativeTo.calendar.weekOfYear",
+ "has options.relativeTo.calendar.year",
+ "has options.relativeTo.calendar.yearMonthFromFields",
+ "has options.relativeTo.calendar.yearOfWeek",
+ "get options.relativeTo.calendar.dateFromFields",
+ "get options.relativeTo.calendar.fields",
+ "call options.relativeTo.calendar.fields",
+ "get options.relativeTo.day",
+ "get options.relativeTo.day.valueOf",
+ "call options.relativeTo.day.valueOf",
+ "get options.relativeTo.hour",
+ "get options.relativeTo.hour.valueOf",
+ "call options.relativeTo.hour.valueOf",
+ "get options.relativeTo.microsecond",
+ "get options.relativeTo.microsecond.valueOf",
+ "call options.relativeTo.microsecond.valueOf",
+ "get options.relativeTo.millisecond",
+ "get options.relativeTo.millisecond.valueOf",
+ "call options.relativeTo.millisecond.valueOf",
+ "get options.relativeTo.minute",
+ "get options.relativeTo.minute.valueOf",
+ "call options.relativeTo.minute.valueOf",
+ "get options.relativeTo.month",
+ "get options.relativeTo.month.valueOf",
+ "call options.relativeTo.month.valueOf",
+ "get options.relativeTo.monthCode",
+ "get options.relativeTo.monthCode.toString",
+ "call options.relativeTo.monthCode.toString",
+ "get options.relativeTo.nanosecond",
+ "get options.relativeTo.nanosecond.valueOf",
+ "call options.relativeTo.nanosecond.valueOf",
+ "get options.relativeTo.offset",
+ "get options.relativeTo.offset.toString",
+ "call options.relativeTo.offset.toString",
+ "get options.relativeTo.second",
+ "get options.relativeTo.second.valueOf",
+ "call options.relativeTo.second.valueOf",
+ "get options.relativeTo.timeZone",
+ "get options.relativeTo.year",
+ "get options.relativeTo.year.valueOf",
+ "call options.relativeTo.year.valueOf",
+ "call options.relativeTo.calendar.dateFromFields",
+ "has options.relativeTo.timeZone.getOffsetNanosecondsFor",
+ "has options.relativeTo.timeZone.getPossibleInstantsFor",
+ "has options.relativeTo.timeZone.id",
+ "get options.relativeTo.timeZone.getOffsetNanosecondsFor",
+ "get options.relativeTo.timeZone.getPossibleInstantsFor",
+ // InterpretISODateTimeOffset
+ "call options.relativeTo.timeZone.getPossibleInstantsFor",
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor",
+ // GetTemporalUnit
+ "get options.unit",
+ "get options.unit.toString",
+ "call options.unit.toString",
+];
+
+const zonedRelativeTo = TemporalHelpers.propertyBagObserver(actual, {
+ year: 2001,
+ month: 5,
+ monthCode: "M05",
+ day: 2,
+ hour: 6,
+ minute: 54,
+ second: 32,
+ millisecond: 987,
+ microsecond: 654,
+ nanosecond: 321,
+ offset: "+00:00",
+ calendar: TemporalHelpers.calendarObserver(actual, "options.relativeTo.calendar"),
+ timeZone: TemporalHelpers.timeZoneObserver(actual, "options.relativeTo.timeZone"),
+}, "options.relativeTo");
+
+// basic order of observable operations, without rounding:
+instance.total(createOptionsObserver({ unit: "nanoseconds", relativeTo: zonedRelativeTo }));
+assert.compareArray(actual, expectedOpsForZonedRelativeTo.concat([
+ "get options.relativeTo.calendar.dateAdd",
+ "get options.relativeTo.calendar.dateUntil",
+]), "order of operations for ZonedDateTime relativeTo");
+actual.splice(0); // clear
+
+// code path through RoundDuration that rounds to the nearest year with minimal calendar operations:
+const expectedOpsForMinimalYearRoundingZoned = expectedOpsForZonedRelativeTo.concat([
+ // ToTemporalDate
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor",
+ // lookup in Duration.p.total
+ "get options.relativeTo.calendar.dateAdd",
+ "get options.relativeTo.calendar.dateUntil",
+ // BalancePossiblyInfiniteDuration → NanosecondsToDays
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 7. GetPlainDateTimeFor
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 11. GetPlainDateTimeFor
+ // BalancePossiblyInfiniteDuration → NanosecondsToDays → AddDaysToZonedDateTime
+ "call options.relativeTo.timeZone.getPossibleInstantsFor",
+ // BalancePossiblyInfiniteDuration → NanosecondsToDays → AddDaysToZonedDateTime
+ "call options.relativeTo.timeZone.getPossibleInstantsFor",
+], [
+ // code path through RoundDuration that rounds to the nearest year:
+ // MoveRelativeZonedDateTime → AddDaysToZonedDateTime
+ "call options.relativeTo.timeZone.getPossibleInstantsFor",
+ // 12.d and 12.f not called because years, months, weeks are 0
+ "call options.relativeTo.calendar.dateUntil", // 12.n
+ // 12.r not called because years, months, weeks are 0
+ "call options.relativeTo.calendar.dateAdd", // 12.x MoveRelativeDate
+]);
+instance.total(createOptionsObserver({ unit: "years", relativeTo: zonedRelativeTo }));
+assert.compareArray(
+ actual,
+ expectedOpsForMinimalYearRoundingZoned,
+ "order of operations with years = 0, unit = years and ZonedDateTime relativeTo"
+);
+actual.splice(0); // clear
+
+// code path through RoundDuration that rounds to the nearest year:
+const expectedOpsForYearRoundingZoned = expectedOpsForZonedRelativeTo.concat([
+ // ToTemporalDate
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor",
+ // lookup in Duration.p.total
+ "get options.relativeTo.calendar.dateAdd",
+ "get options.relativeTo.calendar.dateUntil",
+ // MoveRelativeZonedDateTime → AddZonedDateTime
+ "call options.relativeTo.calendar.dateAdd",
+ "call options.relativeTo.timeZone.getPossibleInstantsFor",
+ // BalancePossiblyInfiniteTimeDurationRelative → NanosecondsToDays
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 8. GetPlainDateTimeFor
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 9. GetPlainDateTimeFor
+ // BalancePossiblyInfiniteTimeDurationRelative → NanosecondsToDays → AddDaysToZonedDateTime
+ "call options.relativeTo.timeZone.getPossibleInstantsFor",
+ // BalancePossiblyInfiniteTimeDurationRelative → NanosecondsToDays → AddDaysToZonedDateTime
+ "call options.relativeTo.timeZone.getPossibleInstantsFor",
+ // RoundDuration → MoveRelativeZonedDateTime → AddZonedDateTime
+ "call options.relativeTo.calendar.dateAdd",
+ "call options.relativeTo.timeZone.getPossibleInstantsFor",
+ // RoundDuration
+ "call options.relativeTo.calendar.dateAdd", // 12.d
+ "call options.relativeTo.calendar.dateAdd", // 12.f
+ "call options.relativeTo.calendar.dateUntil", // 12.n
+ "call options.relativeTo.calendar.dateAdd", // 12.r MoveRelativeDate
+ "call options.relativeTo.calendar.dateAdd", // 12.x MoveRelativeDate
+]);
+instanceYears.total(createOptionsObserver({ unit: "years", relativeTo: zonedRelativeTo }));
+assert.compareArray(
+ actual,
+ expectedOpsForYearRoundingZoned,
+ "order of operations with unit = years and ZonedDateTime relativeTo"
+);
+actual.splice(0); // clear
+
+// code path that hits UnbalanceDateDurationRelative and RoundDuration
+const expectedOpsForUnbalanceRound = expectedOpsForZonedRelativeTo.concat([
+ // ToTemporalDate
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor",
+ // lookup in Duration.p.total
+ "get options.relativeTo.calendar.dateAdd",
+ "get options.relativeTo.calendar.dateUntil",
+ // No user code calls in UnbalanceDateDurationRelative
+ // MoveRelativeZonedDateTime → AddZonedDateTime
+ "call options.relativeTo.calendar.dateAdd",
+ "call options.relativeTo.timeZone.getPossibleInstantsFor", // 13. GetInstantFor
+ // RoundDuration → MoveRelativeZonedDateTime → AddZonedDateTime
+ "call options.relativeTo.calendar.dateAdd",
+ "call options.relativeTo.timeZone.getPossibleInstantsFor", // 13. GetInstantFor
+ // RoundDuration
+ "call options.relativeTo.calendar.dateAdd", // 13.c
+ "call options.relativeTo.calendar.dateAdd", // 13.e
+ "call options.relativeTo.calendar.dateUntil", // 13.m
+ "call options.relativeTo.calendar.dateAdd", // 13.w MoveRelativeDate
+]);
+new Temporal.Duration(0, 1, 1).total(createOptionsObserver({ unit: "months", relativeTo: zonedRelativeTo }));
+assert.compareArray(
+ actual,
+ expectedOpsForUnbalanceRound,
+ "order of operations with unit = months and ZonedDateTime relativeTo"
+);
+actual.splice(0); // clear
+
+// code path that avoids converting Zoned twice in BalanceTimeDurationRelative
+const expectedOpsForBalanceRound = expectedOpsForZonedRelativeTo.concat([
+ // ToTemporalDate
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor",
+ // lookup in Duration.p.total
+ "get options.relativeTo.calendar.dateAdd",
+ "get options.relativeTo.calendar.dateUntil",
+ // No user code calls in UnbalanceDateDurationRelative
+ // No user code calls in AddZonedDateTime (years, months, weeks = 0)
+ // BalanceTimeDurationRelative
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 4.a
+ "call options.relativeTo.timeZone.getPossibleInstantsFor", // 4.b
+ "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // NanosecondsToDays 9
+ "call options.relativeTo.timeZone.getPossibleInstantsFor", // NanosecondsToDays 26
+ "call options.relativeTo.timeZone.getPossibleInstantsFor", // NanosecondsToDays 31.a
+ // RoundDuration → MoveRelativeZonedDateTime → AddZonedDateTime
+ "call options.relativeTo.timeZone.getPossibleInstantsFor", // 10. GetInstantFor
+ // RoundDuration
+ "call options.relativeTo.calendar.dateUntil", // 14.f
+ "call options.relativeTo.calendar.dateAdd", // 14.j MoveRelativeDate
+ "call options.relativeTo.calendar.dateAdd", // 14.p MoveRelativeDate
+]);
+new Temporal.Duration(0, 0, 0, 1, 240).total(createOptionsObserver({ unit: "weeks", relativeTo: zonedRelativeTo }));
+assert.compareArray(
+ actual,
+ expectedOpsForBalanceRound,
+ "order of operations with unit = weeks and no calendar units"
+);
+actual.splice(0); // clear
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js
new file mode 100644
index 0000000000..9c74187855
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js
@@ -0,0 +1,99 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 André Bargull. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ RoundDuration computes on exact mathematical values.
+features: [Temporal]
+---*/
+
+// Return the next Number value in direction to +Infinity.
+function nextUp(num) {
+ if (!Number.isFinite(num)) {
+ return num;
+ }
+ if (num === 0) {
+ return Number.MIN_VALUE;
+ }
+
+ var f64 = new Float64Array([num]);
+ var u64 = new BigUint64Array(f64.buffer);
+ u64[0] += (num < 0 ? -1n : 1n);
+ return f64[0];
+}
+
+// Return the next Number value in direction to -Infinity.
+function nextDown(num) {
+ if (!Number.isFinite(num)) {
+ return num;
+ }
+ if (num === 0) {
+ return -Number.MIN_VALUE;
+ }
+
+ var f64 = new Float64Array([num]);
+ var u64 = new BigUint64Array(f64.buffer);
+ u64[0] += (num < 0 ? 1n : -1n);
+ return f64[0];
+}
+
+let duration = Temporal.Duration.from({
+ hours: 4000,
+ nanoseconds: 1,
+});
+
+let total = duration.total({unit: "hours"});
+
+// From RoundDuration():
+//
+// 7. Let fractionalSeconds be nanoseconds × 10^-9 + microseconds × 10^-6 + milliseconds × 10^-3 + seconds.
+// = nanoseconds × 10^-9
+// = 1 × 10^-9
+// = 10^-9
+// = 0.000000001
+//
+// 13.a. Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours.
+// = (fractionalSeconds / 60) / 60 + 4000
+// = 0.000000001 / 3600 + 4000
+//
+// 13.b. Set hours to RoundNumberToIncrement(fractionalHours, increment, roundingMode).
+// = trunc(fractionalHours)
+// = trunc(0.000000001 / 3600 + 4000)
+// = 4000
+//
+// 13.c. Set remainder to fractionalHours - hours.
+// = fractionalHours - hours
+// = 0.000000001 / 3600 + 4000 - 4000
+// = 0.000000001 / 3600
+//
+// From Temporal.Duration.prototype.total ( options ):
+//
+// 18. If unit is "hours", then let whole be roundResult.[[Hours]].
+// ...
+// 24. Return whole + roundResult.[[Remainder]].
+//
+// |whole| is 4000 and the remainder is (0.000000001 / 3600).
+//
+// 0.000000001 / 3600
+// = (1 / 10^9) / 3600
+// = (1 / 36) / 10^11
+// = 0.02777.... / 10^11
+// = 0.0000000000002777...
+//
+// 4000.0000000000002777... can't be represented exactly, the next best approximation
+// is 4000.0000000000005.
+
+const expected = 4000.0000000000005;
+assert.sameValue(expected, 4000.0000000000002777, "the float representation of the result is 4000.0000000000005");
+
+// The next Number in direction -Infinity is less precise.
+assert.sameValue(nextDown(expected), 4000, "the next Number in direction -Infinity is less precise");
+
+// The next Number in direction +Infinity is less precise.
+assert.sameValue(nextUp(expected), 4000.000000000001, "the next Number in direction +Infinity is less precise");
+
+assert.sameValue(total, expected, "return value of total()");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js
new file mode 100644
index 0000000000..2a73bb5ef0
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js
@@ -0,0 +1,101 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 André Bargull. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ RoundDuration computes on exact mathematical values.
+features: [Temporal]
+---*/
+
+// Return the next Number value in direction to +Infinity.
+function nextUp(num) {
+ if (!Number.isFinite(num)) {
+ return num;
+ }
+ if (num === 0) {
+ return Number.MIN_VALUE;
+ }
+
+ var f64 = new Float64Array([num]);
+ var u64 = new BigUint64Array(f64.buffer);
+ u64[0] += (num < 0 ? -1n : 1n);
+ return f64[0];
+}
+
+// Return the next Number value in direction to -Infinity.
+function nextDown(num) {
+ if (!Number.isFinite(num)) {
+ return num;
+ }
+ if (num === 0) {
+ return -Number.MIN_VALUE;
+ }
+
+ var f64 = new Float64Array([num]);
+ var u64 = new BigUint64Array(f64.buffer);
+ u64[0] += (num < 0 ? 1n : -1n);
+ return f64[0];
+}
+
+let duration = Temporal.Duration.from({
+ hours: 4000,
+ minutes: 59,
+ seconds: 59,
+ milliseconds: 999,
+ microseconds: 999,
+ nanoseconds: 999,
+});
+
+let total = duration.total({unit: "hours"});
+
+// From RoundDuration():
+//
+// 7. Let fractionalSeconds be nanoseconds × 10^-9 + microseconds × 10^-6 + milliseconds × 10^-3 + seconds.
+// = 999 × 10^-9 + 999 × 10^-6 + 999 × 10^-3 + 59
+// = 59.999'999'999
+//
+// 13.a. Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours.
+// = (59.999'999'999 / 60 + 59) / 60 + 4000
+// = 1 - 0.000000001 / 3600 + 4000
+//
+// 13.b. Set hours to RoundNumberToIncrement(fractionalHours, increment, roundingMode).
+// = trunc(fractionalHours)
+// = trunc(1 - 0.000000001 / 3600 + 4000)
+// = 4000
+//
+// 13.c. Set remainder to fractionalHours - hours.
+// = fractionalHours - hours
+// = 1 - 0.000000001 / 3600 + 4000 - 4000
+// = 1 - 0.000000001 / 3600
+//
+// From Temporal.Duration.prototype.total ( options ):
+//
+// 18. If unit is "hours", then let whole be roundResult.[[Hours]].
+// ...
+// 24. Return whole + roundResult.[[Remainder]].
+//
+// |whole| is 4000 and the remainder is (1 - 0.000000001 / 3600).
+//
+// 1 - 0.000000001 / 3600
+// = 1 - (1 / 10^9) / 3600
+// = 1 - (1 / 36) / 10^11
+// = 1 - 0.02777.... / 10^11
+// = 0.9999999999997222...
+//
+// 4000.9999999999997222... can't be represented exactly, the next best approximation
+// is 4000.9999999999995.
+
+const expected = 4000.9999999999995;
+assert.sameValue(expected, 4000.9999999999997222);
+
+// The next Number in direction -Infinity is less precise.
+assert.sameValue(nextDown(expected), 4000.999999999999);
+
+// The next Number in direction +Infinity is less precise.
+assert.sameValue(nextUp(expected), 4001);
+
+assert.sameValue(total, expected);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js
new file mode 100644
index 0000000000..ab3fd772cc
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js
@@ -0,0 +1,121 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ RoundDuration computes in such a way as to avoid precision loss when the
+ computed day, week, month, and year lengths are very large numbers.
+info: |
+ RoundDuration:
+ ...
+ 7. If _unit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then
+ a. If _zonedRelativeTo_ is not *undefined*, then
+ ...
+ iii. Let _fractionalDays_ be _days_ + _result_.[[Days]] + DivideNormalizedTimeDuration(_result_.[[Remainder]], _result_.[[DayLength]]).
+ ...
+ 10. If _unit_ is *"year"*, then
+ ...
+ z. Let _fractionalYears_ be _years_ + _fractionalDays_ / abs(_oneYearDays_).
+ ...
+ 11. If _unit_ is *"month"*, then
+ ...
+ z. Let _fractionalMonths_ be _months_ + _fractionalDays_ / abs(_oneMonthDays_).
+ ...
+ 12. If _unit_ is *"week"*, then
+ ...
+ s. Let _fractionalWeeks_ be _weeks_ + _fractionalDays_ / abs(_oneWeekDays_).
+includes: [compareArray.js]
+features: [Temporal]
+---*/
+
+// Return the next Number value in direction to +Infinity.
+function nextUp(num) {
+ if (!Number.isFinite(num)) {
+ return num;
+ }
+ if (num === 0) {
+ return Number.MIN_VALUE;
+ }
+
+ var f64 = new Float64Array([num]);
+ var u64 = new BigUint64Array(f64.buffer);
+ u64[0] += (num < 0 ? -1n : 1n);
+ return f64[0];
+}
+
+// Return the next Number value in direction to -Infinity.
+function nextDown(num) {
+ if (!Number.isFinite(num)) {
+ return num;
+ }
+ if (num === 0) {
+ return -Number.MIN_VALUE;
+ }
+
+ var f64 = new Float64Array([num]);
+ var u64 = new BigUint64Array(f64.buffer);
+ u64[0] += (num < 0 ? 1n : -1n);
+ return f64[0];
+}
+
+// Return bit pattern representation of Number as a Uint8Array of bytes.
+function f64Repr(f) {
+ const buf = new ArrayBuffer(8);
+ new DataView(buf).setFloat64(0, f);
+ return new Uint8Array(buf);
+}
+
+// ============
+
+const tz = new (class extends Temporal.TimeZone {
+ getPossibleInstantsFor() {
+ // Called in NormalizedTimeDurationToDays 21.a from RoundDuration 7.b.
+ // Sets _result_.[[DayLength]] to 2⁵³ - 1 ns, its largest possible value
+ return [new Temporal.Instant(-86400_0000_0000_000_000_000n + 2n ** 53n - 1n)];
+ }
+})("UTC");
+
+const cal = new (class extends Temporal.Calendar {
+ dateAdd() {
+ // Called in MoveRelativeDate from RoundDuration 10.x, 11.x, or 12.q.
+ // Sets _oneYearDays_, _oneMonthDays_, or _oneWeekDays_ respectively to
+ // 200_000_000, its largest possible value.
+ return new Temporal.PlainDate(275760, 9, 13);
+ }
+})("iso8601");
+
+const relativeTo = new Temporal.ZonedDateTime(-86400_0000_0000_000_000_000n, tz, cal);
+const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, /* nanoseconds = */ 1);
+
+/*
+ * RoundDuration step 7:
+ * ii. result = { [[Days]] = 0, [[Remainder]] = normalized time duration of 1 ns,
+ * [[DayLength]] = Number.MAX_SAFE_INTEGER }
+ * iii. fractionalDays = 0 + 0 + 1 / Number.MAX_SAFE_INTEGER
+ * step 10:
+ * y. oneYearDays = 200_000_000
+ * z. fractionalYears = 0 + (1 / Number.MAX_SAFE_INTEGER) / 200_000_000
+ */
+// Calculated with Python's Decimal module to 50 decimal places
+const expected = 5.55111512312578_3318415740544369642963189519987393e-25;
+
+// Check that we are not accidentally losing precision in our expected value:
+
+assert.sameValue(expected, 5.55111512312578_373662e-25, "the float representation of the result is 5.55111512312578373662e-25");
+assert.compareArray(
+ f64Repr(expected),
+ [0x3a, 0xe5, 0x79, 0x8e, 0xe2, 0x30, 0x8c, 0x3b],
+ "the bit representation of the result is 0x3ae5798ee2308c3b"
+);
+// The next Number in direction -Infinity is less precise.
+assert.sameValue(nextDown(expected), 5.55111512312578_281826e-25, "the next Number in direction -Infinity is less precise");
+// The next Number in direction +Infinity is less precise.
+assert.sameValue(nextUp(expected), 5.55111512312578_465497e-25, "the next Number in direction +Infinity is less precise");
+
+assert.sameValue(d.total({ unit: "years", relativeTo }), expected, "Correct division by large number in years total");
+assert.sameValue(d.total({ unit: "months", relativeTo }), expected, "Correct division by large number in months total");
+assert.sameValue(d.total({ unit: "weeks", relativeTo }), expected, "Correct division by large number in weeks total");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-4.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-4.js
new file mode 100644
index 0000000000..74a77e669f
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-4.js
@@ -0,0 +1,155 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ RoundDuration computes in such a way as to avoid precision loss when the
+ computed day, week, month, and year lengths are very large numbers.
+info: |
+ RoundDuration:
+ ...
+ 7. If _unit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then
+ a. If _zonedRelativeTo_ is not *undefined*, then
+ ...
+ iii. Let _fractionalDays_ be _days_ + _result_.[[Days]] + DivideNormalizedTimeDuration(_result_.[[Remainder]], _result_.[[DayLength]]).
+ ...
+ 10. If _unit_ is *"year"*, then
+ ...
+ z. Let _fractionalYears_ be _years_ + _fractionalDays_ / abs(_oneYearDays_).
+ ...
+ 11. If _unit_ is *"month"*, then
+ ...
+ z. Let _fractionalMonths_ be _months_ + _fractionalDays_ / abs(_oneMonthDays_).
+ ...
+ 12. If _unit_ is *"week"*, then
+ ...
+ s. Let _fractionalWeeks_ be _weeks_ + _fractionalDays_ / abs(_oneWeekDays_).
+includes: [compareArray.js, temporalHelpers.js]
+features: [Temporal]
+---*/
+
+// Return the next Number value in direction to +Infinity.
+function nextUp(num) {
+ if (!Number.isFinite(num)) {
+ return num;
+ }
+ if (num === 0) {
+ return Number.MIN_VALUE;
+ }
+
+ var f64 = new Float64Array([num]);
+ var u64 = new BigUint64Array(f64.buffer);
+ u64[0] += (num < 0 ? -1n : 1n);
+ return f64[0];
+}
+
+// Return the next Number value in direction to -Infinity.
+function nextDown(num) {
+ if (!Number.isFinite(num)) {
+ return num;
+ }
+ if (num === 0) {
+ return -Number.MIN_VALUE;
+ }
+
+ var f64 = new Float64Array([num]);
+ var u64 = new BigUint64Array(f64.buffer);
+ u64[0] += (num < 0 ? 1n : -1n);
+ return f64[0];
+}
+
+// Return bit pattern representation of Number as a Uint8Array of bytes.
+function f64Repr(f) {
+ const buf = new ArrayBuffer(8);
+ new DataView(buf).setFloat64(0, f);
+ return new Uint8Array(buf);
+}
+
+// ============
+// Set up contrived custom time zone and calendar to give dayLengthNs,
+// oneYearDays, oneMonthDays, and oneWeekDays their largest possible values
+
+function createTimeZone() {
+ const tz = new Temporal.TimeZone("UTC");
+ TemporalHelpers.substituteMethod(tz, "getPossibleInstantsFor", [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.a MoveRelativeZonedDateTime → AddZonedDateTime
+ TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.e.ii AddDaysToZonedDateTime
+ TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.i.ii NormalizedTimeDurationToDays step 16
+ TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.i.ii NormalizedTimeDurationToDays step 19
+ [new Temporal.Instant(-86400_0000_0000_000_000_000n)], // RoundDuration step 7.a.i MoveRelativeZonedDateTime → AddZonedDateTime
+ [new Temporal.Instant(-86400_0000_0000_000_000_000n + 2n ** 53n - 1n)], // RoundDuration step 7.a.ii NormalizedTimeDurationToDays step 19
+ // sets dayLengthNs to Number.MAX_SAFE_INTEGER
+ ]);
+ return tz;
+}
+
+function createCalendar() {
+ const cal = new Temporal.Calendar("iso8601");
+ TemporalHelpers.substituteMethod(cal, "dateAdd", [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.a MoveRelativeZonedDateTime → AddZonedDateTime
+ TemporalHelpers.SUBSTITUTE_SKIP, // RoundDuration step 7.a.i MoveRelativeZonedDateTime → AddZonedDateTime
+ new Temporal.PlainDate(-271821, 4, 20), // RoundDuration step 10.d/11.d AddDate
+ new Temporal.PlainDate(-271821, 4, 20), // RoundDuration step 10.f/11.f AddDate
+ new Temporal.PlainDate(275760, 9, 13), // RoundDuration step 10.r/11.r MoveRelativeDate
+ // sets one{Year,Month,Week}Days to 200_000_000
+ ]);
+ return cal;
+}
+
+// ============
+
+// We will calculate the total years/months/weeks of durations with:
+// 1 year/month/week, 1 day, 0.199000001 s
+
+// RoundDuration step 7:
+// ii. result = { [[Days]] = 1, [[Remainder]] = normalized time duration of 199_000_001 ns,
+// [[DayLength]] = Number.MAX_SAFE_INTEGER }
+// iii. fractionalDays = 1 + 0 + 199_000_001 / Number.MAX_SAFE_INTEGER
+// step 10: (similar for steps 11 and 12 in the case of months/weeks)
+// y. oneYearDays = 200_000_000
+// z. fractionalYears = 1 + (1 + 199_000_001 / Number.MAX_SAFE_INTEGER) / 200_000_000
+//
+// Note: if calculated as 1 + 1/200_000_000 + 199_000_001 / Number.MAX_SAFE_INTEGER / 200_000_000
+// this will lose precision and give the result 1.000000005.
+
+// Calculated with Python's Decimal module to 50 decimal places
+const expected = 1.000000005000000_1104671915053146003490515686745299;
+
+// Check that we are not accidentally losing precision in our expected value:
+
+assert.sameValue(expected, 1.000000005000000_2, "the float representation of the result is 1.0000000050000002");
+assert.compareArray(
+ f64Repr(expected),
+ [0x3f, 0xf0, 0x00, 0x00, 0x01, 0x57, 0x98, 0xef],
+ "the bit representation of the result is 0x3ff00000015798ef"
+);
+// The next Number in direction -Infinity is less precise.
+assert.sameValue(nextDown(expected), 1.000000004999999_96961, "the next Number in direction -Infinity is less precise");
+// The next Number in direction +Infinity is less precise.
+assert.sameValue(nextUp(expected), 1.000000005000000_4137, "the next Number in direction +Infinity is less precise");
+
+// ============
+
+let relativeTo = new Temporal.ZonedDateTime(-86400_0000_0000_000_000_000n, createTimeZone(), createCalendar());
+const dYears = new Temporal.Duration(/* years = */ 1, 0, 0, /* days = */ 1, 0, 0, 0, /* milliseconds = */ 199, 0, /* nanoseconds = */ 1);
+assert.sameValue(dYears.total({ unit: "years", relativeTo }), expected, "Correct division by large number in years total");
+
+relativeTo = new Temporal.ZonedDateTime(-86400_0000_0000_000_000_000n, createTimeZone(), createCalendar());
+const dMonths = new Temporal.Duration(0, /* months = */ 1, 0, /* days = */ 1, 0, 0, 0, /* milliseconds = */ 199, 0, /* nanoseconds = */ 1);
+assert.sameValue(dMonths.total({ unit: "months", relativeTo }), expected, "Correct division by large number in months total");
+
+// Weeks calculation doesn't have the AddDate calls to convert months/weeks to days
+const weeksCal = new Temporal.Calendar("iso8601");
+TemporalHelpers.substituteMethod(weeksCal, "dateAdd", [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.a MoveRelativeZonedDateTime → AddZonedDateTime
+ TemporalHelpers.SUBSTITUTE_SKIP, // RoundDuration step 7.a.i MoveRelativeZonedDateTime → AddZonedDateTime
+ new Temporal.PlainDate(275760, 9, 13), // RoundDuration step 12.q MoveRelativeDate
+ // sets one{Year,Month,Week}Days to 200_000_000
+]);
+relativeTo = new Temporal.ZonedDateTime(-86400_0000_0000_000_000_000n, createTimeZone(), weeksCal);
+const dWeeks = new Temporal.Duration(0, 0, /* weejs = */ 1, /* days = */ 1, 0, 0, 0, /* milliseconds = */ 199, 0, /* nanoseconds = */ 1);
+assert.sameValue(dWeeks.total({ unit: "weeks", relativeTo }), expected, "Correct division by large number in weeks total");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-5.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-5.js
new file mode 100644
index 0000000000..9d315db9c2
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-5.js
@@ -0,0 +1,61 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: BalanceTimeDuration computes on exact mathematical values.
+features: [BigInt, Temporal]
+---*/
+
+const seconds = 8692288669465520;
+
+{
+ const milliseconds = 513;
+ const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds, milliseconds);
+
+ const result = d.total({ unit: "milliseconds" });
+
+ // The result should be the nearest Number value to 8692288669465520512
+ const expectedMilliseconds = Number(BigInt(seconds) * 1000n + BigInt(milliseconds));
+ assert.sameValue(expectedMilliseconds, 8692288669465520_513, "check expected value (ms)");
+
+ assert.sameValue(
+ result, expectedMilliseconds,
+ "BalanceTimeDuration should implement floating-point calculation correctly for largestUnit milliseconds"
+ );
+}
+
+{
+ const microseconds = 373761;
+ const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds, 0, microseconds);
+
+ const result = d.total({ unit: "microseconds" });
+
+ // The result should be the nearest Number value to 8692288669465520373761
+ const expectedMicroseconds = Number(BigInt(seconds) * 1_000_000n + BigInt(microseconds));
+ assert.sameValue(expectedMicroseconds, 8692288669465520_373_761, "check expected value (µs)");
+
+ assert.sameValue(
+ result, expectedMicroseconds,
+ "BalanceTimeDuration should implement floating-point calculation correctly for largestUnit milliseconds"
+ );
+}
+
+{
+ const nanoseconds = 321_414_345;
+ const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds, 0, 0, nanoseconds);
+
+ const result = d.total({ unit: "nanoseconds" });
+
+ // The result should be the nearest Number value to 8692288669465520321414345
+ const expectedNanoseconds = Number(BigInt(seconds) * 1_000_000_000n + BigInt(nanoseconds));
+ assert.sameValue(expectedNanoseconds, 8692288669465520_321_414_345, "check expected value (ns)");
+
+ assert.sameValue(
+ result, expectedNanoseconds,
+ "BalanceTimeDuration should implement floating-point calculation correctly for largestUnit nanoseconds"
+ );
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js
new file mode 100644
index 0000000000..8072451c5f
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js
@@ -0,0 +1,143 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 André Bargull. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ DivideNormalizedTimeDuration computes on exact mathematical values.
+info: |
+ Temporal.Duration.prototype.total ( totalOf )
+
+ ...
+ 20. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]],
+ unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], days, norm, 1,
+ unit, "trunc", plainRelativeTo, calendarRec, zonedRelativeTo, timeZoneRec,
+ precalculatedPlainDateTime).
+ 21. Return 𝔽(roundRecord.[[Total]]).
+
+ RoundDuration ( ... )
+
+ ...
+ 14. Else if unit is "hour", then
+ a. Let divisor be 3.6 × 10^12.
+ b. Set total to DivideNormalizedTimeDuration(norm, divisor).
+ ...
+
+ DivideNormalizedTimeDuration ( d, divisor )
+
+ 1. Assert: divisor ≠ 0.
+ 2. Return d.[[TotalNanoseconds]] / divisor.
+features: [Temporal]
+---*/
+
+// Randomly generated test data.
+const data = [
+ {
+ hours: 816,
+ nanoseconds: 2049_187_497_660,
+ },
+ {
+ hours: 7825,
+ nanoseconds: 1865_665_040_770,
+ },
+ {
+ hours: 0,
+ nanoseconds: 1049_560_584_034,
+ },
+ {
+ hours: 2055144,
+ nanoseconds: 2502_078_444_371,
+ },
+ {
+ hours: 31,
+ nanoseconds: 1010_734_758_745,
+ },
+ {
+ hours: 24,
+ nanoseconds: 2958_999_560_387,
+ },
+ {
+ hours: 0,
+ nanoseconds: 342_058_521_588,
+ },
+ {
+ hours: 17746,
+ nanoseconds: 3009_093_506_309,
+ },
+ {
+ hours: 4,
+ nanoseconds: 892_480_914_569,
+ },
+ {
+ hours: 3954,
+ nanoseconds: 571_647_777_618,
+ },
+ {
+ hours: 27,
+ nanoseconds: 2322_199_502_640,
+ },
+ {
+ hours: 258054064,
+ nanoseconds: 2782_411_891_222,
+ },
+ {
+ hours: 1485,
+ nanoseconds: 2422_559_903_100,
+ },
+ {
+ hours: 0,
+ nanoseconds: 1461_068_214_153,
+ },
+ {
+ hours: 393,
+ nanoseconds: 1250_229_561_658,
+ },
+ {
+ hours: 0,
+ nanoseconds: 91_035_820,
+ },
+ {
+ hours: 0,
+ nanoseconds: 790_982_655,
+ },
+ {
+ hours: 150,
+ nanoseconds: 608_531_524,
+ },
+ {
+ hours: 5469,
+ nanoseconds: 889_204_952,
+ },
+ {
+ hours: 7870,
+ nanoseconds: 680_042_770,
+ },
+];
+
+const nsPerHour = 3600_000_000_000;
+
+const fractionDigits = Math.log10(nsPerHour) + Math.log10(100_000_000_000) - Math.log10(36);
+assert.sameValue(fractionDigits, 22);
+
+for (let {hours, nanoseconds} of data) {
+ assert(nanoseconds < nsPerHour);
+
+ // Compute enough fractional digits to approximate the exact result. Use BigInts
+ // to avoid floating point precision loss. Fill to the left with implicit zeros.
+ let fraction = ((BigInt(nanoseconds) * 100_000_000_000n) / 36n).toString().padStart(fractionDigits, "0");
+
+ // Get the Number approximation from the string representation.
+ let expected = Number(`${hours}.${fraction}`);
+
+ let d = Temporal.Duration.from({hours, nanoseconds});
+ let actual = d.total("hours");
+
+ assert.sameValue(
+ actual,
+ expected,
+ `hours=${hours}, nanoseconds=${nanoseconds}`,
+ );
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js
new file mode 100644
index 0000000000..6096f1afac
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js
@@ -0,0 +1,122 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 André Bargull. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ DivideNormalizedTimeDuration computes on exact mathematical values.
+info: |
+ Temporal.Duration.prototype.total ( totalOf )
+
+ ...
+ 20. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]],
+ unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], days, norm, 1,
+ unit, "trunc", plainRelativeTo, calendarRec, zonedRelativeTo, timeZoneRec,
+ precalculatedPlainDateTime).
+ 21. Return 𝔽(roundRecord.[[Total]]).
+
+ RoundDuration ( ... )
+
+ ...
+ 16. Else if unit is "second", then
+ a. Let divisor be 10^9.
+ b. Set total to DivideNormalizedTimeDuration(norm, divisor).
+ ...
+ 17. Else if unit is "millisecond", then
+ a. Let divisor be 10^6.
+ b. Set total to DivideNormalizedTimeDuration(norm, divisor).
+ ...
+ 18. Else if unit is "microsecond", then
+ a. Let divisor be 10^3.
+ b. Set total to DivideNormalizedTimeDuration(norm, divisor).
+ ...
+
+ DivideNormalizedTimeDuration ( d, divisor )
+
+ 1. Assert: divisor ≠ 0.
+ 2. Return d.[[TotalNanoseconds]] / divisor.
+features: [Temporal]
+---*/
+
+// Test duration units where the fractional part is a power of ten.
+const units = [
+ "seconds", "milliseconds", "microseconds", "nanoseconds",
+];
+
+// Conversion factors to nanoseconds precision.
+const toNanos = {
+ "seconds": 1_000_000_000n,
+ "milliseconds": 1_000_000n,
+ "microseconds": 1_000n,
+ "nanoseconds": 1n,
+};
+
+const integers = [
+ // Small integers.
+ 0,
+ 1,
+ 2,
+
+ // Large integers around Number.MAX_SAFE_INTEGER.
+ 2**51,
+ 2**52,
+ 2**53,
+ 2**54,
+];
+
+const fractions = [
+ // True fractions.
+ 0, 1, 10, 100, 125, 200, 250, 500, 750, 800, 900, 950, 999,
+
+ // Fractions with overflow.
+ 1_000,
+ 1_999,
+ 2_000,
+ 2_999,
+ 3_000,
+ 3_999,
+ 4_000,
+ 4_999,
+
+ 999_999,
+ 1_000_000,
+ 1_000_001,
+
+ 999_999_999,
+ 1_000_000_000,
+ 1_000_000_001,
+];
+
+const maxTimeDuration = (2n ** 53n) * (10n ** 9n) - 1n;
+
+// Iterate over all units except the last one.
+for (let unit of units.slice(0, -1)) {
+ let smallerUnit = units[units.indexOf(unit) + 1];
+
+ for (let integer of integers) {
+ for (let fraction of fractions) {
+ // Total nanoseconds must not exceed |maxTimeDuration|.
+ let totalNanoseconds = BigInt(integer) * toNanos[unit] + BigInt(fraction) * toNanos[smallerUnit];
+ if (totalNanoseconds > maxTimeDuration) {
+ continue;
+ }
+
+ // Get the Number approximation from the string representation.
+ let i = BigInt(integer) + BigInt(fraction) / 1000n;
+ let f = String(fraction % 1000).padStart(3, "0");
+ let expected = Number(`${i}.${f}`);
+
+ let d = Temporal.Duration.from({[unit]: integer, [smallerUnit]: fraction});
+ let actual = d.total(unit);
+
+ assert.sameValue(
+ actual,
+ expected,
+ `${unit}=${integer}, ${smallerUnit}=${fraction}`,
+ );
+ }
+ }
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/prop-desc.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/prop-desc.js
new file mode 100644
index 0000000000..8374803e9f
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/prop-desc.js
@@ -0,0 +1,24 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: The "total" property of Temporal.Duration.prototype
+includes: [propertyHelper.js]
+features: [Temporal]
+---*/
+
+assert.sameValue(
+ typeof Temporal.Duration.prototype.total,
+ "function",
+ "`typeof Duration.prototype.total` is `function`"
+);
+
+verifyProperty(Temporal.Duration.prototype, "total", {
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/proto-in-calendar-fields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/proto-in-calendar-fields.js
new file mode 100644
index 0000000000..b40b2d4b30
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/proto-in-calendar-fields.js
@@ -0,0 +1,17 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.duration.prototype.total
+description: If a calendar's fields() method returns a field named '__proto__', PrepareTemporalFields should throw a RangeError.
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const calendar = TemporalHelpers.calendarWithExtraFields(['__proto__']);
+const relativeTo = { year: 2023, month: 5, monthCode: 'M05', day: 1, calendar: calendar, timeZone: 'Europe/Paris' };
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }));
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/read-time-fields-before-datefromfields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/read-time-fields-before-datefromfields.js
new file mode 100644
index 0000000000..0e80cd0ef5
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/read-time-fields-before-datefromfields.js
@@ -0,0 +1,24 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: The time fields are read from the object before being passed to dateFromFields().
+info: |
+ sec-temporal.duration.prototype.total step 4:
+ 4. Let _relativeTo_ be ? ToRelativeTemporalObject(_options_).
+ sec-temporal-torelativetemporalobject step 4.g:
+ g. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _options_).
+ sec-temporal-interprettemporaldatetimefields steps 1–2:
+ 1. Let _timeResult_ be ? ToTemporalTimeRecord(_fields_).
+ 2. Let _temporalDate_ be ? DateFromFields(_calendar_, _fields_, _options_).
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const calendar = TemporalHelpers.calendarMakeInvalidGettersTime();
+const duration = new Temporal.Duration(1, 1, 1, 1, 1, 1, 1);
+duration.total({ unit: 'seconds', relativeTo: { year: 2000, month: 1, day: 1, calendar } });
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-infinity-throws-rangeerror.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-infinity-throws-rangeerror.js
new file mode 100644
index 0000000000..9567656f64
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-infinity-throws-rangeerror.js
@@ -0,0 +1,26 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+description: Throws if any value in the property bag is Infinity or -Infinity
+esid: sec-temporal.duration.prototype.total
+includes: [compareArray.js, temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+const base = { year: 2000, month: 5, day: 2, hour: 15, minute: 30, second: 45, millisecond: 987, microsecond: 654, nanosecond: 321 };
+
+[Infinity, -Infinity].forEach((inf) => {
+ ["year", "month", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"].forEach((prop) => {
+ assert.throws(RangeError, () => instance.total({ unit: "seconds", relativeTo: { ...base, [prop]: inf } }), `${prop} property cannot be ${inf} in relativeTo`);
+
+ const calls = [];
+ const obj = TemporalHelpers.toPrimitiveObserver(calls, inf, prop);
+ assert.throws(RangeError, () => instance.total({ unit: "seconds", relativeTo: { ...base, [prop]: obj } }));
+ assert.compareArray(calls, [`get ${prop}.valueOf`, `call ${prop}.valueOf`], "it fails after fetching the primitive value");
+ });
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-leap-second.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-leap-second.js
new file mode 100644
index 0000000000..c69a1dd35e
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-leap-second.js
@@ -0,0 +1,45 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Leap second is constrained in both an ISO string and a property bag
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+let relativeTo = "2016-12-31T23:59:60";
+const result1 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(
+ result1,
+ 366,
+ "leap second is a valid ISO string for PlainDate relativeTo"
+);
+
+relativeTo = "2016-12-31T23:59:60+00:00[UTC]";
+const result2 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(
+ result2,
+ 366,
+ "leap second is a valid ISO string for ZonedDateTime relativeTo"
+);
+
+relativeTo = { year: 2016, month: 12, day: 31, hour: 23, minute: 59, second: 60 };
+const result3 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(
+ result3,
+ 366,
+ "second: 60 is valid in a property bag for PlainDate relativeTo"
+);
+
+relativeTo = { year: 2016, month: 12, day: 31, hour: 23, minute: 59, second: 60, timeZone: "UTC" };
+const result4 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(
+ result4,
+ 366,
+ "second: 60 is valid in a property bag for ZonedDateTime relativeTo"
+);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-number.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-number.js
new file mode 100644
index 0000000000..765099da48
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-number.js
@@ -0,0 +1,28 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: A number cannot be used in place of a relativeTo
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+const numbers = [
+ 1,
+ 20191101,
+ -20191101,
+ 1234567890,
+];
+
+for (const relativeTo of numbers) {
+ assert.throws(
+ TypeError,
+ () => instance.total({ unit: "days", relativeTo }),
+ `A number (${relativeTo}) is not a valid ISO string for relativeTo`
+ );
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-plaindate-add24hourdaystonormalizedtimeduration-out-of-range.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-plaindate-add24hourdaystonormalizedtimeduration-out-of-range.js
new file mode 100644
index 0000000000..d1bdde5edf
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-plaindate-add24hourdaystonormalizedtimeduration-out-of-range.js
@@ -0,0 +1,19 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.compare
+description: RangeError thrown if adding the duration to the relativeTo date would result in anout-of-range date-time
+features: [Temporal]
+---*/
+
+let duration = Temporal.Duration.from({
+ years: 1,
+ seconds: 2**53 - 1,
+});
+let relativeTo = new Temporal.PlainDate(2000, 1, 1);
+
+assert.throws(RangeError, () => duration.total({ relativeTo, unit: "days" }));
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-ambiguous-wall-clock-time.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-ambiguous-wall-clock-time.js
new file mode 100644
index 0000000000..965d3e8813
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-ambiguous-wall-clock-time.js
@@ -0,0 +1,92 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Correct time zone calls are made when converting a ZonedDateTime-like
+ relativeTo property bag denoting an ambiguous wall-clock time
+includes: [temporalHelpers.js, compareArray.js]
+features: [Temporal]
+---*/
+
+const actual = [];
+
+const dstTimeZone = TemporalHelpers.springForwardFallBackTimeZone();
+const dstTimeZoneObserver = TemporalHelpers.timeZoneObserver(actual, "timeZone", {
+ getOffsetNanosecondsFor: dstTimeZone.getOffsetNanosecondsFor.bind(dstTimeZone),
+ getPossibleInstantsFor: dstTimeZone.getPossibleInstantsFor.bind(dstTimeZone),
+});
+const calendar = TemporalHelpers.calendarObserver(actual, "calendar");
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+let relativeTo = { year: 2000, month: 4, day: 2, hour: 2, minute: 30, timeZone: dstTimeZoneObserver, calendar };
+instance.total({ unit: "days", relativeTo });
+
+const expected = [
+ // GetTemporalCalendarSlotValueWithISODefault
+ "has calendar.dateAdd",
+ "has calendar.dateFromFields",
+ "has calendar.dateUntil",
+ "has calendar.day",
+ "has calendar.dayOfWeek",
+ "has calendar.dayOfYear",
+ "has calendar.daysInMonth",
+ "has calendar.daysInWeek",
+ "has calendar.daysInYear",
+ "has calendar.fields",
+ "has calendar.id",
+ "has calendar.inLeapYear",
+ "has calendar.mergeFields",
+ "has calendar.month",
+ "has calendar.monthCode",
+ "has calendar.monthDayFromFields",
+ "has calendar.monthsInYear",
+ "has calendar.weekOfYear",
+ "has calendar.year",
+ "has calendar.yearMonthFromFields",
+ "has calendar.yearOfWeek",
+ // lookup
+ "get calendar.dateFromFields",
+ "get calendar.fields",
+ // CalendarFields
+ "call calendar.fields",
+ // InterpretTemporalDateTimeFields
+ "call calendar.dateFromFields",
+ // ToTemporalTimeZoneSlotValue
+ "has timeZone.getOffsetNanosecondsFor",
+ "has timeZone.getPossibleInstantsFor",
+ "has timeZone.id",
+ // lookup
+ "get timeZone.getOffsetNanosecondsFor",
+ "get timeZone.getPossibleInstantsFor",
+ // InterpretISODateTimeOffset
+ "call timeZone.getPossibleInstantsFor",
+];
+
+const expectedSpringForward = expected.concat([
+ // DisambiguatePossibleInstants
+ "call timeZone.getOffsetNanosecondsFor",
+ "call timeZone.getOffsetNanosecondsFor",
+ "call timeZone.getPossibleInstantsFor",
+]);
+assert.compareArray(
+ actual.slice(0, expectedSpringForward.length), // ignore operations after ToRelativeTemporalObject
+ expectedSpringForward,
+ "order of operations converting property bag at skipped wall-clock time"
+);
+actual.splice(0); // clear
+
+relativeTo = { year: 2000, month: 10, day: 29, hour: 1, minute: 30, timeZone: dstTimeZoneObserver, calendar };
+instance.total({ unit: "days", relativeTo });
+
+assert.compareArray(
+ actual.slice(0, expected.length), // ignore operations after ToRelativeTemporalObject
+ expected,
+ "order of operations converting property bag at repeated wall-clock time"
+);
+actual.splice(0); // clear
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-builtin-calendar-no-array-iteration.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-builtin-calendar-no-array-iteration.js
new file mode 100644
index 0000000000..44c97b82e9
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-builtin-calendar-no-array-iteration.js
@@ -0,0 +1,25 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Calling the method with a relativeTo property bag with a builtin calendar
+ causes no observable array iteration when getting the calendar fields.
+features: [Temporal]
+---*/
+
+const arrayPrototypeSymbolIteratorOriginal = Array.prototype[Symbol.iterator];
+Array.prototype[Symbol.iterator] = function arrayIterator() {
+ throw new Test262Error("Array should not be iterated");
+}
+
+const timeZone = "UTC";
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+const relativeTo = { year: 2000, month: 5, day: 2, hour: 21, minute: 43, second: 5, timeZone, calendar: "iso8601" };
+instance.total({ unit: "days", relativeTo });
+
+Array.prototype[Symbol.iterator] = arrayPrototypeSymbolIteratorOriginal;
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-datefromfields-called-with-null-prototype-fields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-datefromfields-called-with-null-prototype-fields.js
new file mode 100644
index 0000000000..d8a4d09f9d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-datefromfields-called-with-null-prototype-fields.js
@@ -0,0 +1,19 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Calendar.dateFromFields method is called with a null-prototype fields object
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const calendar = TemporalHelpers.calendarCheckFieldsPrototypePollution();
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+const relativeTo = { year: 2000, month: 5, day: 2, calendar };
+instance.total({ unit: "days", relativeTo });
+assert.sameValue(calendar.dateFromFieldsCallCount, 1, "dateFromFields should be called on the property bag's calendar");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-number.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-number.js
new file mode 100644
index 0000000000..52a4647bcf
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-number.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: A number as calendar in relativeTo property bag is invalid
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+const numbers = [
+ 1,
+ 19970327,
+ -19970327,
+ 1234567890,
+];
+
+for (const calendar of numbers) {
+ const relativeTo = { year: 2019, monthCode: "M11", day: 1, calendar };
+ assert.throws(
+ TypeError,
+ () => instance.total({ unit: "days", relativeTo }),
+ `A number (${calendar}) is not a valid ISO string for relativeTo.calendar`
+ );
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-string.js
new file mode 100644
index 0000000000..8cba83ba56
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-string.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Builtin dateFromFields method is not observably called when the property bag
+ has a string-valued calendar property
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const dateFromFieldsOriginal = Object.getOwnPropertyDescriptor(Temporal.Calendar.prototype, "dateFromFields");
+Object.defineProperty(Temporal.Calendar.prototype, "dateFromFields", {
+ configurable: true,
+ enumerable: false,
+ get() {
+ TemporalHelpers.assertUnreachable("dateFromFields should not be looked up");
+ },
+});
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+const relativeTo = { year: 2000, month: 5, day: 2, calendar: "iso8601" };
+instance.total({ unit: "days", relativeTo });
+
+Object.defineProperty(Temporal.Calendar.prototype, "dateFromFields", dateFromFieldsOriginal);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-wrong-type.js
new file mode 100644
index 0000000000..cef7d31b45
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-wrong-type.js
@@ -0,0 +1,48 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Appropriate error thrown when relativeTo.calendar cannot be converted to a
+ calendar object or string
+features: [BigInt, Symbol, Temporal]
+---*/
+
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+const primitiveTests = [
+ [null, "null"],
+ [true, "boolean"],
+ ["", "empty string"],
+ [1, "number that doesn't convert to a valid ISO string"],
+ [1n, "bigint"],
+];
+
+for (const [calendar, description] of primitiveTests) {
+ const relativeTo = { year: 2019, monthCode: "M11", day: 1, calendar };
+ assert.throws(
+ typeof calendar === 'string' ? RangeError : TypeError,
+ () => instance.total({ unit: "days", relativeTo }),
+ `${description} does not convert to a valid ISO string`
+ );
+}
+
+const typeErrorTests = [
+ [Symbol(), "symbol"],
+ [{}, "plain object that doesn't implement the protocol"],
+ [new Temporal.TimeZone("UTC"), "time zone instance"],
+ [Temporal.PlainDate, "Temporal.PlainDate, object"],
+ [Temporal.PlainDate.prototype, "Temporal.PlainDate.prototype, object"],
+ [Temporal.ZonedDateTime, "Temporal.ZonedDateTime, object"],
+ [Temporal.ZonedDateTime.prototype, "Temporal.ZonedDateTime.prototype, object"],
+];
+
+for (const [calendar, description] of typeErrorTests) {
+ const relativeTo = { year: 2019, monthCode: "M11", day: 1, calendar };
+ assert.throws(TypeError, () => instance.total({ unit: "days", relativeTo }), `${description} is not a valid property bag and does not convert to a string`);
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-getpossibleinstantsfor-called-with-iso8601-calendar.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-getpossibleinstantsfor-called-with-iso8601-calendar.js
new file mode 100644
index 0000000000..16ab8873ec
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-getpossibleinstantsfor-called-with-iso8601-calendar.js
@@ -0,0 +1,59 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Time zone's getPossibleInstantsFor is called with a PlainDateTime with the
+ built-in ISO 8601 calendar
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 2. Let _n_ be _possibleInstants_'s length.
+ ...
+ 5. Assert: _n_ = 0.
+ ...
+ 19. If _disambiguation_ is *"earlier"*, then
+ ...
+ c. Let _earlierDateTime_ be ! CreateTemporalDateTime(..., *"iso8601"*).
+ d. Set _possibleInstants_ to ? GetPossibleInstantsFor(_timeZone_, _earlierDateTime_).
+ ...
+ 20. Assert: _disambiguation_ is *"compatible"* or *"later"*.
+ ...
+ 23. Let _laterDateTime_ be ! CreateTemporalDateTime(..., *"iso8601"*).
+ 24. Set _possibleInstants_ to ? GetPossibleInstantsFor(_timeZone_, _laterDateTime_).
+---*/
+
+class SkippedDateTime extends Temporal.TimeZone {
+ constructor() {
+ super("UTC");
+ this.calls = 0;
+ }
+
+ getPossibleInstantsFor(dateTime) {
+ // Calls occur in pairs. For the first one return no possible instants so
+ // that DisambiguatePossibleInstants will call it again
+ if (this.calls++ % 2 == 0) {
+ return [];
+ }
+
+ assert.sameValue(
+ dateTime.getISOFields().calendar,
+ "iso8601",
+ "getPossibleInstantsFor called with dateTime with built-in ISO 8601 calendar"
+ );
+ return super.getPossibleInstantsFor(dateTime);
+ }
+}
+
+const nonBuiltinISOCalendar = new Temporal.Calendar("iso8601");
+const timeZone = new SkippedDateTime();
+const relativeTo = { year: 2000, month: 5, day: 2, timeZone, calendar: nonBuiltinISOCalendar };
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+instance.total({ unit: "days", relativeTo });
+
+assert.sameValue(timeZone.calls, 10, "getPossibleInstantsFor should have been called 10 times");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-invalid-offset-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-invalid-offset-string.js
new file mode 100644
index 0000000000..dcb4944855
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-invalid-offset-string.js
@@ -0,0 +1,32 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: relativeTo property bag with offset property is rejected if offset is in the wrong format
+features: [Temporal]
+---*/
+
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+const badOffsets = [
+ "00:00", // missing sign
+ "+0", // too short
+ "-000:00", // too long
+ 0, // must be a string
+ null, // must be a string
+ true, // must be a string
+ 1000n, // must be a string
+];
+badOffsets.forEach((offset) => {
+ const relativeTo = { year: 2021, month: 10, day: 28, offset, timeZone };
+ assert.throws(
+ typeof(offset) === 'string' ? RangeError : TypeError,
+ () => instance.total({ unit: "days", relativeTo }),
+ `"${offset} is not a valid offset string`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-no-time-units.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-no-time-units.js
new file mode 100644
index 0000000000..dc9d54ed5d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-no-time-units.js
@@ -0,0 +1,17 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Missing time units in relativeTo property bag default to 0
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+let relativeTo = { year: 2000, month: 1, day: 1 };
+const result = instance.total({ unit: "days", relativeTo });
+assert.sameValue(result, 367, "missing time units default to 0");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-non-integer.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-non-integer.js
new file mode 100644
index 0000000000..d5200d49b8
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-non-integer.js
@@ -0,0 +1,18 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: RangeError thrown if time zone reports an offset that is not an integer number of nanoseconds
+features: [Temporal]
+includes: [temporalHelpers.js]
+---*/
+
+[3600_000_000_000.5, NaN, -Infinity, Infinity].forEach((wrongOffset) => {
+ const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset);
+ const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+ assert.throws(RangeError, () => duration.total({ unit: "seconds", relativeTo: { year: 2000, month: 5, day: 2, hour: 12, timeZone } }));
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-not-callable.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-not-callable.js
new file mode 100644
index 0000000000..4929986c8c
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-not-callable.js
@@ -0,0 +1,22 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: TypeError thrown if timeZone.getOffsetNanosecondsFor is not callable
+features: [BigInt, Symbol, Temporal, arrow-function]
+---*/
+
+[undefined, null, true, Math.PI, 'string', Symbol('sym'), 42n, {}].forEach((notCallable) => {
+ const timeZone = new Temporal.TimeZone("UTC");
+ const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+ timeZone.getOffsetNanosecondsFor = notCallable;
+ assert.throws(
+ TypeError,
+ () => duration.total({ unit: "seconds", relativeTo: { year: 2000, month: 5, day: 2, hour: 12, timeZone } }),
+ `Uncallable ${notCallable === null ? 'null' : typeof notCallable} getOffsetNanosecondsFor should throw TypeError`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-out-of-range.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-out-of-range.js
new file mode 100644
index 0000000000..332fa1bec5
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-out-of-range.js
@@ -0,0 +1,18 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: RangeError thrown if time zone reports an offset that is out of range
+features: [Temporal]
+includes: [temporalHelpers.js]
+---*/
+
+[-86400_000_000_000, 86400_000_000_000].forEach((wrongOffset) => {
+ const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset);
+ const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+ assert.throws(RangeError, () => duration.total({ unit: "seconds", relativeTo: { year: 2000, month: 5, day: 2, hour: 12, timeZone } }));
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-wrong-type.js
new file mode 100644
index 0000000000..323e64cc8f
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-wrong-type.js
@@ -0,0 +1,27 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: TypeError thrown if time zone reports an offset that is not a Number
+features: [Temporal]
+includes: [temporalHelpers.js]
+---*/
+
+[
+ undefined,
+ null,
+ true,
+ "+01:00",
+ Symbol(),
+ 3600_000_000_000n,
+ {},
+ { valueOf() { return 3600_000_000_000; } },
+].forEach((wrongOffset) => {
+ const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset);
+ const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+ assert.throws(TypeError, () => duration.total({ unit: "seconds", relativeTo: { year: 2000, month: 5, day: 2, hour: 12, timeZone } }));
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-datetime.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-datetime.js
new file mode 100644
index 0000000000..d5fa6c32ef
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-datetime.js
@@ -0,0 +1,66 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Conversion of ISO date-time strings to Temporal.TimeZone instances
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1);
+
+let timeZone = "2021-08-19T17:30";
+assert.throws(RangeError, () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), "bare date-time string is not a time zone");
+
+[
+ "2021-08-19T17:30-07:00:01",
+ "2021-08-19T17:30-07:00:00",
+ "2021-08-19T17:30-07:00:00.1",
+ "2021-08-19T17:30-07:00:00.0",
+ "2021-08-19T17:30-07:00:00.01",
+ "2021-08-19T17:30-07:00:00.00",
+ "2021-08-19T17:30-07:00:00.001",
+ "2021-08-19T17:30-07:00:00.000",
+ "2021-08-19T17:30-07:00:00.0001",
+ "2021-08-19T17:30-07:00:00.0000",
+ "2021-08-19T17:30-07:00:00.00001",
+ "2021-08-19T17:30-07:00:00.00000",
+ "2021-08-19T17:30-07:00:00.000001",
+ "2021-08-19T17:30-07:00:00.000000",
+ "2021-08-19T17:30-07:00:00.0000001",
+ "2021-08-19T17:30-07:00:00.0000000",
+ "2021-08-19T17:30-07:00:00.00000001",
+ "2021-08-19T17:30-07:00:00.00000000",
+ "2021-08-19T17:30-07:00:00.000000001",
+ "2021-08-19T17:30-07:00:00.000000000",
+].forEach((timeZone) => {
+ assert.throws(
+ RangeError,
+ () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }),
+ `ISO string ${timeZone} with a sub-minute offset is not a valid time zone`
+ );
+});
+
+// The following are all valid strings so should not throw:
+
+[
+ "2021-08-19T17:30Z",
+ "2021-08-19T1730Z",
+ "2021-08-19T17:30-07:00",
+ "2021-08-19T1730-07:00",
+ "2021-08-19T17:30-0700",
+ "2021-08-19T1730-0700",
+ "2021-08-19T17:30[UTC]",
+ "2021-08-19T1730[UTC]",
+ "2021-08-19T17:30Z[UTC]",
+ "2021-08-19T1730Z[UTC]",
+ "2021-08-19T17:30-07:00[UTC]",
+ "2021-08-19T1730-07:00[UTC]",
+ "2021-08-19T17:30-0700[UTC]",
+ "2021-08-19T1730-0700[UTC]",
+].forEach((timeZone) => {
+ instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } });
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-leap-second.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-leap-second.js
new file mode 100644
index 0000000000..a77fbb0f78
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-leap-second.js
@@ -0,0 +1,22 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Leap second is a valid ISO string for TimeZone
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1);
+let timeZone = "2016-12-31T23:59:60+00:00[UTC]";
+
+// A string with a leap second is a valid ISO string, so the following
+// operation should not throw
+
+instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } });
+
+timeZone = "2021-08-19T17:30:45.123456789+23:59[+23:59:60]";
+assert.throws(RangeError, () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), "leap second in time zone name not valid");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-year-zero.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-year-zero.js
new file mode 100644
index 0000000000..2592d5267a
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-year-zero.js
@@ -0,0 +1,24 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Negative zero, as an extended year, is rejected
+features: [Temporal, arrow-function]
+---*/
+
+const invalidStrings = [
+ "-000000-10-31T17:45Z",
+ "-000000-10-31T17:45+00:00[UTC]",
+];
+const instance = new Temporal.Duration(1);
+invalidStrings.forEach((timeZone) => {
+ assert.throws(
+ RangeError,
+ () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }),
+ "reject minus zero as extended year"
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string.js
new file mode 100644
index 0000000000..bc64d7156c
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string.js
@@ -0,0 +1,40 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Time zone IDs are valid input for a time zone
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const getPossibleInstantsForOriginal = Object.getOwnPropertyDescriptor(Temporal.TimeZone.prototype, "getPossibleInstantsFor");
+Object.defineProperty(Temporal.TimeZone.prototype, "getPossibleInstantsFor", {
+ configurable: true,
+ enumerable: false,
+ get() {
+ TemporalHelpers.assertUnreachable("getPossibleInstantsFor should not be looked up");
+ },
+});
+const getOffsetNanosecondsForOriginal = Object.getOwnPropertyDescriptor(Temporal.TimeZone.prototype, "getOffsetNanosecondsFor");
+Object.defineProperty(Temporal.TimeZone.prototype, "getOffsetNanosecondsFor", {
+ configurable: true,
+ enumerable: false,
+ get() {
+ TemporalHelpers.assertUnreachable("getOffsetNanosecondsFor should not be looked up");
+ },
+});
+
+const instance = new Temporal.Duration(1);
+
+// The following are all valid strings so should not throw:
+
+["UTC", "+01:00"].forEach((timeZone) => {
+ instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } });
+});
+
+Object.defineProperty(Temporal.TimeZone.prototype, "getPossibleInstantsFor", getPossibleInstantsForOriginal);
+Object.defineProperty(Temporal.TimeZone.prototype, "getOffsetNanosecondsFor", getOffsetNanosecondsForOriginal);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-wrong-type.js
new file mode 100644
index 0000000000..1f2d79fa47
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-wrong-type.js
@@ -0,0 +1,42 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Appropriate error thrown when argument cannot be converted to a valid string
+ or object for TimeZone
+features: [BigInt, Symbol, Temporal]
+---*/
+
+const instance = new Temporal.Duration(1);
+
+const primitiveTests = [
+ [null, "null"],
+ [true, "boolean"],
+ ["", "empty string"],
+ [1, "number that doesn't convert to a valid ISO string"],
+ [19761118, "number that would convert to a valid ISO string in other contexts"],
+ [1n, "bigint"],
+];
+
+for (const [timeZone, description] of primitiveTests) {
+ assert.throws(
+ typeof timeZone === 'string' ? RangeError : TypeError,
+ () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }),
+ `${description} does not convert to a valid ISO string`
+ );
+}
+
+const typeErrorTests = [
+ [Symbol(), "symbol"],
+ [{}, "object not implementing time zone protocol"],
+ [new Temporal.Calendar("iso8601"), "calendar instance"],
+];
+
+for (const [timeZone, description] of typeErrorTests) {
+ assert.throws(TypeError, () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), `${description} is not a valid object and does not convert to a string`);
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-datetime.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-datetime.js
new file mode 100644
index 0000000000..b2e0cd8a56
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-datetime.js
@@ -0,0 +1,40 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Conversion of ISO date-time strings as relativeTo option to
+ Temporal.ZonedDateTime or Temporal.PlainDateTime instances
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+let relativeTo = "2019-11-01T00:00";
+const result1 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(result1, 367, "bare date-time string is a plain relativeTo");
+
+relativeTo = "2019-11-01T00:00-07:00";
+const result2 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(result2, 367, "date-time + offset is a plain relativeTo");
+
+relativeTo = "2019-11-01T00:00[-07:00]";
+const result3 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(result3, 367, "date-time + IANA annotation is a zoned relativeTo");
+
+relativeTo = "2019-11-01T00:00Z[-07:00]";
+const result4 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(result4, 367, "date-time + Z + IANA annotation is a zoned relativeTo");
+
+relativeTo = "2019-11-01T00:00+00:00[UTC]";
+const result5 = instance.total({ unit: "days", relativeTo });
+assert.sameValue(result5, 367, "date-time + offset + IANA annotation is a zoned relativeTo");
+
+relativeTo = "2019-11-01T00:00Z";
+assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }), "date-time + Z throws without an IANA annotation");
+relativeTo = "2019-11-01T00:00+04:15[UTC]";
+assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }), "date-time + offset + IANA annotation throws if wall time and exact time mismatch");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-invalid.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-invalid.js
new file mode 100644
index 0000000000..f60e10f831
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-invalid.js
@@ -0,0 +1,16 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: RangeError thrown if relativeTo is a string with the wrong format
+features: [Temporal]
+---*/
+
+['bad string', '15:30:45.123456', 'iso8601', 'UTC', 'P1YT1H'].forEach((relativeTo) => {
+ const duration = new Temporal.Duration(0, 0, 0, 31);
+ assert.throws(RangeError, () => duration.total({ unit: "months", relativeTo }));
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime-invalid.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime-invalid.js
new file mode 100644
index 0000000000..d3d544c220
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime-invalid.js
@@ -0,0 +1,26 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 André Bargull. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Throws a RangeError if "relativeTo" is a date/time value outside the valid limits.
+info: |
+ Temporal.Duration.prototype.total ( totalOf )
+ ...
+ 6. Let relativeTo be ? ToRelativeTemporalObject(totalOf).
+ ...
+
+ ToRelativeTemporalObject ( options )
+ ...
+ 9. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar).
+features: [Temporal]
+---*/
+
+var duration = Temporal.Duration.from({nanoseconds: 0});
+var options = {unit: "nanoseconds", relativeTo: "+999999-01-01"};
+
+assert.throws(RangeError, () => duration.total(options));
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime.js
new file mode 100644
index 0000000000..8f5a2c3f94
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime.js
@@ -0,0 +1,17 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: The relativeTo option accepts a PlainDateTime-like ISO 8601 string
+features: [Temporal]
+---*/
+
+['2000-01-01', '2000-01-01T00:00', '2000-01-01T00:00[u-ca=iso8601]'].forEach((relativeTo) => {
+ const duration = new Temporal.Duration(0, 0, 0, 31);
+ const result = duration.total({ unit: "months", relativeTo });
+ assert.sameValue(result, 1);
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime-wrong-offset.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime-wrong-offset.js
new file mode 100644
index 0000000000..10b00920d9
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime-wrong-offset.js
@@ -0,0 +1,19 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Throws if a ZonedDateTime-like relativeTo string has the wrong UTC offset
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+const relativeTo = "2000-01-01T00:00+05:30[UTC]";
+assert.throws(
+ RangeError,
+ () => instance.total({ unit: "days", relativeTo }),
+ "total should throw RangeError on a string with UTC offset mismatch"
+);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime.js
new file mode 100644
index 0000000000..c23b3e97a4
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime.js
@@ -0,0 +1,22 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: The relativeTo option accepts a ZonedDateTime-like ISO 8601 string
+features: [Temporal]
+---*/
+
+[
+ '2000-01-01[UTC]',
+ '2000-01-01T00:00[UTC]',
+ '2000-01-01T00:00+00:00[UTC]',
+ '2000-01-01T00:00+00:00[UTC][u-ca=iso8601]',
+].forEach((relativeTo) => {
+ const duration = new Temporal.Duration(0, 0, 0, 31);
+ const result = duration.total({ unit: "months", relativeTo });
+ assert.sameValue(result, 1);
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-sub-minute-offset.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-sub-minute-offset.js
new file mode 100644
index 0000000000..46f3849688
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-sub-minute-offset.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: relativeTo string accepts trailing zeroes in sub-minute UTC offset
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+let result;
+let relativeTo;
+
+const action = (relativeTo) => instance.total({ unit: "days", relativeTo });
+
+relativeTo = "1970-01-01T00:00-00:45:00[-00:45]";
+result = action(relativeTo);
+assert.sameValue(result, 366, "ISO string offset accepted with zero seconds (string)");
+
+relativeTo = { year: 1970, month: 1, day: 1, offset: "+00:45:00.000000000", timeZone: "+00:45" };
+result = action(relativeTo);
+assert.sameValue(result, 366, "ISO string offset accepted with zero seconds (property bag)");
+
+relativeTo = "1970-01-01T00:00+00:44:30.123456789[+00:45]";
+assert.throws(RangeError, () => action(relativeTo), "rounding is not accepted between ISO offset and time zone");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-undefined-throw-on-calendar-units.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-undefined-throw-on-calendar-units.js
new file mode 100644
index 0000000000..47a8fdb33f
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-undefined-throw-on-calendar-units.js
@@ -0,0 +1,35 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ The relativeTo option is required when the Duration contains years, months,
+ or weeks, and unit is days; or unit is weeks or months
+features: [Temporal, arrow-function]
+---*/
+
+const oneYear = new Temporal.Duration(1);
+const oneMonth = new Temporal.Duration(0, 1);
+const oneWeek = new Temporal.Duration(0, 0, 1);
+const oneDay = new Temporal.Duration(0, 0, 0, 1);
+
+const options = { unit: "days" };
+assert.sameValue(oneDay.total(options), 1, "days do not require relativeTo");
+assert.sameValue(oneDay.total("days"), 1, "days do not require relativeTo (string shorthand)");
+assert.throws(RangeError, () => oneWeek.total(options), "total days of weeks requires relativeTo");
+assert.throws(RangeError, () => oneWeek.total("days"), "total days of weeks requires relativeTo (string shorthand)");
+assert.throws(RangeError, () => oneMonth.total(options), "total days of months requires relativeTo");
+assert.throws(RangeError, () => oneMonth.total("days"), "total days of months requires relativeTo (string shorthand)");
+assert.throws(RangeError, () => oneYear.total(options), "total days of years requires relativeTo");
+assert.throws(RangeError, () => oneYear.total("days"), "total days of years requires relativeTo (string shorthand)");
+
+["months", "weeks"].forEach((unit) => {
+ [oneDay, oneWeek, oneMonth, oneYear].forEach((duration) => {
+ assert.throws(RangeError, () => duration.total({ unit }), `${duration} total ${unit} requires relativeTo`);
+ assert.throws(RangeError, () => duration.total(unit), `${duration} total ${unit} requires relativeTo (string shorthand)`);
+ });
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-wrong-type.js
new file mode 100644
index 0000000000..1cb6e1bf8d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-wrong-type.js
@@ -0,0 +1,50 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Appropriate error thrown when relativeTo cannot be converted to a valid
+ relativeTo string or property bag
+features: [BigInt, Symbol, Temporal]
+---*/
+
+const timeZone = new Temporal.TimeZone('UTC');
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+const primitiveTests = [
+ [undefined, 'undefined'],
+ [null, 'null'],
+ [true, 'boolean'],
+ ['', 'empty string'],
+ [1, "number that doesn't convert to a valid ISO string"],
+ [1n, 'bigint']
+];
+
+for (const [relativeTo, description] of primitiveTests) {
+ assert.throws(
+ typeof relativeTo === 'string' || typeof relativeTo === 'undefined' ? RangeError : TypeError,
+ () => instance.total({ unit: 'days', relativeTo }),
+ `${description} does not convert to a valid ISO string (first argument)`
+ );
+}
+
+const typeErrorTests = [
+ [Symbol(), 'symbol'],
+ [{}, 'plain object'],
+ [Temporal.PlainDate, 'Temporal.PlainDate, object'],
+ [Temporal.PlainDate.prototype, 'Temporal.PlainDate.prototype, object'],
+ [Temporal.ZonedDateTime, 'Temporal.ZonedDateTime, object'],
+ [Temporal.ZonedDateTime.prototype, 'Temporal.ZonedDateTime.prototype, object']
+];
+
+for (const [relativeTo, description] of typeErrorTests) {
+ assert.throws(
+ TypeError,
+ () => instance.total({ unit: 'days', relativeTo }),
+ `${description} is not a valid property bag and does not convert to a string`
+ );
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-negative-epochnanoseconds.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-negative-epochnanoseconds.js
new file mode 100644
index 0000000000..a52a8c4943
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-negative-epochnanoseconds.js
@@ -0,0 +1,26 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: A pre-epoch value is handled correctly by the modulo operation in GetISOPartsFromEpoch
+info: |
+ sec-temporal-getisopartsfromepoch step 1:
+ 1. Let _remainderNs_ be the mathematical value whose sign is the sign of _epochNanoseconds_ and whose magnitude is abs(_epochNanoseconds_) modulo 10<sup>6</sup>.
+ sec-temporal-builtintimezonegetplaindatetimefor step 2:
+ 2. Let _result_ be ! GetISOPartsFromEpoch(_instant_.[[Nanoseconds]]).
+features: [Temporal]
+---*/
+
+const relativeTo = new Temporal.ZonedDateTime(-13849764_999_999_999n, "UTC");
+const duration = new Temporal.Duration(0, 0, 0, 1);
+
+// This code path shows up anywhere we convert an exact time, before the Unix
+// epoch, with nonzero microseconds or nanoseconds, into a wall time; in this
+// case via relativeTo.
+
+const result = duration.total({ relativeTo, unit: "days" });
+assert.sameValue(result, 1);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js
new file mode 100644
index 0000000000..d415117f89
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js
@@ -0,0 +1,128 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Abstract operation NormalizedTimeDurationToDays can throw four different
+ RangeErrors.
+info: |
+ NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] )
+ 22. If days < 0 and sign = 1, throw a RangeError exception.
+ 23. If days > 0 and sign = -1, throw a RangeError exception.
+ ...
+ 25. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
+ ...
+ 28. If dayLength ≥ 2⁵³, throw a RangeError exception.
+features: [Temporal, BigInt]
+includes: [temporalHelpers.js]
+---*/
+
+const oneNsDuration = Temporal.Duration.from({ nanoseconds: 1 });
+const negOneNsDuration = Temporal.Duration.from({ nanoseconds: -1 });
+const dayNs = 86_400_000_000_000;
+const epochInstant = new Temporal.Instant(0n);
+
+function timeZoneSubstituteValues(
+ getPossibleInstantsFor,
+ getOffsetNanosecondsFor
+) {
+ const tz = new Temporal.TimeZone("UTC");
+ TemporalHelpers.substituteMethod(
+ tz,
+ "getPossibleInstantsFor",
+ getPossibleInstantsFor
+ );
+ TemporalHelpers.substituteMethod(
+ tz,
+ "getOffsetNanosecondsFor",
+ getOffsetNanosecondsFor
+ );
+ return tz;
+}
+
+// Step 22: days < 0 and sign = 1
+let zdt = new Temporal.ZonedDateTime(
+ 0n, // Sets _startNs_ to 0
+ timeZoneSubstituteValues(
+ [[epochInstant]], // Returned in step 16, setting _relativeResult_
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Pre-conversion in Duration.p.total
+ dayNs - 1, // Returned in step 8, setting _startDateTime_
+ -dayNs + 1, // Returned in step 9, setting _endDateTime_
+ ]
+ )
+);
+assert.throws(RangeError, () =>
+ // Using 1ns duration _nanoseconds_ to 1 and _sign_ to 1
+ oneNsDuration.total({
+ relativeTo: zdt,
+ unit: "day",
+ }),
+ "RangeError when days < 0 and sign = 1"
+);
+
+// Step 23: days > 0 and sign = -1
+zdt = new Temporal.ZonedDateTime(
+ 0n, // Sets _startNs_ to 0
+ timeZoneSubstituteValues(
+ [[epochInstant]], // Returned in step 16, setting _relativeResult_
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Pre-conversion in Duration.p.total
+ -dayNs + 1, // Returned in step 8, setting _startDateTime_
+ dayNs - 1, // Returned in step 9, setting _endDateTime_
+ ]
+ )
+);
+assert.throws(RangeError, () =>
+ // Using -1ns duration sets _nanoseconds_ to -1 and _sign_ to -1
+ negOneNsDuration.total({
+ relativeTo: zdt,
+ unit: "day",
+ }),
+ "RangeError when days > 0 and sign = -1"
+);
+
+// Step 25: nanoseconds > 0 and sign = -1
+zdt = new Temporal.ZonedDateTime(
+ 0n, // Sets _startNs_ to 0
+ timeZoneSubstituteValues(
+ [
+ [new Temporal.Instant(-2n)], // Returned in step 16, setting _relativeResult_
+ [new Temporal.Instant(-4n)], // Returned in step 21.a, setting _oneDayFarther_
+ ],
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // pre-conversion in Duration.p.total
+ dayNs - 1, // Returned in step 8, setting _startDateTime_
+ -dayNs + 1, // Returned in step 9, setting _endDateTime_
+ ]
+ )
+);
+assert.throws(RangeError, () =>
+ // Using -1ns duration sets _nanoseconds_ to -1 and _sign_ to -1
+ negOneNsDuration.total({
+ relativeTo: zdt,
+ unit: "day",
+ }),
+ "RangeError when nanoseconds > 0 and sign = -1"
+);
+
+// Step 28: day length is an unsafe integer
+zdt = new Temporal.ZonedDateTime(
+ 0n,
+ timeZoneSubstituteValues(
+ // Not called in step 16 because _days_ = 0
+ // Returned in step 21.a, making _oneDayFarther_ 2^53 ns later than _relativeResult_
+ [[new Temporal.Instant(2n ** 53n)]],
+ []
+ )
+);
+assert.throws(RangeError, () =>
+ oneNsDuration.total({
+ relativeTo: zdt,
+ unit: "days",
+ }),
+ "Should throw RangeError when time zone calculates an outrageous day length"
+);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-non-integer.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-non-integer.js
new file mode 100644
index 0000000000..4be071d057
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-non-integer.js
@@ -0,0 +1,19 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: RangeError thrown if time zone reports an offset that is not an integer number of nanoseconds
+features: [Temporal]
+includes: [temporalHelpers.js]
+---*/
+
+[3600_000_000_000.5, NaN, -Infinity, Infinity].forEach((wrongOffset) => {
+ const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset);
+ const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+ const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, timeZone);
+ assert.throws(RangeError, () => duration.total({ unit: "seconds", relativeTo: datetime }));
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-not-callable.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-not-callable.js
new file mode 100644
index 0000000000..0534adf10f
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-not-callable.js
@@ -0,0 +1,23 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: TypeError thrown if timeZone.getOffsetNanosecondsFor is not callable
+features: [BigInt, Symbol, Temporal, arrow-function]
+---*/
+
+[undefined, null, true, Math.PI, 'string', Symbol('sym'), 42n, {}].forEach((notCallable) => {
+ const timeZone = new Temporal.TimeZone("UTC");
+ const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+ const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, timeZone);
+ timeZone.getOffsetNanosecondsFor = notCallable;
+ assert.throws(
+ TypeError,
+ () => duration.total({ unit: "seconds", relativeTo: datetime }),
+ `Uncallable ${notCallable === null ? 'null' : typeof notCallable} getOffsetNanosecondsFor should throw TypeError`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-out-of-range.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-out-of-range.js
new file mode 100644
index 0000000000..10d39099e8
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-out-of-range.js
@@ -0,0 +1,19 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: RangeError thrown if time zone reports an offset that is out of range
+features: [Temporal]
+includes: [temporalHelpers.js]
+---*/
+
+[-86400_000_000_000, 86400_000_000_000].forEach((wrongOffset) => {
+ const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset);
+ const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+ const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, timeZone);
+ assert.throws(RangeError, () => duration.total({ unit: "seconds", relativeTo: datetime }));
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-wrong-type.js
new file mode 100644
index 0000000000..26cd533b6c
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-wrong-type.js
@@ -0,0 +1,28 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: TypeError thrown if time zone reports an offset that is not a Number
+features: [Temporal]
+includes: [temporalHelpers.js]
+---*/
+
+[
+ undefined,
+ null,
+ true,
+ "+01:00",
+ Symbol(),
+ 3600_000_000_000n,
+ {},
+ { valueOf() { return 3600_000_000_000; } },
+].forEach((wrongOffset) => {
+ const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset);
+ const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+ const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, timeZone);
+ assert.throws(TypeError, () => duration.total({ unit: "seconds", relativeTo: datetime }));
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days-different-sign.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days-different-sign.js
new file mode 100644
index 0000000000..8173ef0b23
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days-different-sign.js
@@ -0,0 +1,38 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 André Bargull. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Relative to a ZonedDateTime with a fractional number of days and different sign.
+features: [Temporal]
+---*/
+
+let duration = Temporal.Duration.from({
+ weeks: 1,
+ days: 0,
+ hours: 1,
+});
+
+let cal = new class extends Temporal.Calendar {
+ #dateAdd = 0;
+
+ dateAdd(date, duration, options) {
+ if (++this.#dateAdd === 1) {
+ duration = "-P1W";
+ }
+ return super.dateAdd(date, duration, options);
+ }
+}("iso8601");
+
+let zdt = new Temporal.ZonedDateTime(0n, "UTC", cal);
+
+let result = duration.total({
+ relativeTo: zdt,
+ unit: "days",
+});
+
+assert.sameValue(result, -7 + 1 / 24);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days.js
new file mode 100644
index 0000000000..6f234590f0
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days.js
@@ -0,0 +1,27 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 André Bargull. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ Relative to a ZonedDateTime with a fractional number of days.
+features: [Temporal]
+---*/
+
+let duration = Temporal.Duration.from({
+ weeks: 1,
+ days: 0,
+ hours: 1,
+});
+
+let zdt = new Temporal.ZonedDateTime(0n, "UTC", "iso8601");
+
+let result = duration.total({
+ relativeTo: zdt,
+ unit: "days",
+});
+
+assert.sameValue(result, 7 + 1 / 24);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/shell.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/shell.js
new file mode 100644
index 0000000000..eda1477282
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/shell.js
@@ -0,0 +1,24 @@
+// GENERATED, DO NOT EDIT
+// file: isConstructor.js
+// Copyright (C) 2017 André Bargull. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+description: |
+ Test if a given function is a constructor function.
+defines: [isConstructor]
+features: [Reflect.construct]
+---*/
+
+function isConstructor(f) {
+ if (typeof f !== "function") {
+ throw new Test262Error("isConstructor invoked with a non-function value");
+ }
+
+ try {
+ Reflect.construct(function(){}, [], f);
+ } catch (e) {
+ return false;
+ }
+ return true;
+}
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/timezone-getpossibleinstantsfor-iterable.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/timezone-getpossibleinstantsfor-iterable.js
new file mode 100644
index 0000000000..bd7ca83961
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/timezone-getpossibleinstantsfor-iterable.js
@@ -0,0 +1,36 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: An iterable returned from timeZone.getPossibleInstantsFor is consumed after each call
+info: |
+ sec-temporal.duration.prototype.total steps 4 and 10:
+ 4. Let _relativeTo_ be ? ToRelativeTemporalObject(_options_).
+ 10. Let _balanceResult_ be ? BalanceDuration(_unbalanceResult_.[[Days]], [...], _unbalanceResult_.[[Nanoseconds]], _unit_, _intermediate_).
+ sec-temporal-torelativetemporalobject step 6.d:
+ d. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_result_.[[Year]], [...], _result_.[[Nanosecond]], _offsetNs_, _timeZone_, *"compatible"*, *"reject"*).
+ sec-temporal-interpretisodatetimeoffset step 7:
+ 7. Let _possibleInstants_ be ? GetPossibleInstantsFor(_timeZone_, _dateTime_).
+ sec-temporal-addzoneddatetime step 8:
+ 8. Let _intermediateInstant_ be ? BuiltinTimeZoneGetInstantFor(_timeZone_, _intermediateDateTime_, *"compatible"*).
+ sec-temporal-builtintimezonegetinstantfor step 1:
+ 1. Let _possibleInstants_ be ? GetPossibleInstantsFor(_timeZone_, _dateTime_).
+ sec-temporal-getpossibleinstantsfor step 2:
+ 2. Let _list_ be ? IterableToList(_possibleInstants_).
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const expected = [
+ "2000-01-01T00:00:00", // called once on the input relativeTo if ZonedDateTime
+ "2001-02-09T00:00:00", // called once on the intermediate ZonedDateTime with the calendar parts of the Duration added
+];
+
+TemporalHelpers.checkTimeZonePossibleInstantsIterable((timeZone) => {
+ const duration = new Temporal.Duration(1, 1, 1, 1, 1, 1, 1);
+ duration.total({ unit: 'seconds', relativeTo: { year: 2000, month: 1, day: 1, timeZone } });
+}, expected);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-disallowed-units-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-disallowed-units-string.js
new file mode 100644
index 0000000000..ac37e1be6e
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-disallowed-units-string.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Specifically disallowed units for the unit option
+features: [Temporal, arrow-function]
+---*/
+
+const instance = new Temporal.Duration(0, 0, 0, 4, 5, 6, 7, 987, 654, 321);
+const invalidUnits = [
+ "era",
+ "eras",
+];
+invalidUnits.forEach((unit) => {
+ assert.throws(
+ RangeError,
+ () => instance.total({ unit }),
+ `{ unit: "${unit}" } should not be allowed as an argument to total`
+ );
+ assert.throws(
+ RangeError,
+ () => instance.total(unit),
+ `"${unit}" should not be allowed as an argument to total`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-invalid-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-invalid-string.js
new file mode 100644
index 0000000000..119d7a5f05
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-invalid-string.js
@@ -0,0 +1,21 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.protoype.total
+description: RangeError thrown when unit option not one of the allowed string values
+info: |
+ sec-getoption step 10:
+ 10. If _values_ is not *undefined* and _values_ does not contain an element equal to _value_, throw a *RangeError* exception.
+ sec-temporal-totemporaldurationtotal step 1:
+ 1. Let _unit_ be ? GetOption(_normalizedOptions_, *"unit"*, « String », « *"year"*, *"years"*, *"month"*, *"months"*, *"week"*, *"weeks"*, *"day"*, *"days"*, *"hour"*, *"hours"*, *"minute"*, *"minutes"*, *"second"*, *"seconds"*, *"millisecond"*, *"milliseconds"*, *"microsecond"*, *"microseconds"*, *"nanosecond"*, *"nanoseconds"* », *undefined*).
+ sec-temporal.duration.protoype.total step 5:
+ 5. Let _unit_ be ? ToTemporalDurationTotalUnit(_options_).
+features: [Temporal]
+---*/
+
+const duration = new Temporal.Duration(0, 0, 0, 1);
+assert.throws(RangeError, () => duration.total({ unit: "other string" }));
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted-string.js
new file mode 100644
index 0000000000..82b7932d1d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted-string.js
@@ -0,0 +1,24 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Plural units are accepted as well for the shorthand for the unit option
+includes: [temporalHelpers.js]
+features: [Temporal, arrow-function]
+---*/
+
+const duration = new Temporal.Duration(0, 0, 0, 4, 5, 6, 7, 987, 654, 321);
+const validUnits = [
+ "day",
+ "hour",
+ "minute",
+ "second",
+ "millisecond",
+ "microsecond",
+ "nanosecond",
+];
+TemporalHelpers.checkPluralUnitsAccepted((unit) => duration.total(unit), validUnits);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted.js
new file mode 100644
index 0000000000..17737506cc
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted.js
@@ -0,0 +1,28 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Plural units are accepted as well for the unit option
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321);
+const relativeTo = new Temporal.PlainDate(2000, 1, 1);
+const validUnits = [
+ "year",
+ "month",
+ "week",
+ "day",
+ "hour",
+ "minute",
+ "second",
+ "millisecond",
+ "microsecond",
+ "nanosecond",
+];
+TemporalHelpers.checkPluralUnitsAccepted((unit) => duration.total({ unit, relativeTo }), validUnits);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-string-shorthand-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-string-shorthand-string.js
new file mode 100644
index 0000000000..9aab057420
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-string-shorthand-string.js
@@ -0,0 +1,27 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: String as first argument is equivalent to options bag with unit option
+features: [Temporal, arrow-function]
+---*/
+
+const instance = new Temporal.Duration(0, 0, 0, 4, 5, 6, 7, 987, 654, 321);
+const validUnits = [
+ "day",
+ "hour",
+ "minute",
+ "second",
+ "millisecond",
+ "microsecond",
+ "nanosecond",
+];
+validUnits.forEach((unit) => {
+ const full = instance.total({ unit });
+ const shorthand = instance.total(unit);
+ assert.sameValue(shorthand, full, `"${unit}" as first argument to total is equivalent to options bag`);
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-wrong-type.js
new file mode 100644
index 0000000000..7fa1a8ba6b
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-wrong-type.js
@@ -0,0 +1,25 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2021 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Type conversions for unit option
+info: |
+ sec-getoption step 9.a:
+ a. Set _value_ to ? ToString(_value_).
+ sec-temporal-totemporaldurationtotal step 1:
+ 1. Let _unit_ be ? GetOption(_normalizedOptions_, *"unit"*, « String », « *"year"*, *"years"*, *"month"*, *"months"*, *"week"*, *"weeks"*, *"day"*, *"days"*, *"hour"*, *"hours"*, *"minute"*, *"minutes"*, *"second"*, *"seconds"*, *"millisecond"*, *"milliseconds"*, *"microsecond"*, *"microseconds"*, *"nanosecond"*, *"nanoseconds"* », *undefined*).
+ sec-temporal.duration.protoype.total step 5:
+ 5. Let _unit_ be ? ToTemporalDurationTotalUnit(_options_).
+includes: [compareArray.js, temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const duration = new Temporal.Duration(0, 0, 0, 1);
+TemporalHelpers.checkStringOptionWrongType("unit", "hour",
+ (unit) => duration.total({ unit }),
+ (result, descr) => assert.sameValue(result, 24, descr),
+);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/year-zero.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/year-zero.js
new file mode 100644
index 0000000000..eb790cce0a
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/year-zero.js
@@ -0,0 +1,20 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2022 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: Negative zero, as an extended year, is rejected
+features: [Temporal, arrow-function]
+---*/
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 24);
+
+let relativeTo = "-000000-11-04T00:00";
+assert.throws(
+ RangeError,
+ () => { instance.total({ unit: "days", relativeTo }); },
+ "reject minus zero as extended year"
+);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-day-length.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-day-length.js
new file mode 100644
index 0000000000..8b93da72eb
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-day-length.js
@@ -0,0 +1,43 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: A malicious time zone resulting a day length of zero is handled correctly
+info: |
+ Based on a test by André Bargull.
+
+ RoundDuration step 6:
+ d. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _intermediate_).
+ e. Set _days_ to _days_ + _result_.[[Days]] + _result_.[[Nanoseconds]] / _result_.[[DayLength]].
+
+ NanosecondsToDays steps 19-23:
+ 19. If _days_ < 0 and _sign_ = 1, throw a *RangeError* exception.
+ 20. If _days_ > 0 and _sign_ = -1, throw a *RangeError* exception.
+ 21. If _nanoseconds_ < 0, then
+ a. Assert: sign is -1.
+ 22. If _nanoseconds_ > 0 and _sign_ = -1, throw a *RangeError* exception.
+ 23. Assert: The inequality abs(_nanoseconds_) < abs(_dayLengthNs_) holds.
+features: [Temporal]
+---*/
+
+const instance = new Temporal.Duration(0, 0, 0, 0, -24, 0, 0, 0, 0, -1);
+
+const tz = new class extends Temporal.TimeZone {
+ #getPossibleInstantsForCalls = 0;
+
+ getPossibleInstantsFor(dt) {
+ this.#getPossibleInstantsForCalls++;
+
+ if (this.#getPossibleInstantsForCalls <= 2) {
+ return [new Temporal.Instant(-86400_000_000_000n - 2n)]
+ }
+ return super.getPossibleInstantsFor(dt);
+ }
+}("UTC");
+
+const relativeTo = new Temporal.ZonedDateTime(0n, tz, "iso8601");
+assert.throws(RangeError, () => instance.total({ relativeTo, unit: "days" }));
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-year-month-week-length.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-year-month-week-length.js
new file mode 100644
index 0000000000..8036c28320
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-year-month-week-length.js
@@ -0,0 +1,34 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2023 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.duration.prototype.total
+description: >
+ A malicious calendar resulting in a year, month, or week length of zero is
+ handled correctly
+info: |
+ RoundDuration
+ 10.z. If _oneYearDays_ = 0, throw a *RangeError* exception.
+ ...
+ 11.z. If _oneMonthDays_ = 0, throw a *RangeError* exception.
+ ...
+ 12.s. If _oneWeekDays_ = 0, throw a *RangeError* exception.
+features: [Temporal]
+---*/
+
+const cal = new class extends Temporal.Calendar {
+ dateAdd(date, duration, options) {
+ // Called several times, last call sets oneYear/Month/WeekDays to 0
+ return new Temporal.PlainDate(1970, 1, 1);
+ }
+}("iso8601");
+
+const instance = new Temporal.Duration(1, 0, 0, 0, 0, 0, 0, 0, 0, 1);
+const relativeTo = new Temporal.ZonedDateTime(0n, "UTC", cal);
+
+assert.throws(RangeError, () => instance.total({ relativeTo, unit: "years" }), "zero year length handled correctly");
+assert.throws(RangeError, () => instance.total({ relativeTo, unit: "months" }), "zero month length handled correctly");
+assert.throws(RangeError, () => instance.total({ relativeTo, unit: "weeks" }), "zero week length handled correctly");
+
+reportCompare(0, 0);