diff options
Diffstat (limited to 'js/src/tests/test262/intl402/Temporal/TimeZone')
60 files changed, 9151 insertions, 0 deletions
diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/basic.js b/js/src/tests/test262/intl402/Temporal/TimeZone/basic.js new file mode 100644 index 0000000000..2344d05187 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/basic.js @@ -0,0 +1,32 @@ +// |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.timezone +description: Basic tests for the Temporal.TimeZone constructor. +features: [Temporal] +---*/ + +const valid = [ + ["Europe/Vienna"], + ["America/New_York"], + ["Africa/CAIRO", "Africa/Cairo"], + ["africa/cairo", "Africa/Cairo"], + ["Asia/Ulaanbaatar"], + ["Asia/Ulan_Bator"], + ["UTC"], + ["GMT"] +]; +for (const [zone, id = zone] of valid) { + const result = new Temporal.TimeZone(zone); + assert.sameValue(typeof result, "object", `object should be created for ${zone}`); + assert.sameValue(result.id, id, `id for ${zone} should be ${id}`); +} + +const invalid = ["+00:01.1", "-01.1"]; +for (const zone of invalid) { + assert.throws(RangeError, () => new Temporal.TimeZone(zone), `should throw for ${zone}`); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/etc-timezone.js b/js/src/tests/test262/intl402/Temporal/TimeZone/etc-timezone.js new file mode 100644 index 0000000000..7703f6b58f --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/etc-timezone.js @@ -0,0 +1,69 @@ +// |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.timezone +description: Some Etc/GMT{+/-}{0}N timezones are valid, but not all +features: [Temporal] +---*/ + +// "Etc/GMT-0" through "Etc/GMT-14" are OK + +[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14].forEach((n) => { + let tz = "Etc/GMT-" + n; + let instance = new Temporal.TimeZone(tz); + assert.sameValue( + instance.toString(), + tz, + tz + " is a valid timezone" + ); +}); + +let gmtMinus24TZ = "Etc/GMT-24"; +assert.throws( + RangeError, + () => { new Temporal.TimeZone(gmtMinus24TZ); }, + gmtMinus24TZ + " is an invalid timezone" +); + +// "Etc/GMT-0N" is not OK (1 ≤ N ≤ 9) +[1,2,3,4,5,6,7,8,9].forEach((n) => { + let tz = "Etc/GMT-0" + n; + assert.throws( + RangeError, + () => { new Temporal.TimeZone(tz); }, + tz + " is an invalid timezone" + ); +}); + +// "Etc/GMT+0N" is not OK (0 ≤ N ≤ 9) +[0,1,2,3,4,5,6,7,8,9].forEach((n) => { + let tz = "Etc/GMT+0" + n; + assert.throws( + RangeError, + () => { new Temporal.TimeZone(tz); }, + tz + " is an invalid timezone" + ); +}); + +// Etc/GMT+0" through "Etc/GMT+12" are OK + +[0,1,2,3,4,5,6,7,8,9,10,11,12].forEach((n) => { + let tz = "Etc/GMT+" + n; + let instance = new Temporal.TimeZone(tz); + assert.sameValue( + instance.toString(), + tz, + tz + " is a valid timezone" + ); +}); + +let gmtPlus24TZ = "Etc/GMT+24"; +assert.throws( + RangeError, + () => { new Temporal.TimeZone(gmtPlus24TZ); }, + gmtPlus24TZ + " is an invalid timezone" +); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/from/argument-object.js b/js/src/tests/test262/intl402/Temporal/TimeZone/from/argument-object.js new file mode 100644 index 0000000000..64c3264f09 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/from/argument-object.js @@ -0,0 +1,39 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2020 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.timezone.from +description: An object is returned unchanged +features: [Temporal] +---*/ + +class CustomTimeZone extends Temporal.TimeZone {} + +const objects = [ + new Temporal.TimeZone("Europe/Madrid"), + new CustomTimeZone("Africa/Accra"), +]; + +const thisValues = [ + Temporal.TimeZone, + CustomTimeZone, + {}, + null, + undefined, + 7, +]; + +for (const thisValue of thisValues) { + for (const object of objects) { + const result = Temporal.TimeZone.from.call(thisValue, object); + assert.sameValue(result, object); + } + + const zdt = new Temporal.ZonedDateTime(0n, "Africa/Cairo"); + const fromZdt = Temporal.TimeZone.from.call(thisValue, zdt); + assert.notSameValue(fromZdt, zdt.getTimeZone(), "from() creates a new object for a string slot value"); + assert.sameValue(fromZdt.id, "Africa/Cairo"); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/from/argument-valid.js b/js/src/tests/test262/intl402/Temporal/TimeZone/from/argument-valid.js new file mode 100644 index 0000000000..340be95d3b --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/from/argument-valid.js @@ -0,0 +1,33 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2020 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.timezone.from +description: Built-in time zones are parsed correctly out of valid strings +features: [Temporal] +---*/ + +const valids = [ + ["Africa/Bissau"], + ["America/Belem"], + ["Europe/Vienna"], + ["America/New_York"], + ["Africa/CAIRO", "Africa/Cairo"], + ["Asia/Ulan_Bator"], + ["GMT"], + ["etc/gmt", "Etc/GMT"], + ["1994-11-05T08:15:30-05:00[America/New_York]", "America/New_York"], + ["1994-11-05T08:15:30-05[America/New_York]", "America/New_York"], + ["1994-11-05T08:15:30\u221205:00[America/New_York]", "America/New_York"], + ["1994-11-05T08:15:30\u221205[America/New_York]", "America/New_York"], +]; + +for (const [valid, canonical = valid] of valids) { + const result = Temporal.TimeZone.from(valid); + assert.sameValue(Object.getPrototypeOf(result), Temporal.TimeZone.prototype); + assert.sameValue(result.id, canonical); + assert.sameValue(result.toString(), canonical); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/from/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/from/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/from/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/from/etc-timezone.js b/js/src/tests/test262/intl402/Temporal/TimeZone/from/etc-timezone.js new file mode 100644 index 0000000000..b29088007e --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/from/etc-timezone.js @@ -0,0 +1,69 @@ +// |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.timezone.from +description: Some Etc/GMT{+/-}{0}N timezones are valid, but not all +features: [Temporal] +---*/ + +// "Etc/GMT-0" through "Etc/GMT-14" are OK + +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].forEach((n) => { + const tz = "Etc/GMT-" + n; + const instance = Temporal.TimeZone.from(tz); + assert.sameValue( + instance.toString(), + tz, + tz + " is a valid timezone" + ); +}); + +const gmtMinus24TZ = "Etc/GMT-24"; +assert.throws( + RangeError, + () => Temporal.TimeZone.from(gmtMinus24TZ), + gmtMinus24TZ + " is an invalid timezone" +); + +// "Etc/GMT-0N" is not OK (1 ≤ N ≤ 9) +[1, 2, 3, 4, 5, 6, 7, 8, 9].forEach((n) => { + const tz = "Etc/GMT-0" + n; + assert.throws( + RangeError, + () => Temporal.TimeZone.from(tz), + tz + " is an invalid timezone" + ); +}); + +// "Etc/GMT+0N" is not OK (0 ≤ N ≤ 9) +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach((n) => { + const tz = "Etc/GMT+0" + n; + assert.throws( + RangeError, + () => Temporal.TimeZone.from(tz), + tz + " is an invalid timezone" + ); +}); + +// "Etc/GMT+0" through "Etc/GMT+12" are OK + +[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].forEach((n) => { + const tz = "Etc/GMT+" + n; + const instance = Temporal.TimeZone.from(tz); + assert.sameValue( + instance.toString(), + tz, + tz + " is a valid timezone" + ); +}); + +const gmtPlus24TZ = "Etc/GMT+24"; +assert.throws( + RangeError, + () => Temporal.TimeZone.from(gmtPlus24TZ), + gmtPlus24TZ + " is an invalid timezone" +); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/from/iana-legacy-names.js b/js/src/tests/test262/intl402/Temporal/TimeZone/from/iana-legacy-names.js new file mode 100644 index 0000000000..d04aa99656 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/from/iana-legacy-names.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.timezone +description: IANA legacy names must be supported +features: [Temporal] +---*/ + +const legacyNames = [ + "Etc/GMT0", + "GMT0", + "GMT-0", + "GMT+0", + "EST5EDT", + "CST6CDT", + "MST7MDT", + "PST8PDT" +]; + +legacyNames.forEach((arg) => { + const tz = Temporal.TimeZone.from(arg); + assert.sameValue(tz.toString(), arg, `"${arg}" does not match "${tz.toString()}" time zone identifier`); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/from/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/from/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/from/shell.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/from/timezone-case-insensitive.js b/js/src/tests/test262/intl402/Temporal/TimeZone/from/timezone-case-insensitive.js new file mode 100644 index 0000000000..52a86b6497 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/from/timezone-case-insensitive.js @@ -0,0 +1,628 @@ +// |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.timezone.from +description: Time zone identifiers are case-normalized +features: [Temporal] +---*/ + +const timeZoneIdentifiers = [ + // IANA TZDB Zone names + 'Africa/Abidjan', + 'Africa/Algiers', + 'Africa/Bissau', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/El_Aaiun', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Khartoum', + 'Africa/Lagos', + 'Africa/Maputo', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Sao_Tome', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Araguaina', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Catamarca', + 'America/Argentina/Cordoba', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/Mendoza', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Asuncion', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Cayenne', + 'America/Chicago', + 'America/Chihuahua', + // 'America/Ciudad_Juarez' // uncomment after Node supports this ID added in TZDB 2022g + 'America/Costa_Rica', + 'America/Cuiaba', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Fort_Nelson', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Indianapolis', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Juneau', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/New_York', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Nuuk', + 'America/Ojinaga', + 'America/Panama', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port-au-Prince', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Sitka', + 'America/St_Johns', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Tijuana', + 'America/Toronto', + 'America/Vancouver', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/Troll', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Colombo', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Hebron', + 'Asia/Ho_Chi_Minh', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Kathmandu', + 'Asia/Khandyga', + 'Asia/Kolkata', + 'Asia/Krasnoyarsk', + 'Asia/Kuching', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Nicosia', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qostanay', + 'Asia/Qyzylorda', + 'Asia/Riyadh', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ulaanbaatar', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yangon', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faroe', + 'Atlantic/Madeira', + 'Atlantic/South_Georgia', + 'Atlantic/Stanley', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/Perth', + 'Australia/Sydney', + 'CET', + 'CST6CDT', + 'EET', + 'EST', + 'EST5EDT', + 'Etc/GMT', + 'Etc/GMT+1', + 'Etc/GMT+10', + 'Etc/GMT+11', + 'Etc/GMT+12', + 'Etc/GMT+2', + 'Etc/GMT+3', + 'Etc/GMT+4', + 'Etc/GMT+5', + 'Etc/GMT+6', + 'Etc/GMT+7', + 'Etc/GMT+8', + 'Etc/GMT+9', + 'Etc/GMT-1', + 'Etc/GMT-10', + 'Etc/GMT-11', + 'Etc/GMT-12', + 'Etc/GMT-13', + 'Etc/GMT-14', + 'Etc/GMT-2', + 'Etc/GMT-3', + 'Etc/GMT-4', + 'Etc/GMT-5', + 'Etc/GMT-6', + 'Etc/GMT-7', + 'Etc/GMT-8', + 'Etc/GMT-9', + 'Etc/UTC', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Chisinau', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Helsinki', + 'Europe/Istanbul', + 'Europe/Kaliningrad', + 'Europe/Kirov', + 'Europe/Kyiv', + 'Europe/Lisbon', + 'Europe/London', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Minsk', + 'Europe/Moscow', + 'Europe/Paris', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Sofia', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Ulyanovsk', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zurich', + 'HST', + 'Indian/Chagos', + 'Indian/Maldives', + 'Indian/Mauritius', + 'MET', + 'MST', + 'MST7MDT', + 'PST8PDT', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Kanton', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Marquesas', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + + // IANA TZDB Link names + 'WET', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Asmara', + 'Africa/Asmera', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Kampala', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Timbuktu', + 'America/Anguilla', + 'America/Antigua', + 'America/Argentina/ComodRivadavia', + 'America/Aruba', + 'America/Atikokan', + 'America/Atka', + 'America/Blanc-Sablon', + 'America/Buenos_Aires', + 'America/Catamarca', + 'America/Cayman', + 'America/Coral_Harbour', + 'America/Cordoba', + 'America/Creston', + 'America/Curacao', + 'America/Dominica', + 'America/Ensenada', + 'America/Fort_Wayne', + 'America/Godthab', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Indianapolis', + 'America/Jujuy', + 'America/Knox_IN', + 'America/Kralendijk', + 'America/Louisville', + 'America/Lower_Princes', + 'America/Marigot', + 'America/Mendoza', + 'America/Montreal', + 'America/Montserrat', + 'America/Nassau', + 'America/Nipigon', + 'America/Pangnirtung', + 'America/Port_of_Spain', + 'America/Porto_Acre', + 'America/Rainy_River', + 'America/Rosario', + 'America/Santa_Isabel', + 'America/Shiprock', + 'America/St_Barthelemy', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Thunder_Bay', + 'America/Tortola', + 'America/Virgin', + 'Antarctica/DumontDUrville', + 'Antarctica/McMurdo', + 'Antarctica/South_Pole', + 'Antarctica/Syowa', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Ashkhabad', + 'Asia/Bahrain', + 'Asia/Brunei', + 'Asia/Calcutta', + 'Asia/Chongqing', + 'Asia/Chungking', + 'Asia/Dacca', + 'Asia/Harbin', + 'Asia/Istanbul', + 'Asia/Kashgar', + 'Asia/Katmandu', + 'Asia/Kuala_Lumpur', + 'Asia/Kuwait', + 'Asia/Macao', + 'Asia/Muscat', + 'Asia/Phnom_Penh', + 'Asia/Rangoon', + 'Asia/Saigon', + 'Asia/Tel_Aviv', + 'Asia/Thimbu', + 'Asia/Ujung_Pandang', + 'Asia/Ulan_Bator', + 'Asia/Vientiane', + 'Atlantic/Faeroe', + 'Atlantic/Jan_Mayen', + 'Atlantic/Reykjavik', + 'Atlantic/St_Helena', + 'Australia/ACT', + 'Australia/Canberra', + 'Australia/Currie', + 'Australia/LHI', + 'Australia/NSW', + 'Australia/North', + 'Australia/Queensland', + 'Australia/South', + 'Australia/Tasmania', + 'Australia/Victoria', + 'Australia/West', + 'Australia/Yancowinna', + 'Brazil/Acre', + 'Brazil/DeNoronha', + 'Brazil/East', + 'Brazil/West', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Canada/Saskatchewan', + 'Canada/Yukon', + 'Chile/Continental', + 'Chile/EasterIsland', + 'Cuba', + 'Egypt', + 'Eire', + 'Etc/GMT+0', + 'Etc/GMT-0', + 'Etc/GMT0', + 'Etc/Greenwich', + 'Etc/UCT', + 'Etc/Universal', + 'Etc/Zulu', + 'Europe/Amsterdam', + 'Europe/Belfast', + 'Europe/Bratislava', + 'Europe/Busingen', + 'Europe/Copenhagen', + 'Europe/Guernsey', + 'Europe/Isle_of_Man', + 'Europe/Jersey', + 'Europe/Kiev', + 'Europe/Ljubljana', + 'Europe/Luxembourg', + 'Europe/Mariehamn', + 'Europe/Monaco', + 'Europe/Nicosia', + 'Europe/Oslo', + 'Europe/Podgorica', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Skopje', + 'Europe/Stockholm', + 'Europe/Tiraspol', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'GB', + 'GB-Eire', + 'GMT', + 'GMT+0', + 'GMT-0', + 'GMT0', + 'Greenwich', + 'Hongkong', + 'Iceland', + 'Indian/Antananarivo', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Mayotte', + 'Indian/Reunion', + 'Iran', + 'Israel', + 'Jamaica', + 'Japan', + 'Kwajalein', + 'Libya', + 'Mexico/BajaNorte', + 'Mexico/BajaSur', + 'Mexico/General', + 'NZ', + 'NZ-CHAT', + 'Navajo', + 'PRC', + 'Pacific/Chuuk', + 'Pacific/Enderbury', + 'Pacific/Funafuti', + 'Pacific/Johnston', + 'Pacific/Majuro', + 'Pacific/Midway', + 'Pacific/Pohnpei', + 'Pacific/Ponape', + 'Pacific/Saipan', + 'Pacific/Samoa', + 'Pacific/Truk', + 'Pacific/Wake', + 'Pacific/Wallis', + 'Pacific/Yap', + 'Poland', + 'Portugal', + 'ROC', + 'ROK', + 'Singapore', + 'Turkey', + 'UCT', + 'US/Alaska', + 'US/Aleutian', + 'US/Arizona', + 'US/Central', + 'US/East-Indiana', + 'US/Eastern', + 'US/Hawaii', + 'US/Indiana-Starke', + 'US/Michigan', + 'US/Mountain', + 'US/Pacific', + 'US/Pacific-New', + 'US/Samoa', + 'UTC', + 'Universal', + 'W-SU', + 'Zulu' +]; + +// We want to test all available named time zone identifiers (both primary and non-primary), +// but no ECMAScript built-in API exposes that list. So we use a union of two sources: +// 1. A hard-coded list of Zone and Link identifiers from the 2022g version of IANA TZDB. +// 2. Canonical IDs exposed by Intl.supportedValuesOf('timeZone'), which ensures that IDs +// added to TZDB later than 2022g will be tested. (New IDs are almost always added as primary.) +const ids = [...new Set([...timeZoneIdentifiers, ...Intl.supportedValuesOf('timeZone')])]; +for (const id of ids) { + const lower = id.toLowerCase(); + const upper = id.toUpperCase(); + assert.sameValue(new Temporal.TimeZone(id).toString(), id, `Time zone created from string "${id}"`); + assert.sameValue(new Temporal.TimeZone(upper).toString(), id, `Time zone created from string "${upper}"`); + assert.sameValue(new Temporal.TimeZone(lower).toString(), id, `Time zone created from string "${lower}"`); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/from/timezone-string-datetime.js b/js/src/tests/test262/intl402/Temporal/TimeZone/from/timezone-string-datetime.js new file mode 100644 index 0000000000..c0b0a03f5c --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/from/timezone-string-datetime.js @@ -0,0 +1,23 @@ +// |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.timezone.from +description: Conversion of ISO date-time strings to Temporal.TimeZone instances (with IANA time zones) +features: [Temporal] +---*/ + +let timeZone = "2021-08-19T17:30[America/Vancouver]"; +const result1 = Temporal.TimeZone.from(timeZone); +assert.sameValue(result1.id, "America/Vancouver", "date-time + IANA annotation is the IANA time zone"); + +timeZone = "2021-08-19T17:30Z[America/Vancouver]"; +const result2 = Temporal.TimeZone.from(timeZone); +assert.sameValue(result2.id, "America/Vancouver", "date-time + Z + IANA annotation is the IANA time zone"); + +timeZone = "2021-08-19T17:30-07:00[America/Vancouver]"; +const result3 = Temporal.TimeZone.from(timeZone); +assert.sameValue(result3.id, "America/Vancouver", "date-time + offset + IANA annotation is the IANA time zone"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/iana-legacy-names.js b/js/src/tests/test262/intl402/Temporal/TimeZone/iana-legacy-names.js new file mode 100644 index 0000000000..150633f6ce --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/iana-legacy-names.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.timezone +description: IANA legacy names must be supported +features: [Temporal] +---*/ + +const legacyNames = [ + "Etc/GMT0", + "GMT0", + "GMT-0", + "GMT+0", + "EST5EDT", + "CST6CDT", + "MST7MDT", + "PST8PDT" +]; + +legacyNames.forEach((arg) => { + const tz = new Temporal.TimeZone(arg); + assert.sameValue(tz.toString(), arg, `"${arg}" does not match "${tz.toString()}" time zone identifier`); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/links-africa.js b/js/src/tests/test262/intl402/Temporal/TimeZone/links-africa.js new file mode 100644 index 0000000000..0e5ed598b1 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/links-africa.js @@ -0,0 +1,57 @@ +// |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.timezone +description: > + TimeZone constructor accepts link names as its input. +features: [Temporal] +---*/ + +const testCases = [ + "Africa/Accra", // Link Africa/Abidjan Africa/Accra # Ghana + "Africa/Bamako", // Link Africa/Abidjan Africa/Bamako # Mali + "Africa/Banjul", // Link Africa/Abidjan Africa/Banjul # The Gambia + "Africa/Conakry", // Link Africa/Abidjan Africa/Conakry # Guinea + "Africa/Dakar", // Link Africa/Abidjan Africa/Dakar # Senegal + "Africa/Freetown", // Link Africa/Abidjan Africa/Freetown # Sierra Leone + "Africa/Lome", // Link Africa/Abidjan Africa/Lome # Togo + "Africa/Nouakchott", // Link Africa/Abidjan Africa/Nouakchott # Mauritania + "Africa/Ouagadougou", // Link Africa/Abidjan Africa/Ouagadougou # Burkina Faso + "Atlantic/St_Helena", // Link Africa/Abidjan Atlantic/St_Helena # St Helena + "Africa/Addis_Ababa", // Link Africa/Nairobi Africa/Addis_Ababa # Ethiopia + "Africa/Asmara", // Link Africa/Nairobi Africa/Asmara # Eritrea + "Africa/Dar_es_Salaam", // Link Africa/Nairobi Africa/Dar_es_Salaam # Tanzania + "Africa/Djibouti", // Link Africa/Nairobi Africa/Djibouti + "Africa/Kampala", // Link Africa/Nairobi Africa/Kampala # Uganda + "Africa/Mogadishu", // Link Africa/Nairobi Africa/Mogadishu # Somalia + "Indian/Antananarivo", // Link Africa/Nairobi Indian/Antananarivo # Madagascar + "Indian/Comoro", // Link Africa/Nairobi Indian/Comoro + "Indian/Mayotte", // Link Africa/Nairobi Indian/Mayotte + "Africa/Blantyre", // Link Africa/Maputo Africa/Blantyre # Malawi + "Africa/Bujumbura", // Link Africa/Maputo Africa/Bujumbura # Burundi + "Africa/Gaborone", // Link Africa/Maputo Africa/Gaborone # Botswana + "Africa/Harare", // Link Africa/Maputo Africa/Harare # Zimbabwe + "Africa/Kigali", // Link Africa/Maputo Africa/Kigali # Rwanda + "Africa/Lubumbashi", // Link Africa/Maputo Africa/Lubumbashi # E Dem. Rep. of Congo + "Africa/Lusaka", // Link Africa/Maputo Africa/Lusaka # Zambia + "Africa/Bangui", // Link Africa/Lagos Africa/Bangui # Central African Republic + "Africa/Brazzaville", // Link Africa/Lagos Africa/Brazzaville # Rep. of the Congo + "Africa/Douala", // Link Africa/Lagos Africa/Douala # Cameroon + "Africa/Kinshasa", // Link Africa/Lagos Africa/Kinshasa # Dem. Rep. of the Congo (west) + "Africa/Libreville", // Link Africa/Lagos Africa/Libreville # Gabon + "Africa/Luanda", // Link Africa/Lagos Africa/Luanda # Angola + "Africa/Malabo", // Link Africa/Lagos Africa/Malabo # Equatorial Guinea + "Africa/Niamey", // Link Africa/Lagos Africa/Niamey # Niger + "Africa/Porto-Novo", // Link Africa/Lagos Africa/Porto-Novo # Benin + "Africa/Maseru", // Link Africa/Johannesburg Africa/Maseru # Lesotho + "Africa/Mbabane", // Link Africa/Johannesburg Africa/Mbabane # Eswatini +]; + +for (let id of testCases) { + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/links-asia.js b/js/src/tests/test262/intl402/Temporal/TimeZone/links-asia.js new file mode 100644 index 0000000000..80b5b3e62d --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/links-asia.js @@ -0,0 +1,28 @@ +// |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.timezone +description: > + TimeZone constructor accepts link names as its input. +features: [Temporal] +---*/ + +const testCases = [ + "Europe/Nicosia", // Link Asia/Nicosia Europe/Nicosia + "Asia/Bahrain", // Link Asia/Qatar Asia/Bahrain + "Antarctica/Syowa", // Link Asia/Riyadh Antarctica/Syowa + "Asia/Aden", // Link Asia/Riyadh Asia/Aden # Yemen + "Asia/Kuwait", // Link Asia/Riyadh Asia/Kuwait + "Asia/Phnom_Penh", // Link Asia/Bangkok Asia/Phnom_Penh # Cambodia + "Asia/Vientiane", // Link Asia/Bangkok Asia/Vientiane # Laos + "Asia/Muscat", // Link Asia/Dubai Asia/Muscat # Oman +]; + +for (let id of testCases) { + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/links-australasia.js b/js/src/tests/test262/intl402/Temporal/TimeZone/links-australasia.js new file mode 100644 index 0000000000..b24164d8f3 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/links-australasia.js @@ -0,0 +1,24 @@ +// |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.timezone +description: > + TimeZone constructor accepts link names as its input. +features: [Temporal] +---*/ + +const testCases = [ + "Pacific/Saipan", // Link Pacific/Guam Pacific/Saipan # N Mariana Is + "Antarctica/McMurdo", // Link Pacific/Auckland Antarctica/McMurdo + "Antarctica/DumontDUrville", // Link Pacific/Port_Moresby Antarctica/DumontDUrville + "Pacific/Midway", // Link Pacific/Pago_Pago Pacific/Midway # in US minor outlying islands +]; + +for (let id of testCases) { + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/links-backward.js b/js/src/tests/test262/intl402/Temporal/TimeZone/links-backward.js new file mode 100644 index 0000000000..afab22aa45 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/links-backward.js @@ -0,0 +1,143 @@ +// |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.timezone +description: > + TimeZone constructor accepts link names as its input. +features: [Temporal] +---*/ + +const testCases = [ + "Africa/Asmera", // Link Africa/Nairobi Africa/Asmera + "Africa/Timbuktu", // Link Africa/Abidjan Africa/Timbuktu + "America/Argentina/ComodRivadavia", // Link America/Argentina/Catamarca America/Argentina/ComodRivadavia + "America/Atka", // Link America/Adak America/Atka + "America/Buenos_Aires", // Link America/Argentina/Buenos_Aires America/Buenos_Aires + "America/Catamarca", // Link America/Argentina/Catamarca America/Catamarca + "America/Coral_Harbour", // Link America/Panama America/Coral_Harbour + "America/Cordoba", // Link America/Argentina/Cordoba America/Cordoba + "America/Ensenada", // Link America/Tijuana America/Ensenada + "America/Fort_Wayne", // Link America/Indiana/Indianapolis America/Fort_Wayne + "America/Godthab", // Link America/Nuuk America/Godthab + "America/Indianapolis", // Link America/Indiana/Indianapolis America/Indianapolis + "America/Jujuy", // Link America/Argentina/Jujuy America/Jujuy + "America/Knox_IN", // Link America/Indiana/Knox America/Knox_IN + "America/Louisville", // Link America/Kentucky/Louisville America/Louisville + "America/Mendoza", // Link America/Argentina/Mendoza America/Mendoza + "America/Montreal", // Link America/Toronto America/Montreal + "America/Porto_Acre", // Link America/Rio_Branco America/Porto_Acre + "America/Rosario", // Link America/Argentina/Cordoba America/Rosario + "America/Santa_Isabel", // Link America/Tijuana America/Santa_Isabel + "America/Shiprock", // Link America/Denver America/Shiprock + "America/Virgin", // Link America/Puerto_Rico America/Virgin + "Antarctica/South_Pole", // Link Pacific/Auckland Antarctica/South_Pole + "Asia/Ashkhabad", // Link Asia/Ashgabat Asia/Ashkhabad + "Asia/Calcutta", // Link Asia/Kolkata Asia/Calcutta + "Asia/Chongqing", // Link Asia/Shanghai Asia/Chongqing + "Asia/Chungking", // Link Asia/Shanghai Asia/Chungking + "Asia/Dacca", // Link Asia/Dhaka Asia/Dacca + "Asia/Harbin", // Link Asia/Shanghai Asia/Harbin + "Asia/Kashgar", // Link Asia/Urumqi Asia/Kashgar + "Asia/Katmandu", // Link Asia/Kathmandu Asia/Katmandu + "Asia/Macao", // Link Asia/Macau Asia/Macao + "Asia/Rangoon", // Link Asia/Yangon Asia/Rangoon + "Asia/Saigon", // Link Asia/Ho_Chi_Minh Asia/Saigon + "Asia/Tel_Aviv", // Link Asia/Jerusalem Asia/Tel_Aviv + "Asia/Thimbu", // Link Asia/Thimphu Asia/Thimbu + "Asia/Ujung_Pandang", // Link Asia/Makassar Asia/Ujung_Pandang + "Asia/Ulan_Bator", // Link Asia/Ulaanbaatar Asia/Ulan_Bator + "Atlantic/Faeroe", // Link Atlantic/Faroe Atlantic/Faeroe + "Atlantic/Jan_Mayen", // Link Europe/Oslo Atlantic/Jan_Mayen + "Australia/ACT", // Link Australia/Sydney Australia/ACT + "Australia/Canberra", // Link Australia/Sydney Australia/Canberra + "Australia/Currie", // Link Australia/Hobart Australia/Currie + "Australia/LHI", // Link Australia/Lord_Howe Australia/LHI + "Australia/NSW", // Link Australia/Sydney Australia/NSW + "Australia/North", // Link Australia/Darwin Australia/North + "Australia/Queensland", // Link Australia/Brisbane Australia/Queensland + "Australia/South", // Link Australia/Adelaide Australia/South + "Australia/Tasmania", // Link Australia/Hobart Australia/Tasmania + "Australia/Victoria", // Link Australia/Melbourne Australia/Victoria + "Australia/West", // Link Australia/Perth Australia/West + "Australia/Yancowinna", // Link Australia/Broken_Hill Australia/Yancowinna + "Brazil/Acre", // Link America/Rio_Branco Brazil/Acre + "Brazil/DeNoronha", // Link America/Noronha Brazil/DeNoronha + "Brazil/East", // Link America/Sao_Paulo Brazil/East + "Brazil/West", // Link America/Manaus Brazil/West + "Canada/Atlantic", // Link America/Halifax Canada/Atlantic + "Canada/Central", // Link America/Winnipeg Canada/Central + "Canada/Eastern", // Link America/Toronto Canada/Eastern + "Canada/Mountain", // Link America/Edmonton Canada/Mountain + "Canada/Newfoundland", // Link America/St_Johns Canada/Newfoundland + "Canada/Pacific", // Link America/Vancouver Canada/Pacific + "Canada/Saskatchewan", // Link America/Regina Canada/Saskatchewan + "Canada/Yukon", // Link America/Whitehorse Canada/Yukon + "Chile/Continental", // Link America/Santiago Chile/Continental + "Chile/EasterIsland", // Link Pacific/Easter Chile/EasterIsland + "Cuba", // Link America/Havana Cuba + "Egypt", // Link Africa/Cairo Egypt + "Eire", // Link Europe/Dublin Eire + "Etc/UCT", // Link Etc/UTC Etc/UCT + "Europe/Belfast", // Link Europe/London Europe/Belfast + "Europe/Kiev", // Link Europe/Kyiv Europe/Kiev + "Europe/Tiraspol", // Link Europe/Chisinau Europe/Tiraspol + "GB", // Link Europe/London GB + "GB-Eire", // Link Europe/London GB-Eire + "GMT+0", // Link Etc/GMT GMT+0 + "GMT-0", // Link Etc/GMT GMT-0 + "GMT0", // Link Etc/GMT GMT0 + "Greenwich", // Link Etc/GMT Greenwich + "Hongkong", // Link Asia/Hong_Kong Hongkong + "Iceland", // Link Atlantic/Reykjavik Iceland + "Iran", // Link Asia/Tehran Iran + "Israel", // Link Asia/Jerusalem Israel + "Jamaica", // Link America/Jamaica Jamaica + "Japan", // Link Asia/Tokyo Japan + "Kwajalein", // Link Pacific/Kwajalein Kwajalein + "Libya", // Link Africa/Tripoli Libya + "Mexico/BajaNorte", // Link America/Tijuana Mexico/BajaNorte + "Mexico/BajaSur", // Link America/Mazatlan Mexico/BajaSur + "Mexico/General", // Link America/Mexico_City Mexico/General + "NZ", // Link Pacific/Auckland NZ + "NZ-CHAT", // Link Pacific/Chatham NZ-CHAT + "Navajo", // Link America/Denver Navajo + "PRC", // Link Asia/Shanghai PRC + "Pacific/Enderbury", // Link Pacific/Kanton Pacific/Enderbury + "Pacific/Johnston", // Link Pacific/Honolulu Pacific/Johnston + "Pacific/Ponape", // Link Pacific/Pohnpei Pacific/Ponape + "Pacific/Samoa", // Link Pacific/Pago_Pago Pacific/Samoa + "Pacific/Truk", // Link Pacific/Chuuk Pacific/Truk + "Pacific/Yap", // Link Pacific/Chuuk Pacific/Yap + "Poland", // Link Europe/Warsaw Poland + "Portugal", // Link Europe/Lisbon Portugal + "ROC", // Link Asia/Taipei ROC + "ROK", // Link Asia/Seoul ROK + "Singapore", // Link Asia/Singapore Singapore + "Turkey", // Link Europe/Istanbul Turkey + "UCT", // Link Etc/UTC UCT + "US/Alaska", // Link America/Anchorage US/Alaska + "US/Aleutian", // Link America/Adak US/Aleutian + "US/Arizona", // Link America/Phoenix US/Arizona + "US/Central", // Link America/Chicago US/Central + "US/East-Indiana", // Link America/Indiana/Indianapolis US/East-Indiana + "US/Eastern", // Link America/New_York US/Eastern + "US/Hawaii", // Link Pacific/Honolulu US/Hawaii + "US/Indiana-Starke", // Link America/Indiana/Knox US/Indiana-Starke + "US/Michigan", // Link America/Detroit US/Michigan + "US/Mountain", // Link America/Denver US/Mountain + "US/Pacific", // Link America/Los_Angeles US/Pacific + "US/Samoa", // Link Pacific/Pago_Pago US/Samoa + "UTC", // Link Etc/UTC UTC + "Universal", // Link Etc/UTC Universal + "W-SU", // Link Europe/Moscow W-SU + "Zulu", // Link Etc/UTC Zulu +]; + +for (let id of testCases) { + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/links-backzone.js b/js/src/tests/test262/intl402/Temporal/TimeZone/links-backzone.js new file mode 100644 index 0000000000..90eb8dca19 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/links-backzone.js @@ -0,0 +1,28 @@ +// |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.timezone +description: > + TimeZone constructor accepts link names as its input. +features: [Temporal] +---*/ + +const testCases = [ + "Africa/Asmera", // Link Africa/Asmara Africa/Asmera + "America/Kralendijk", // Link America/Curacao America/Kralendijk + "America/Lower_Princes", // Link America/Curacao America/Lower_Princes + "America/Marigot", // Link America/Port_of_Spain America/Marigot + "America/St_Barthelemy", // Link America/Port_of_Spain America/St_Barthelemy + "America/Virgin", // Link America/St_Thomas America/Virgin + "Antarctica/South_Pole", // Link Antarctica/McMurdo Antarctica/South_Pole + "Asia/Chungking", // Link Asia/Chongqing Asia/Chungking +]; + +for (let id of testCases) { + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/links-etcetera.js b/js/src/tests/test262/intl402/Temporal/TimeZone/links-etcetera.js new file mode 100644 index 0000000000..e67c2bea7a --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/links-etcetera.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.timezone +description: > + TimeZone constructor accepts link names as its input. +features: [Temporal] +---*/ + +const testCases = [ + "GMT", // Link Etc/GMT GMT + "Etc/Universal", // Link Etc/UTC Etc/Universal + "Etc/Zulu", // Link Etc/UTC Etc/Zulu + "Etc/Greenwich", // Link Etc/GMT Etc/Greenwich + "Etc/GMT-0", // Link Etc/GMT Etc/GMT-0 + "Etc/GMT+0", // Link Etc/GMT Etc/GMT+0 + "Etc/GMT0", // Link Etc/GMT Etc/GMT0 +]; + +for (let id of testCases) { + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/links-europe.js b/js/src/tests/test262/intl402/Temporal/TimeZone/links-europe.js new file mode 100644 index 0000000000..81149e8d12 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/links-europe.js @@ -0,0 +1,36 @@ +// |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.timezone +description: > + TimeZone constructor accepts link names as its input. +features: [Temporal] +---*/ + +const testCases = [ + "Europe/Jersey", // Link Europe/London Europe/Jersey + "Europe/Guernsey", // Link Europe/London Europe/Guernsey + "Europe/Isle_of_Man", // Link Europe/London Europe/Isle_of_Man + "Europe/Mariehamn", // Link Europe/Helsinki Europe/Mariehamn + "Europe/Busingen", // Link Europe/Zurich Europe/Busingen + "Europe/Vatican", // Link Europe/Rome Europe/Vatican + "Europe/San_Marino", // Link Europe/Rome Europe/San_Marino + "Europe/Vaduz", // Link Europe/Zurich Europe/Vaduz + "Arctic/Longyearbyen", // Link Europe/Oslo Arctic/Longyearbyen + "Europe/Ljubljana", // Link Europe/Belgrade Europe/Ljubljana # Slovenia + "Europe/Podgorica", // Link Europe/Belgrade Europe/Podgorica # Montenegro + "Europe/Sarajevo", // Link Europe/Belgrade Europe/Sarajevo # Bosnia and Herzegovina + "Europe/Skopje", // Link Europe/Belgrade Europe/Skopje # North Macedonia + "Europe/Zagreb", // Link Europe/Belgrade Europe/Zagreb # Croatia + "Europe/Bratislava", // Link Europe/Prague Europe/Bratislava + "Asia/Istanbul", // Link Europe/Istanbul Asia/Istanbul # Istanbul is in both continents. +]; + +for (let id of testCases) { + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/links-northamerica.js b/js/src/tests/test262/intl402/Temporal/TimeZone/links-northamerica.js new file mode 100644 index 0000000000..c1b24fe974 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/links-northamerica.js @@ -0,0 +1,43 @@ +// |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.timezone +description: > + TimeZone constructor accepts link names as its input. +features: [Temporal] +---*/ + +const testCases = [ + "America/Creston", // Link America/Phoenix America/Creston + "America/Nassau", // Link America/Toronto America/Nassau + "America/Atikokan", // Link America/Panama America/Atikokan + "America/Cayman", // Link America/Panama America/Cayman + "America/Anguilla", // Link America/Puerto_Rico America/Anguilla + "America/Antigua", // Link America/Puerto_Rico America/Antigua + "America/Aruba", // Link America/Puerto_Rico America/Aruba + "America/Curacao", // Link America/Puerto_Rico America/Curacao + "America/Blanc-Sablon", // Link America/Puerto_Rico America/Blanc-Sablon # Quebec (Lower North Shore) + "America/Dominica", // Link America/Puerto_Rico America/Dominica + "America/Grenada", // Link America/Puerto_Rico America/Grenada + "America/Guadeloupe", // Link America/Puerto_Rico America/Guadeloupe + "America/Kralendijk", // Link America/Puerto_Rico America/Kralendijk # Caribbean Netherlands + "America/Lower_Princes", // Link America/Puerto_Rico America/Lower_Princes # Sint Maarten + "America/Marigot", // Link America/Puerto_Rico America/Marigot # St Martin (French part) + "America/Montserrat", // Link America/Puerto_Rico America/Montserrat + "America/Port_of_Spain", // Link America/Puerto_Rico America/Port_of_Spain # Trinidad & Tobago + "America/St_Barthelemy", // Link America/Puerto_Rico America/St_Barthelemy # St Barthélemy + "America/St_Kitts", // Link America/Puerto_Rico America/St_Kitts # St Kitts & Nevis + "America/St_Lucia", // Link America/Puerto_Rico America/St_Lucia + "America/St_Thomas", // Link America/Puerto_Rico America/St_Thomas # Virgin Islands (US) + "America/St_Vincent", // Link America/Puerto_Rico America/St_Vincent + "America/Tortola", // Link America/Puerto_Rico America/Tortola # Virgin Islands (UK) +]; + +for (let id of testCases) { + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/non-canonical-utc.js b/js/src/tests/test262/intl402/Temporal/TimeZone/non-canonical-utc.js new file mode 100644 index 0000000000..468ecd2afd --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/non-canonical-utc.js @@ -0,0 +1,30 @@ +// |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.timezone +description: > + TimeZone constructor canonicalises its input. +features: [Temporal] +---*/ + +const testCases = [ + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT-0", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", +]; + +for (let id of testCases) { + let tz = new Temporal.TimeZone(id); + + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/argument-object.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/argument-object.js new file mode 100644 index 0000000000..551550d21a --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/argument-object.js @@ -0,0 +1,84 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Justin Grant. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.timezone.from +description: Objects with IANA IDs are compared case-insensitively with their canonical IDs +features: [Temporal] +---*/ + +class CustomTimeZone extends Temporal.TimeZone { + constructor(id) { + super("UTC"); + this._id = id; + } + get id() { + return this._id; + } +} + +const classInstancesIANA = [ + new Temporal.TimeZone("Asia/Calcutta"), + new CustomTimeZone("Asia/Calcutta"), + new Temporal.TimeZone("Asia/Kolkata"), + new CustomTimeZone("Asia/Kolkata"), + new CustomTimeZone("ASIA/calcutta"), + new CustomTimeZone("Asia/KOLKATA") +]; + +const plainObjectsIANA = [ + { id: "Asia/Calcutta", getPossibleInstantsFor: null, getOffsetNanosecondsFor: null }, + { id: "Asia/Kolkata", getPossibleInstantsFor: null, getOffsetNanosecondsFor: null }, + { id: "ASIA/calcutta", getPossibleInstantsFor: null, getOffsetNanosecondsFor: null }, + { id: "asia/kolkatA", getPossibleInstantsFor: null, getOffsetNanosecondsFor: null } +]; + +for (const object1 of classInstancesIANA) { + for (const object2 of classInstancesIANA) { + assert.sameValue(object1.equals(object2), true, `Receiver ${object1.id} should not equal argument ${object2.id}`); + } + for (const object2 of plainObjectsIANA) { + assert.sameValue(object1.equals(object2), true, `Receiver ${object2.id} should not equal argument ${object1.id}`); + } +} + +const classInstancesIANADifferentCanonical = [ + new Temporal.TimeZone("Asia/Colombo"), + new CustomTimeZone("Asia/Colombo"), + new Temporal.TimeZone("ASIA/colombo"), + new CustomTimeZone("ASIA/colombo") +]; + +for (const object1 of classInstancesIANADifferentCanonical) { + for (const object2 of classInstancesIANA) { + assert.sameValue(object1.equals(object2), false, `Receiver ${object1.id} should not equal argument ${object2.id}`); + assert.sameValue(object2.equals(object1), false, `Receiver ${object2.id} should not equal argument ${object1.id}`); + } + for (const object2 of plainObjectsIANA) { + assert.sameValue(object1.equals(object2), false, `Receiver ${object1.id} should not equal argument ${object2.id}`); + assert.sameValue( + object1.equals(object2.id), + false, + `Receiver ${object1.id} should not equal argument ${object2.id}` + ); + } +} + +const classInstancesCustomNotIANA = [new CustomTimeZone("Moon/Cheese")]; +for (const object1 of classInstancesCustomNotIANA) { + for (const object2 of classInstancesIANA) { + assert.sameValue(object1.equals(object2), false, `Receiver ${object1.id} should not equal argument ${object2.id}`); + assert.sameValue(object2.equals(object1), false, `Receiver ${object2.id} should not equal argument ${object1.id}`); + } + for (const object2 of plainObjectsIANA) { + assert.sameValue(object1.equals(object2), false, `Receiver ${object1.id} should not equal argument ${object2.id}`); + assert.sameValue( + object1.equals(object2.id), + false, + `Receiver ${object1.id} should not equal argument ${object2.id}` + ); + } +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/argument-valid.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/argument-valid.js new file mode 100644 index 0000000000..836a3eff24 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/argument-valid.js @@ -0,0 +1,29 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Justin Grant. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.timezone.from +description: Built-in time zones are parsed correctly out of valid strings +features: [Temporal] +---*/ + +const valids = [ + ["Africa/CAIRO", "Africa/Cairo"], + ["Asia/Ulan_Bator", "Asia/Ulaanbaatar"], + ["etc/gmt", "Etc/GMT"], + ["1994-11-05T08:15:30-05:00[America/New_York]", "America/New_York"], + ["1994-11-05T08:15:30+05:30[Asia/Calcutta]", "Asia/Calcutta"], + ["1994-11-05T08:15:30+05:30[Asia/Calcutta]", "Asia/Kolkata"], + ["1994-11-05T08:15:30+05:30[Asia/Kolkata]", "Asia/Calcutta"], + ["1994-11-05T08:15:30+05:30[Asia/Kolkata]", "Asia/Kolkata"], +]; + +for (const [valid, canonical = valid] of valids) { + const tzValid = Temporal.TimeZone.from(canonical); + const tzCanonical = Temporal.TimeZone.from(canonical); + assert.sameValue(tzValid.equals(tzCanonical), true); + assert.sameValue(tzCanonical.equals(tzValid), true); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/canonical-iana-names.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/canonical-iana-names.js new file mode 100644 index 0000000000..6be500a228 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/canonical-iana-names.js @@ -0,0 +1,50 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Justin Grant. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.timezone.from +description: Canonicalizes to evaluate time zone equality +features: [Temporal] +---*/ + +const neverEqual = new Temporal.TimeZone('Asia/Tokyo'); +const zdt = new Temporal.ZonedDateTime(0n, 'America/Los_Angeles'); +const ids = [ + ['America/Atka', 'America/Adak'], + ['America/Knox_IN', 'America/Indiana/Knox'], + ['Asia/Ashkhabad', 'Asia/Ashgabat'], + ['Asia/Dacca', 'Asia/Dhaka'], + ['Asia/Istanbul', 'Europe/Istanbul'], + ['Asia/Macao', 'Asia/Macau'], + ['Asia/Thimbu', 'Asia/Thimphu'], + ['Asia/Ujung_Pandang', 'Asia/Makassar'], + ['Asia/Ulan_Bator', 'Asia/Ulaanbaatar'] +]; + +for (const [identifier, primaryIdentifier] of ids) { + const tz1 = new Temporal.TimeZone(identifier); + const tz2 = new Temporal.TimeZone(primaryIdentifier); + + // compare objects + assert.sameValue(tz1.equals(tz2), true); + assert.sameValue(tz2.equals(tz1), true); + assert.sameValue(tz1.equals(neverEqual), false); + + // compare string IDs + assert.sameValue(tz1.equals(tz2.id), true); + assert.sameValue(tz2.equals(tz1.id), true); + assert.sameValue(tz1.equals(neverEqual.id), false); + + // compare ZonedDateTime instances + assert.sameValue(tz1.equals(zdt.withTimeZone(tz2)), true); + assert.sameValue(tz2.equals(zdt.withTimeZone(tz1)), true); + assert.sameValue(tz1.equals(zdt.withTimeZone(neverEqual)), false); + + // compare IXDTF strings + assert.sameValue(tz1.equals(zdt.withTimeZone(tz2).toString()), true); + assert.sameValue(tz2.equals(zdt.withTimeZone(tz1).toString()), true); + assert.sameValue(tz1.equals(zdt.withTimeZone(neverEqual).toString()), false); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/canonical-not-equal.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/canonical-not-equal.js new file mode 100644 index 0000000000..f37c49054d --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/canonical-not-equal.js @@ -0,0 +1,28 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Justin Grant. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.timezone.from +description: Canonical time zone identifiers are never equal to each other +features: [Temporal] +---*/ + +// supportedValuesOf only returns canonical IDs +const ids = Intl.supportedValuesOf("timeZone"); + +const forEachDistinctPair = (array, func) => { + for (let i = 0; i < array.length; i++) { + for (let j = i + 1; j < array.length; j++) { + func(array[i], array[j]); + } + } +}; + +forEachDistinctPair(ids, (id1, id2) => { + const tz = new Temporal.TimeZone(id1); + assert.sameValue(tz.equals(id2), false); +}) + + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/offset-and-iana.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/offset-and-iana.js new file mode 100644 index 0000000000..af9579cf53 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/offset-and-iana.js @@ -0,0 +1,25 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Justin Grant. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.timezone +description: Offset string time zones compare as expected +features: [Temporal] +---*/ + +const zdt = new Temporal.ZonedDateTime(0n, "America/Los_Angeles"); +const otz1 = new Temporal.TimeZone("+05:30"); +const otz2 = new Temporal.TimeZone("+0530"); +const tz = new Temporal.TimeZone("Asia/Kolkata"); +assert.sameValue(otz1.equals(otz2), true); +assert.sameValue(otz2.equals(otz1), true); +assert.sameValue(otz1.equals("+05:30"), true); +assert.sameValue(otz1.equals(zdt.withTimeZone(otz2)), true); +assert.sameValue(otz1.equals(zdt.withTimeZone(otz2).toString()), true); +assert.sameValue(otz1.equals(tz), false); +assert.sameValue(otz1.equals("Asia/Kolkata"), false); +assert.sameValue(otz1.equals(zdt.withTimeZone(tz)), false); +assert.sameValue(otz1.equals(zdt.withTimeZone(tz).toString()), false); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/shell.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/timezone-case-insensitive.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/timezone-case-insensitive.js new file mode 100644 index 0000000000..f586342153 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/equals/timezone-case-insensitive.js @@ -0,0 +1,627 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Justin Grant. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.timezone.from +description: Time zone names are compared case-insensitively +features: [Temporal] +---*/ + +const timeZoneIdentifiers = [ + // IANA TZDB Zone names + 'Africa/Abidjan', + 'Africa/Algiers', + 'Africa/Bissau', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/El_Aaiun', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Khartoum', + 'Africa/Lagos', + 'Africa/Maputo', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Sao_Tome', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Araguaina', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Catamarca', + 'America/Argentina/Cordoba', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/Mendoza', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Asuncion', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Cayenne', + 'America/Chicago', + 'America/Chihuahua', + // 'America/Ciudad_Juarez' // uncomment after Node supports this ID added in TZDB 2022g + 'America/Costa_Rica', + 'America/Cuiaba', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Fort_Nelson', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Indianapolis', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Juneau', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/New_York', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Nuuk', + 'America/Ojinaga', + 'America/Panama', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port-au-Prince', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Sitka', + 'America/St_Johns', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Tijuana', + 'America/Toronto', + 'America/Vancouver', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/Troll', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Colombo', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Hebron', + 'Asia/Ho_Chi_Minh', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Kathmandu', + 'Asia/Khandyga', + 'Asia/Kolkata', + 'Asia/Krasnoyarsk', + 'Asia/Kuching', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Nicosia', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qostanay', + 'Asia/Qyzylorda', + 'Asia/Riyadh', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ulaanbaatar', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yangon', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faroe', + 'Atlantic/Madeira', + 'Atlantic/South_Georgia', + 'Atlantic/Stanley', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/Perth', + 'Australia/Sydney', + 'CET', + 'CST6CDT', + 'EET', + 'EST', + 'EST5EDT', + 'Etc/GMT', + 'Etc/GMT+1', + 'Etc/GMT+10', + 'Etc/GMT+11', + 'Etc/GMT+12', + 'Etc/GMT+2', + 'Etc/GMT+3', + 'Etc/GMT+4', + 'Etc/GMT+5', + 'Etc/GMT+6', + 'Etc/GMT+7', + 'Etc/GMT+8', + 'Etc/GMT+9', + 'Etc/GMT-1', + 'Etc/GMT-10', + 'Etc/GMT-11', + 'Etc/GMT-12', + 'Etc/GMT-13', + 'Etc/GMT-14', + 'Etc/GMT-2', + 'Etc/GMT-3', + 'Etc/GMT-4', + 'Etc/GMT-5', + 'Etc/GMT-6', + 'Etc/GMT-7', + 'Etc/GMT-8', + 'Etc/GMT-9', + 'Etc/UTC', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Chisinau', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Helsinki', + 'Europe/Istanbul', + 'Europe/Kaliningrad', + 'Europe/Kirov', + 'Europe/Kyiv', + 'Europe/Lisbon', + 'Europe/London', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Minsk', + 'Europe/Moscow', + 'Europe/Paris', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Sofia', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Ulyanovsk', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zurich', + 'HST', + 'Indian/Chagos', + 'Indian/Maldives', + 'Indian/Mauritius', + 'MET', + 'MST', + 'MST7MDT', + 'PST8PDT', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Kanton', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Marquesas', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + + // IANA TZDB Link names + 'WET', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Asmara', + 'Africa/Asmera', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Kampala', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Timbuktu', + 'America/Anguilla', + 'America/Antigua', + 'America/Argentina/ComodRivadavia', + 'America/Aruba', + 'America/Atikokan', + 'America/Atka', + 'America/Blanc-Sablon', + 'America/Buenos_Aires', + 'America/Catamarca', + 'America/Cayman', + 'America/Coral_Harbour', + 'America/Cordoba', + 'America/Creston', + 'America/Curacao', + 'America/Dominica', + 'America/Ensenada', + 'America/Fort_Wayne', + 'America/Godthab', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Indianapolis', + 'America/Jujuy', + 'America/Knox_IN', + 'America/Kralendijk', + 'America/Louisville', + 'America/Lower_Princes', + 'America/Marigot', + 'America/Mendoza', + 'America/Montreal', + 'America/Montserrat', + 'America/Nassau', + 'America/Nipigon', + 'America/Pangnirtung', + 'America/Port_of_Spain', + 'America/Porto_Acre', + 'America/Rainy_River', + 'America/Rosario', + 'America/Santa_Isabel', + 'America/Shiprock', + 'America/St_Barthelemy', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Thunder_Bay', + 'America/Tortola', + 'America/Virgin', + 'Antarctica/DumontDUrville', + 'Antarctica/McMurdo', + 'Antarctica/South_Pole', + 'Antarctica/Syowa', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Ashkhabad', + 'Asia/Bahrain', + 'Asia/Brunei', + 'Asia/Calcutta', + 'Asia/Chongqing', + 'Asia/Chungking', + 'Asia/Dacca', + 'Asia/Harbin', + 'Asia/Istanbul', + 'Asia/Kashgar', + 'Asia/Katmandu', + 'Asia/Kuala_Lumpur', + 'Asia/Kuwait', + 'Asia/Macao', + 'Asia/Muscat', + 'Asia/Phnom_Penh', + 'Asia/Rangoon', + 'Asia/Saigon', + 'Asia/Tel_Aviv', + 'Asia/Thimbu', + 'Asia/Ujung_Pandang', + 'Asia/Ulan_Bator', + 'Asia/Vientiane', + 'Atlantic/Faeroe', + 'Atlantic/Jan_Mayen', + 'Atlantic/Reykjavik', + 'Atlantic/St_Helena', + 'Australia/ACT', + 'Australia/Canberra', + 'Australia/Currie', + 'Australia/LHI', + 'Australia/NSW', + 'Australia/North', + 'Australia/Queensland', + 'Australia/South', + 'Australia/Tasmania', + 'Australia/Victoria', + 'Australia/West', + 'Australia/Yancowinna', + 'Brazil/Acre', + 'Brazil/DeNoronha', + 'Brazil/East', + 'Brazil/West', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Canada/Saskatchewan', + 'Canada/Yukon', + 'Chile/Continental', + 'Chile/EasterIsland', + 'Cuba', + 'Egypt', + 'Eire', + 'Etc/GMT+0', + 'Etc/GMT-0', + 'Etc/GMT0', + 'Etc/Greenwich', + 'Etc/UCT', + 'Etc/Universal', + 'Etc/Zulu', + 'Europe/Amsterdam', + 'Europe/Belfast', + 'Europe/Bratislava', + 'Europe/Busingen', + 'Europe/Copenhagen', + 'Europe/Guernsey', + 'Europe/Isle_of_Man', + 'Europe/Jersey', + 'Europe/Kiev', + 'Europe/Ljubljana', + 'Europe/Luxembourg', + 'Europe/Mariehamn', + 'Europe/Monaco', + 'Europe/Nicosia', + 'Europe/Oslo', + 'Europe/Podgorica', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Skopje', + 'Europe/Stockholm', + 'Europe/Tiraspol', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'GB', + 'GB-Eire', + 'GMT', + 'GMT+0', + 'GMT-0', + 'GMT0', + 'Greenwich', + 'Hongkong', + 'Iceland', + 'Indian/Antananarivo', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Mayotte', + 'Indian/Reunion', + 'Iran', + 'Israel', + 'Jamaica', + 'Japan', + 'Kwajalein', + 'Libya', + 'Mexico/BajaNorte', + 'Mexico/BajaSur', + 'Mexico/General', + 'NZ', + 'NZ-CHAT', + 'Navajo', + 'PRC', + 'Pacific/Chuuk', + 'Pacific/Enderbury', + 'Pacific/Funafuti', + 'Pacific/Johnston', + 'Pacific/Majuro', + 'Pacific/Midway', + 'Pacific/Pohnpei', + 'Pacific/Ponape', + 'Pacific/Saipan', + 'Pacific/Samoa', + 'Pacific/Truk', + 'Pacific/Wake', + 'Pacific/Wallis', + 'Pacific/Yap', + 'Poland', + 'Portugal', + 'ROC', + 'ROK', + 'Singapore', + 'Turkey', + 'UCT', + 'US/Alaska', + 'US/Aleutian', + 'US/Arizona', + 'US/Central', + 'US/East-Indiana', + 'US/Eastern', + 'US/Hawaii', + 'US/Indiana-Starke', + 'US/Michigan', + 'US/Mountain', + 'US/Pacific', + 'US/Samoa', + 'UTC', + 'Universal', + 'W-SU', + 'Zulu' +]; + +// We want to test all available named time zone identifiers (both primary and non-primary), +// but no ECMAScript built-in API exposes that list. So we use a union of two sources: +// 1. A hard-coded list of Zone and Link identifiers from the 2022g version of IANA TZDB. +// 2. Canonical IDs exposed by Intl.supportedValuesOf('timeZone'), which ensures that IDs +// added to TZDB later than 2022g will be tested. (New IDs are almost always added as primary.) +const ids = [...new Set([...timeZoneIdentifiers, ...Intl.supportedValuesOf('timeZone')])]; +for (const id of ids) { + const lower = id.toLowerCase(); + const upper = id.toUpperCase(); + const tz = new Temporal.TimeZone(id); + assert.sameValue(tz.equals(upper), true, `Time zone "${id}" compared to string "${upper}"`); + assert.sameValue(tz.equals(lower), true, `Time zone "${id}" compared to string "${lower}"`); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/infinity-throws-rangeerror.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/infinity-throws-rangeerror.js new file mode 100644 index 0000000000..0c644cf356 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/infinity-throws-rangeerror.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. + +/*--- +description: Throws if eraYear in the property bag is Infinity or -Infinity +esid: sec-temporal.timezone.prototype.getinstantfor +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +const instance = new Temporal.TimeZone("UTC"); +const base = { era: "ad", month: 5, day: 2, hour: 15, calendar: "gregory" }; + +[Infinity, -Infinity].forEach((inf) => { + assert.throws(RangeError, () => instance.getInstantFor({ ...base, eraYear: inf }), `eraYear property cannot be ${inf}`); + + const calls = []; + const obj = TemporalHelpers.toPrimitiveObserver(calls, inf, "eraYear"); + assert.throws(RangeError, () => instance.getInstantFor({ ...base, eraYear: obj })); + assert.compareArray(calls, ["get eraYear.valueOf", "call eraYear.valueOf"], "it fails after fetching the primitive value"); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/shell.js new file mode 100644 index 0000000000..60f74c2518 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getInstantFor/shell.js @@ -0,0 +1,2158 @@ +// GENERATED, DO NOT EDIT +// file: temporalHelpers.js +// Copyright (C) 2021 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + This defines helper objects and functions for testing Temporal. +defines: [TemporalHelpers] +features: [Symbol.species, Symbol.iterator, Temporal] +---*/ + +const ASCII_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/u; + +function formatPropertyName(propertyKey, objectName = "") { + switch (typeof propertyKey) { + case "symbol": + if (Symbol.keyFor(propertyKey) !== undefined) { + return `${objectName}[Symbol.for('${Symbol.keyFor(propertyKey)}')]`; + } else if (propertyKey.description.startsWith('Symbol.')) { + return `${objectName}[${propertyKey.description}]`; + } else { + return `${objectName}[Symbol('${propertyKey.description}')]` + } + case "string": + if (propertyKey !== String(Number(propertyKey))) { + if (ASCII_IDENTIFIER.test(propertyKey)) { + return objectName ? `${objectName}.${propertyKey}` : propertyKey; + } + return `${objectName}['${propertyKey.replace(/'/g, "\\'")}']` + } + // fall through + default: + // integer or string integer-index + return `${objectName}[${propertyKey}]`; + } +} + +const SKIP_SYMBOL = Symbol("Skip"); + +var TemporalHelpers = { + /* + * Codes and maximum lengths of months in the ISO 8601 calendar. + */ + ISOMonths: [ + { month: 1, monthCode: "M01", daysInMonth: 31 }, + { month: 2, monthCode: "M02", daysInMonth: 29 }, + { month: 3, monthCode: "M03", daysInMonth: 31 }, + { month: 4, monthCode: "M04", daysInMonth: 30 }, + { month: 5, monthCode: "M05", daysInMonth: 31 }, + { month: 6, monthCode: "M06", daysInMonth: 30 }, + { month: 7, monthCode: "M07", daysInMonth: 31 }, + { month: 8, monthCode: "M08", daysInMonth: 31 }, + { month: 9, monthCode: "M09", daysInMonth: 30 }, + { month: 10, monthCode: "M10", daysInMonth: 31 }, + { month: 11, monthCode: "M11", daysInMonth: 30 }, + { month: 12, monthCode: "M12", daysInMonth: 31 } + ], + + /* + * assertDuration(duration, years, ..., nanoseconds[, description]): + * + * Shorthand for asserting that each field of a Temporal.Duration is equal to + * an expected value. + */ + assertDuration(duration, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(duration instanceof Temporal.Duration, `${prefix}instanceof`); + assert.sameValue(duration.years, years, `${prefix}years result:`); + assert.sameValue(duration.months, months, `${prefix}months result:`); + assert.sameValue(duration.weeks, weeks, `${prefix}weeks result:`); + assert.sameValue(duration.days, days, `${prefix}days result:`); + assert.sameValue(duration.hours, hours, `${prefix}hours result:`); + assert.sameValue(duration.minutes, minutes, `${prefix}minutes result:`); + assert.sameValue(duration.seconds, seconds, `${prefix}seconds result:`); + assert.sameValue(duration.milliseconds, milliseconds, `${prefix}milliseconds result:`); + assert.sameValue(duration.microseconds, microseconds, `${prefix}microseconds result:`); + assert.sameValue(duration.nanoseconds, nanoseconds, `${prefix}nanoseconds result`); + }, + + /* + * assertDateDuration(duration, years, months, weeks, days, [, description]): + * + * Shorthand for asserting that each date field of a Temporal.Duration is + * equal to an expected value. + */ + assertDateDuration(duration, years, months, weeks, days, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(duration instanceof Temporal.Duration, `${prefix}instanceof`); + assert.sameValue(duration.years, years, `${prefix}years result:`); + assert.sameValue(duration.months, months, `${prefix}months result:`); + assert.sameValue(duration.weeks, weeks, `${prefix}weeks result:`); + assert.sameValue(duration.days, days, `${prefix}days result:`); + assert.sameValue(duration.hours, 0, `${prefix}hours result should be zero:`); + assert.sameValue(duration.minutes, 0, `${prefix}minutes result should be zero:`); + assert.sameValue(duration.seconds, 0, `${prefix}seconds result should be zero:`); + assert.sameValue(duration.milliseconds, 0, `${prefix}milliseconds result should be zero:`); + assert.sameValue(duration.microseconds, 0, `${prefix}microseconds result should be zero:`); + assert.sameValue(duration.nanoseconds, 0, `${prefix}nanoseconds result should be zero:`); + }, + + /* + * assertDurationsEqual(actual, expected[, description]): + * + * Shorthand for asserting that each field of a Temporal.Duration is equal to + * the corresponding field in another Temporal.Duration. + */ + assertDurationsEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.Duration, `${prefix}expected value should be a Temporal.Duration`); + TemporalHelpers.assertDuration(actual, expected.years, expected.months, expected.weeks, expected.days, expected.hours, expected.minutes, expected.seconds, expected.milliseconds, expected.microseconds, expected.nanoseconds, description); + }, + + /* + * assertInstantsEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.Instants are of the correct type + * and equal according to their equals() methods. + */ + assertInstantsEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.Instant, `${prefix}expected value should be a Temporal.Instant`); + assert(actual instanceof Temporal.Instant, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + }, + + /* + * assertPlainDate(date, year, ..., nanosecond[, description[, era, eraYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainDate is equal to + * an expected value. (Except the `calendar` property, since callers may want + * to assert either object equality with an object they put in there, or the + * value of date.calendarId.) + */ + assertPlainDate(date, year, month, monthCode, day, description = "", era = undefined, eraYear = undefined) { + const prefix = description ? `${description}: ` : ""; + assert(date instanceof Temporal.PlainDate, `${prefix}instanceof`); + assert.sameValue(date.era, era, `${prefix}era result:`); + assert.sameValue(date.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(date.year, year, `${prefix}year result:`); + assert.sameValue(date.month, month, `${prefix}month result:`); + assert.sameValue(date.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(date.day, day, `${prefix}day result:`); + }, + + /* + * assertPlainDateTime(datetime, year, ..., nanosecond[, description[, era, eraYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainDateTime is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of datetime.calendarId.) + */ + assertPlainDateTime(datetime, year, month, monthCode, day, hour, minute, second, millisecond, microsecond, nanosecond, description = "", era = undefined, eraYear = undefined) { + const prefix = description ? `${description}: ` : ""; + assert(datetime instanceof Temporal.PlainDateTime, `${prefix}instanceof`); + assert.sameValue(datetime.era, era, `${prefix}era result:`); + assert.sameValue(datetime.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(datetime.year, year, `${prefix}year result:`); + assert.sameValue(datetime.month, month, `${prefix}month result:`); + assert.sameValue(datetime.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(datetime.day, day, `${prefix}day result:`); + assert.sameValue(datetime.hour, hour, `${prefix}hour result:`); + assert.sameValue(datetime.minute, minute, `${prefix}minute result:`); + assert.sameValue(datetime.second, second, `${prefix}second result:`); + assert.sameValue(datetime.millisecond, millisecond, `${prefix}millisecond result:`); + assert.sameValue(datetime.microsecond, microsecond, `${prefix}microsecond result:`); + assert.sameValue(datetime.nanosecond, nanosecond, `${prefix}nanosecond result:`); + }, + + /* + * assertPlainDateTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.PlainDateTimes are of the correct + * type, equal according to their equals() methods, and additionally that + * their calendar internal slots are the same value. + */ + assertPlainDateTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.PlainDateTime, `${prefix}expected value should be a Temporal.PlainDateTime`); + assert(actual instanceof Temporal.PlainDateTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + assert.sameValue( + actual.getISOFields().calendar, + expected.getISOFields().calendar, + `${prefix}calendar same value:` + ); + }, + + /* + * assertPlainMonthDay(monthDay, monthCode, day[, description [, referenceISOYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainMonthDay is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of monthDay.calendarId().) + */ + assertPlainMonthDay(monthDay, monthCode, day, description = "", referenceISOYear = 1972) { + const prefix = description ? `${description}: ` : ""; + assert(monthDay instanceof Temporal.PlainMonthDay, `${prefix}instanceof`); + assert.sameValue(monthDay.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(monthDay.day, day, `${prefix}day result:`); + assert.sameValue(monthDay.getISOFields().isoYear, referenceISOYear, `${prefix}referenceISOYear result:`); + }, + + /* + * assertPlainTime(time, hour, ..., nanosecond[, description]): + * + * Shorthand for asserting that each field of a Temporal.PlainTime is equal to + * an expected value. + */ + assertPlainTime(time, hour, minute, second, millisecond, microsecond, nanosecond, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(time instanceof Temporal.PlainTime, `${prefix}instanceof`); + assert.sameValue(time.hour, hour, `${prefix}hour result:`); + assert.sameValue(time.minute, minute, `${prefix}minute result:`); + assert.sameValue(time.second, second, `${prefix}second result:`); + assert.sameValue(time.millisecond, millisecond, `${prefix}millisecond result:`); + assert.sameValue(time.microsecond, microsecond, `${prefix}microsecond result:`); + assert.sameValue(time.nanosecond, nanosecond, `${prefix}nanosecond result:`); + }, + + /* + * assertPlainTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.PlainTimes are of the correct + * type and equal according to their equals() methods. + */ + assertPlainTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.PlainTime, `${prefix}expected value should be a Temporal.PlainTime`); + assert(actual instanceof Temporal.PlainTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + }, + + /* + * assertPlainYearMonth(yearMonth, year, month, monthCode[, description[, era, eraYear, referenceISODay]]): + * + * Shorthand for asserting that each field of a Temporal.PlainYearMonth is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of yearMonth.calendarId.) + */ + assertPlainYearMonth(yearMonth, year, month, monthCode, description = "", era = undefined, eraYear = undefined, referenceISODay = 1) { + const prefix = description ? `${description}: ` : ""; + assert(yearMonth instanceof Temporal.PlainYearMonth, `${prefix}instanceof`); + assert.sameValue(yearMonth.era, era, `${prefix}era result:`); + assert.sameValue(yearMonth.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(yearMonth.year, year, `${prefix}year result:`); + assert.sameValue(yearMonth.month, month, `${prefix}month result:`); + assert.sameValue(yearMonth.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(yearMonth.getISOFields().isoDay, referenceISODay, `${prefix}referenceISODay result:`); + }, + + /* + * assertZonedDateTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.ZonedDateTimes are of the correct + * type, equal according to their equals() methods, and additionally that + * their time zones and calendar internal slots are the same value. + */ + assertZonedDateTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.ZonedDateTime, `${prefix}expected value should be a Temporal.ZonedDateTime`); + assert(actual instanceof Temporal.ZonedDateTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + assert.sameValue(actual.timeZone, expected.timeZone, `${prefix}time zone same value:`); + assert.sameValue( + actual.getISOFields().calendar, + expected.getISOFields().calendar, + `${prefix}calendar same value:` + ); + }, + + /* + * assertUnreachable(description): + * + * Helper for asserting that code is not executed. This is useful for + * assertions that methods of user calendars and time zones are not called. + */ + assertUnreachable(description) { + let message = "This code should not be executed"; + if (description) { + message = `${message}: ${description}`; + } + throw new Test262Error(message); + }, + + /* + * checkCalendarDateUntilLargestUnitSingular(func, expectedLargestUnitCalls): + * + * When an options object with a largestUnit property is synthesized inside + * Temporal and passed to user code such as calendar.dateUntil(), the value of + * the largestUnit property should be in the singular form, even if the input + * was given in the plural form. + * (This doesn't apply when the options object is passed through verbatim.) + * + * func(calendar, largestUnit, index) is the operation under test. It's called + * with an instance of a calendar that keeps track of which largestUnit is + * passed to dateUntil(), each key of expectedLargestUnitCalls in turn, and + * the key's numerical index in case the function needs to generate test data + * based on the index. At the end, the actual values passed to dateUntil() are + * compared with the array values of expectedLargestUnitCalls. + */ + checkCalendarDateUntilLargestUnitSingular(func, expectedLargestUnitCalls) { + const actual = []; + + class DateUntilOptionsCalendar extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateUntil(earlier, later, options) { + actual.push(options.largestUnit); + return super.dateUntil(earlier, later, options); + } + + toString() { + return "date-until-options"; + } + } + + const calendar = new DateUntilOptionsCalendar(); + Object.entries(expectedLargestUnitCalls).forEach(([largestUnit, expected], index) => { + func(calendar, largestUnit, index); + assert.compareArray(actual, expected, `largestUnit passed to calendar.dateUntil() for largestUnit ${largestUnit}`); + actual.splice(0); // empty it for the next check + }); + }, + + /* + * checkPlainDateTimeConversionFastPath(func): + * + * ToTemporalDate and ToTemporalTime should both, if given a + * Temporal.PlainDateTime instance, convert to the desired type by reading the + * PlainDateTime's internal slots, rather than calling any getters. + * + * func(datetime, calendar) is the actual operation to test, that must + * internally call the abstract operation ToTemporalDate or ToTemporalTime. + * It is passed a Temporal.PlainDateTime instance, as well as the instance's + * calendar object (so that it doesn't have to call the calendar getter itself + * if it wants to make any assertions about the calendar.) + */ + checkPlainDateTimeConversionFastPath(func, message = "checkPlainDateTimeConversionFastPath") { + const actual = []; + const expected = []; + + const calendar = new Temporal.Calendar("iso8601"); + const datetime = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321, calendar); + const prototypeDescrs = Object.getOwnPropertyDescriptors(Temporal.PlainDateTime.prototype); + ["year", "month", "monthCode", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"].forEach((property) => { + Object.defineProperty(datetime, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + const value = prototypeDescrs[property].get.call(this); + return { + toString() { + actual.push(`toString ${formatPropertyName(property)}`); + return value.toString(); + }, + valueOf() { + actual.push(`valueOf ${formatPropertyName(property)}`); + return value; + }, + }; + }, + }); + }); + Object.defineProperty(datetime, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(datetime, calendar); + assert.compareArray(actual, expected, `${message}: property getters not called`); + }, + + /* + * Check that an options bag that accepts units written in the singular form, + * also accepts the same units written in the plural form. + * func(unit) should call the method with the appropriate options bag + * containing unit as a value. This will be called twice for each element of + * validSingularUnits, once with singular and once with plural, and the + * results of each pair should be the same (whether a Temporal object or a + * primitive value.) + */ + checkPluralUnitsAccepted(func, validSingularUnits) { + const plurals = { + year: 'years', + month: 'months', + week: 'weeks', + day: 'days', + hour: 'hours', + minute: 'minutes', + second: 'seconds', + millisecond: 'milliseconds', + microsecond: 'microseconds', + nanosecond: 'nanoseconds', + }; + + validSingularUnits.forEach((unit) => { + const singularValue = func(unit); + const pluralValue = func(plurals[unit]); + const desc = `Plural ${plurals[unit]} produces the same result as singular ${unit}`; + if (singularValue instanceof Temporal.Duration) { + TemporalHelpers.assertDurationsEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.Instant) { + TemporalHelpers.assertInstantsEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.PlainDateTime) { + TemporalHelpers.assertPlainDateTimesEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.PlainTime) { + TemporalHelpers.assertPlainTimesEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.ZonedDateTime) { + TemporalHelpers.assertZonedDateTimesEqual(pluralValue, singularValue, desc); + } else { + assert.sameValue(pluralValue, singularValue); + } + }); + }, + + /* + * checkRoundingIncrementOptionWrongType(checkFunc, assertTrueResultFunc, assertObjectResultFunc): + * + * Checks the type handling of the roundingIncrement option. + * checkFunc(roundingIncrement) is a function which takes the value of + * roundingIncrement to test, and calls the method under test with it, + * returning the result. assertTrueResultFunc(result, description) should + * assert that result is the expected result with roundingIncrement: true, and + * assertObjectResultFunc(result, description) should assert that result is + * the expected result with roundingIncrement being an object with a valueOf() + * method. + */ + checkRoundingIncrementOptionWrongType(checkFunc, assertTrueResultFunc, assertObjectResultFunc) { + // null converts to 0, which is out of range + assert.throws(RangeError, () => checkFunc(null), "null"); + // Booleans convert to either 0 or 1, and 1 is allowed + const trueResult = checkFunc(true); + assertTrueResultFunc(trueResult, "true"); + assert.throws(RangeError, () => checkFunc(false), "false"); + // Symbols and BigInts cannot convert to numbers + assert.throws(TypeError, () => checkFunc(Symbol()), "symbol"); + assert.throws(TypeError, () => checkFunc(2n), "bigint"); + + // Objects prefer their valueOf() methods when converting to a number + assert.throws(RangeError, () => checkFunc({}), "plain object"); + + const expected = [ + "get roundingIncrement.valueOf", + "call roundingIncrement.valueOf", + ]; + const actual = []; + const observer = TemporalHelpers.toPrimitiveObserver(actual, 2, "roundingIncrement"); + const objectResult = checkFunc(observer); + assertObjectResultFunc(objectResult, "object with valueOf"); + assert.compareArray(actual, expected, "order of operations"); + }, + + /* + * checkStringOptionWrongType(propertyName, value, checkFunc, assertFunc): + * + * Checks the type handling of a string option, of which there are several in + * Temporal. + * propertyName is the name of the option, and value is the value that + * assertFunc should expect it to have. + * checkFunc(value) is a function which takes the value of the option to test, + * and calls the method under test with it, returning the result. + * assertFunc(result, description) should assert that result is the expected + * result with the option value being an object with a toString() method + * which returns the given value. + */ + checkStringOptionWrongType(propertyName, value, checkFunc, assertFunc) { + // null converts to the string "null", which is an invalid string value + assert.throws(RangeError, () => checkFunc(null), "null"); + // Booleans convert to the strings "true" or "false", which are invalid + assert.throws(RangeError, () => checkFunc(true), "true"); + assert.throws(RangeError, () => checkFunc(false), "false"); + // Symbols cannot convert to strings + assert.throws(TypeError, () => checkFunc(Symbol()), "symbol"); + // Numbers convert to strings which are invalid + assert.throws(RangeError, () => checkFunc(2), "number"); + // BigInts convert to strings which are invalid + assert.throws(RangeError, () => checkFunc(2n), "bigint"); + + // Objects prefer their toString() methods when converting to a string + assert.throws(RangeError, () => checkFunc({}), "plain object"); + + const expected = [ + `get ${propertyName}.toString`, + `call ${propertyName}.toString`, + ]; + const actual = []; + const observer = TemporalHelpers.toPrimitiveObserver(actual, value, propertyName); + const result = checkFunc(observer); + assertFunc(result, "object with toString"); + assert.compareArray(actual, expected, "order of operations"); + }, + + /* + * checkSubclassingIgnored(construct, constructArgs, method, methodArgs, + * resultAssertions): + * + * Methods of Temporal classes that return a new instance of the same class, + * must not take the constructor of a subclass into account, nor the @@species + * property. This helper runs tests to ensure this. + * + * construct(...constructArgs) must yield a valid instance of the Temporal + * class. instance[method](...methodArgs) is the method call under test, which + * must also yield a valid instance of the same Temporal class, not a + * subclass. See below for the individual tests that this runs. + * resultAssertions() is a function that performs additional assertions on the + * instance returned by the method under test. + */ + checkSubclassingIgnored(...args) { + this.checkSubclassConstructorNotObject(...args); + this.checkSubclassConstructorUndefined(...args); + this.checkSubclassConstructorThrows(...args); + this.checkSubclassConstructorNotCalled(...args); + this.checkSubclassSpeciesInvalidResult(...args); + this.checkSubclassSpeciesNotAConstructor(...args); + this.checkSubclassSpeciesNull(...args); + this.checkSubclassSpeciesUndefined(...args); + this.checkSubclassSpeciesThrows(...args); + }, + + /* + * Checks that replacing the 'constructor' property of the instance with + * various primitive values does not affect the returned new instance. + */ + checkSubclassConstructorNotObject(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = value; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + }, + + /* + * Checks that replacing the 'constructor' property of the subclass with + * undefined does not affect the returned new instance. + */ + checkSubclassConstructorUndefined(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = undefined; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Checks that making the 'constructor' property of the instance throw when + * called does not affect the returned new instance. + */ + checkSubclassConstructorThrows(construct, constructArgs, method, methodArgs, resultAssertions) { + function CustomError() {} + const instance = new construct(...constructArgs); + Object.defineProperty(instance, "constructor", { + get() { + throw new CustomError(); + } + }); + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Checks that when subclassing, the subclass constructor is not called by + * the method under test. + */ + checkSubclassConstructorNotCalled(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it's a + * constructor that returns a non-object value. + */ + checkSubclassSpeciesInvalidResult(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = { + [Symbol.species]: function() { + return value; + }, + }; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "plain object"); + }, + + /* + * Check that the constructor's @@species property is ignored when it's not a + * constructor. + */ + checkSubclassSpeciesNotAConstructor(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = { + [Symbol.species]: value, + }; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "plain object"); + }, + + /* + * Check that the constructor's @@species property is ignored when it's null. + */ + checkSubclassSpeciesNull(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = { + [Symbol.species]: null, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it's + * undefined. + */ + checkSubclassSpeciesUndefined(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = { + [Symbol.species]: undefined, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it throws, + * i.e. it is not called at all. + */ + checkSubclassSpeciesThrows(construct, constructArgs, method, methodArgs, resultAssertions) { + function CustomError() {} + + const instance = new construct(...constructArgs); + instance.constructor = { + get [Symbol.species]() { + throw new CustomError(); + }, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + }, + + /* + * checkSubclassingIgnoredStatic(construct, method, methodArgs, resultAssertions): + * + * Static methods of Temporal classes that return a new instance of the class, + * must not use the this-value as a constructor. This helper runs tests to + * ensure this. + * + * construct[method](...methodArgs) is the static method call under test, and + * must yield a valid instance of the Temporal class, not a subclass. See + * below for the individual tests that this runs. + * resultAssertions() is a function that performs additional assertions on the + * instance returned by the method under test. + */ + checkSubclassingIgnoredStatic(...args) { + this.checkStaticInvalidReceiver(...args); + this.checkStaticReceiverNotCalled(...args); + this.checkThisValueNotCalled(...args); + }, + + /* + * Check that calling the static method with a receiver that's not callable, + * still calls the intrinsic constructor. + */ + checkStaticInvalidReceiver(construct, method, methodArgs, resultAssertions) { + function check(value, description) { + const result = construct[method].apply(value, methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "Non-callable object"); + }, + + /* + * Check that calling the static method with a receiver that returns a value + * that's not callable, still calls the intrinsic constructor. + */ + checkStaticReceiverNotCalled(construct, method, methodArgs, resultAssertions) { + function check(value, description) { + const receiver = function () { + return value; + }; + const result = construct[method].apply(receiver, methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "Non-callable object"); + }, + + /* + * Check that the receiver isn't called. + */ + checkThisValueNotCalled(construct, method, methodArgs, resultAssertions) { + let called = false; + + class MySubclass extends construct { + constructor(...args) { + called = true; + super(...args); + } + } + + const result = MySubclass[method](...methodArgs); + assert.sameValue(called, false); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that any iterable returned from a custom time zone's + * getPossibleInstantsFor() method is exhausted. + * The custom time zone object is passed in to func(). + * expected is an array of strings representing the expected calls to the + * getPossibleInstantsFor() method. The PlainDateTimes that it is called with, + * are compared (using their toString() results) with the array. + */ + checkTimeZonePossibleInstantsIterable(func, expected) { + // A custom time zone that returns an iterable instead of an array from its + // getPossibleInstantsFor() method, and for testing purposes skips + // 00:00-01:00 UTC on January 1, 2030, and repeats 00:00-01:00 UTC+1 on + // January 3, 2030. Otherwise identical to the UTC time zone. + class TimeZonePossibleInstantsIterable extends Temporal.TimeZone { + constructor() { + super("UTC"); + this.getPossibleInstantsForCallCount = 0; + this.getPossibleInstantsForCalledWith = []; + this.getPossibleInstantsForReturns = []; + this.iteratorExhausted = []; + } + + toString() { + return "Custom/Iterable"; + } + + getOffsetNanosecondsFor(instant) { + if (Temporal.Instant.compare(instant, "2030-01-01T00:00Z") >= 0 && + Temporal.Instant.compare(instant, "2030-01-03T01:00Z") < 0) { + return 3600_000_000_000; + } else { + return 0; + } + } + + getPossibleInstantsFor(dateTime) { + this.getPossibleInstantsForCallCount++; + this.getPossibleInstantsForCalledWith.push(dateTime); + + // Fake DST transition + let retval = super.getPossibleInstantsFor(dateTime); + if (dateTime.toPlainDate().equals("2030-01-01") && dateTime.hour === 0) { + retval = []; + } else if (dateTime.toPlainDate().equals("2030-01-03") && dateTime.hour === 0) { + retval.push(retval[0].subtract({ hours: 1 })); + } else if (dateTime.year === 2030 && dateTime.month === 1 && dateTime.day >= 1 && dateTime.day <= 2) { + retval[0] = retval[0].subtract({ hours: 1 }); + } + + this.getPossibleInstantsForReturns.push(retval); + this.iteratorExhausted.push(false); + return { + callIndex: this.getPossibleInstantsForCallCount - 1, + timeZone: this, + *[Symbol.iterator]() { + yield* this.timeZone.getPossibleInstantsForReturns[this.callIndex]; + this.timeZone.iteratorExhausted[this.callIndex] = true; + }, + }; + } + } + + const timeZone = new TimeZonePossibleInstantsIterable(); + func(timeZone); + + assert.sameValue(timeZone.getPossibleInstantsForCallCount, expected.length, "getPossibleInstantsFor() method called correct number of times"); + + for (let index = 0; index < expected.length; index++) { + assert.sameValue(timeZone.getPossibleInstantsForCalledWith[index].toString(), expected[index], "getPossibleInstantsFor() called with expected PlainDateTime"); + assert(timeZone.iteratorExhausted[index], "iterated through the whole iterable"); + } + }, + + /* + * Check that any calendar-carrying Temporal object has its [[Calendar]] + * internal slot read by ToTemporalCalendar, and does not fetch the calendar + * by calling getters. + * The custom calendar object is passed in to func() so that it can do its + * own additional assertions involving the calendar if necessary. (Sometimes + * there is nothing to assert as the calendar isn't stored anywhere that can + * be asserted about.) + */ + checkToTemporalCalendarFastPath(func) { + class CalendarFastPathCheck extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(...args) { + return super.dateFromFields(...args).withCalendar(this); + } + + monthDayFromFields(...args) { + const { isoYear, isoMonth, isoDay } = super.monthDayFromFields(...args).getISOFields(); + return new Temporal.PlainMonthDay(isoMonth, isoDay, this, isoYear); + } + + yearMonthFromFields(...args) { + const { isoYear, isoMonth, isoDay } = super.yearMonthFromFields(...args).getISOFields(); + return new Temporal.PlainYearMonth(isoYear, isoMonth, this, isoDay); + } + + toString() { + return "fast-path-check"; + } + } + const calendar = new CalendarFastPathCheck(); + + const plainDate = new Temporal.PlainDate(2000, 5, 2, calendar); + const plainDateTime = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321, calendar); + const plainMonthDay = new Temporal.PlainMonthDay(5, 2, calendar); + const plainYearMonth = new Temporal.PlainYearMonth(2000, 5, calendar); + const zonedDateTime = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, "UTC", calendar); + + [plainDate, plainDateTime, plainMonthDay, plainYearMonth, zonedDateTime].forEach((temporalObject) => { + const actual = []; + const expected = []; + + Object.defineProperty(temporalObject, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(temporalObject, calendar); + assert.compareArray(actual, expected, "calendar getter not called"); + }); + }, + + checkToTemporalInstantFastPath(func) { + const actual = []; + const expected = []; + + const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, "UTC"); + Object.defineProperty(datetime, 'toString', { + get() { + actual.push("get toString"); + return function (options) { + actual.push("call toString"); + return Temporal.ZonedDateTime.prototype.toString.call(this, options); + }; + }, + }); + + func(datetime); + assert.compareArray(actual, expected, "toString not called"); + }, + + checkToTemporalPlainDateTimeFastPath(func) { + const actual = []; + const expected = []; + + const calendar = new Temporal.Calendar("iso8601"); + const date = new Temporal.PlainDate(2000, 5, 2, calendar); + const prototypeDescrs = Object.getOwnPropertyDescriptors(Temporal.PlainDate.prototype); + ["year", "month", "monthCode", "day"].forEach((property) => { + Object.defineProperty(date, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + const value = prototypeDescrs[property].get.call(this); + return TemporalHelpers.toPrimitiveObserver(actual, value, property); + }, + }); + }); + ["hour", "minute", "second", "millisecond", "microsecond", "nanosecond"].forEach((property) => { + Object.defineProperty(date, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + return undefined; + }, + }); + }); + Object.defineProperty(date, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(date, calendar); + assert.compareArray(actual, expected, "property getters not called"); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that the + * fromFields methods are always called with a null-prototype fields object. + */ + calendarCheckFieldsPrototypePollution() { + class CalendarCheckFieldsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + } + + // toString must remain "iso8601", so that some methods don't throw due to + // incompatible calendars + + dateFromFields(fields, options = {}) { + this.dateFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "dateFromFields should be called with null-prototype fields object"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options = {}) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "yearMonthFromFields should be called with null-prototype fields object"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options = {}) { + this.monthDayFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "monthDayFromFields should be called with null-prototype fields object"); + return super.monthDayFromFields(fields, options); + } + } + + return new CalendarCheckFieldsPrototypePollution(); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that the + * mergeFields() method is always called with null-prototype fields objects. + */ + calendarCheckMergeFieldsPrototypePollution() { + class CalendarCheckMergeFieldsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.mergeFieldsCallCount = 0; + } + + toString() { + return "merge-fields-null-proto"; + } + + mergeFields(fields, additionalFields) { + this.mergeFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "mergeFields should be called with null-prototype fields object (first argument)"); + assert.sameValue(Object.getPrototypeOf(additionalFields), null, "mergeFields should be called with null-prototype fields object (second argument)"); + return super.mergeFields(fields, additionalFields); + } + } + + return new CalendarCheckMergeFieldsPrototypePollution(); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that methods + * are always called with a null-prototype options object. + */ + calendarCheckOptionsPrototypePollution() { + class CalendarCheckOptionsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.yearMonthFromFieldsCallCount = 0; + this.dateUntilCallCount = 0; + } + + toString() { + return "options-null-proto"; + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(options), null, "yearMonthFromFields should be called with null-prototype options"); + return super.yearMonthFromFields(fields, options); + } + + dateUntil(one, two, options) { + this.dateUntilCallCount++; + assert.sameValue(Object.getPrototypeOf(options), null, "dateUntil should be called with null-prototype options"); + return super.dateUntil(one, two, options); + } + } + + return new CalendarCheckOptionsPrototypePollution(); + }, + + /* + * A custom calendar that asserts its dateAdd() method is called with the + * options parameter having the value undefined. + */ + calendarDateAddUndefinedOptions() { + class CalendarDateAddUndefinedOptions extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateAddCallCount = 0; + } + + toString() { + return "dateadd-undef-options"; + } + + dateAdd(date, duration, options) { + this.dateAddCallCount++; + assert.sameValue(options, undefined, "dateAdd shouldn't be called with options"); + return super.dateAdd(date, duration, options); + } + } + return new CalendarDateAddUndefinedOptions(); + }, + + /* + * A custom calendar that asserts its dateAdd() method is called with a + * PlainDate instance. Optionally, it also asserts that the PlainDate instance + * is the specific object `this.specificPlainDate`, if it is set by the + * calling code. + */ + calendarDateAddPlainDateInstance() { + class CalendarDateAddPlainDateInstance extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateAddCallCount = 0; + this.specificPlainDate = undefined; + } + + toString() { + return "dateadd-plain-date-instance"; + } + + dateFromFields(...args) { + return super.dateFromFields(...args).withCalendar(this); + } + + dateAdd(date, duration, options) { + this.dateAddCallCount++; + assert(date instanceof Temporal.PlainDate, "dateAdd() should be called with a PlainDate instance"); + if (this.dateAddCallCount === 1 && this.specificPlainDate) { + assert.sameValue(date, this.specificPlainDate, `dateAdd() should be called first with the specific PlainDate instance ${this.specificPlainDate}`); + } + return super.dateAdd(date, duration, options).withCalendar(this); + } + } + return new CalendarDateAddPlainDateInstance(); + }, + + /* + * A custom calendar that returns an iterable instead of an array from its + * fields() method, otherwise identical to the ISO calendar. + */ + calendarFieldsIterable() { + class CalendarFieldsIterable extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.fieldsCallCount = 0; + this.fieldsCalledWith = []; + this.iteratorExhausted = []; + } + + toString() { + return "fields-iterable"; + } + + fields(fieldNames) { + this.fieldsCallCount++; + this.fieldsCalledWith.push(fieldNames.slice()); + this.iteratorExhausted.push(false); + return { + callIndex: this.fieldsCallCount - 1, + calendar: this, + *[Symbol.iterator]() { + yield* this.calendar.fieldsCalledWith[this.callIndex]; + this.calendar.iteratorExhausted[this.callIndex] = true; + }, + }; + } + } + return new CalendarFieldsIterable(); + }, + + /* + * A custom calendar that asserts its ...FromFields() methods are called with + * the options parameter having the value undefined. + */ + calendarFromFieldsUndefinedOptions() { + class CalendarFromFieldsUndefinedOptions extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + } + + toString() { + return "from-fields-undef-options"; + } + + dateFromFields(fields, options) { + this.dateFromFieldsCallCount++; + assert.sameValue(options, undefined, "dateFromFields shouldn't be called with options"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(options, undefined, "yearMonthFromFields shouldn't be called with options"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + this.monthDayFromFieldsCallCount++; + assert.sameValue(options, undefined, "monthDayFromFields shouldn't be called with options"); + return super.monthDayFromFields(fields, options); + } + } + return new CalendarFromFieldsUndefinedOptions(); + }, + + /* + * A custom calendar that modifies the fields object passed in to + * dateFromFields, sabotaging its time properties. + */ + calendarMakeInfinityTime() { + class CalendarMakeInfinityTime extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(fields, options) { + const retval = super.dateFromFields(fields, options); + fields.hour = Infinity; + fields.minute = Infinity; + fields.second = Infinity; + fields.millisecond = Infinity; + fields.microsecond = Infinity; + fields.nanosecond = Infinity; + return retval; + } + } + return new CalendarMakeInfinityTime(); + }, + + /* + * A custom calendar that defines getters on the fields object passed into + * dateFromFields that throw, sabotaging its time properties. + */ + calendarMakeInvalidGettersTime() { + class CalendarMakeInvalidGettersTime extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(fields, options) { + const retval = super.dateFromFields(fields, options); + const throwingDescriptor = { + get() { + throw new Test262Error("reading a sabotaged time field"); + }, + }; + Object.defineProperties(fields, { + hour: throwingDescriptor, + minute: throwingDescriptor, + second: throwingDescriptor, + millisecond: throwingDescriptor, + microsecond: throwingDescriptor, + nanosecond: throwingDescriptor, + }); + return retval; + } + } + return new CalendarMakeInvalidGettersTime(); + }, + + /* + * A custom calendar whose mergeFields() method returns a proxy object with + * all of its Get and HasProperty operations observable, as well as adding a + * "shouldNotBeCopied": true property. + */ + calendarMergeFieldsGetters() { + class CalendarMergeFieldsGetters extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.mergeFieldsReturnOperations = []; + } + + toString() { + return "merge-fields-getters"; + } + + dateFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.monthDayFromFields(fields, options); + } + + mergeFields(fields, additionalFields) { + const retval = super.mergeFields(fields, additionalFields); + retval._calendar = this; + retval.shouldNotBeCopied = true; + return new Proxy(retval, { + get(target, key) { + target._calendar.mergeFieldsReturnOperations.push(`get ${key}`); + const result = target[key]; + if (result === undefined) { + return undefined; + } + return TemporalHelpers.toPrimitiveObserver(target._calendar.mergeFieldsReturnOperations, result, key); + }, + has(target, key) { + target._calendar.mergeFieldsReturnOperations.push(`has ${key}`); + return key in target; + }, + }); + } + } + return new CalendarMergeFieldsGetters(); + }, + + /* + * A custom calendar whose mergeFields() method returns a primitive value, + * given by @primitive, and which records the number of calls made to its + * dateFromFields(), yearMonthFromFields(), and monthDayFromFields() methods. + */ + calendarMergeFieldsReturnsPrimitive(primitive) { + class CalendarMergeFieldsPrimitive extends Temporal.Calendar { + constructor(mergeFieldsReturnValue) { + super("iso8601"); + this._mergeFieldsReturnValue = mergeFieldsReturnValue; + this.dateFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + } + + toString() { + return "merge-fields-primitive"; + } + + dateFromFields(fields, options) { + this.dateFromFieldsCallCount++; + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + this.monthDayFromFieldsCallCount++; + return super.monthDayFromFields(fields, options); + } + + mergeFields() { + return this._mergeFieldsReturnValue; + } + } + return new CalendarMergeFieldsPrimitive(primitive); + }, + + /* + * A custom calendar whose fields() method returns the same value as the + * iso8601 calendar, with the addition of extraFields provided as parameter. + */ + calendarWithExtraFields(fields) { + class CalendarWithExtraFields extends Temporal.Calendar { + constructor(extraFields) { + super("iso8601"); + this._extraFields = extraFields; + } + + fields(fieldNames) { + return super.fields(fieldNames).concat(this._extraFields); + } + } + + return new CalendarWithExtraFields(fields); + }, + + /* + * crossDateLineTimeZone(): + * + * This returns an instance of a custom time zone class that implements one + * single transition where the time zone moves from one side of the + * International Date Line to the other, for the purpose of testing time zone + * calculations without depending on system time zone data. + * + * The transition occurs at epoch second 1325239200 and goes from offset + * -10:00 to +14:00. In other words, the time zone skips the whole calendar + * day of 2011-12-30. This is the same as the real-life transition in the + * Pacific/Apia time zone. + */ + crossDateLineTimeZone() { + const { compare } = Temporal.PlainDate; + const skippedDay = new Temporal.PlainDate(2011, 12, 30); + const transitionEpoch = 1325239200_000_000_000n; + const beforeOffset = new Temporal.TimeZone("-10:00"); + const afterOffset = new Temporal.TimeZone("+14:00"); + + class CrossDateLineTimeZone extends Temporal.TimeZone { + constructor() { + super("+14:00"); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < transitionEpoch) { + return beforeOffset.getOffsetNanosecondsFor(instant); + } + return afterOffset.getOffsetNanosecondsFor(instant); + } + + getPossibleInstantsFor(datetime) { + const comparison = compare(datetime.toPlainDate(), skippedDay); + if (comparison === 0) { + return []; + } + if (comparison < 0) { + return [beforeOffset.getInstantFor(datetime)]; + } + return [afterOffset.getInstantFor(datetime)]; + } + + getPreviousTransition(instant) { + if (instant.epochNanoseconds > transitionEpoch) return new Temporal.Instant(transitionEpoch); + return null; + } + + getNextTransition(instant) { + if (instant.epochNanoseconds < transitionEpoch) return new Temporal.Instant(transitionEpoch); + return null; + } + + toString() { + return "Custom/Date_Line"; + } + } + return new CrossDateLineTimeZone(); + }, + + /* + * observeProperty(calls, object, propertyName, value): + * + * Defines an own property @object.@propertyName with value @value, that + * will log any calls to its accessors to the array @calls. + */ + observeProperty(calls, object, propertyName, value, objectName = "") { + Object.defineProperty(object, propertyName, { + get() { + calls.push(`get ${formatPropertyName(propertyName, objectName)}`); + return value; + }, + set(v) { + calls.push(`set ${formatPropertyName(propertyName, objectName)}`); + } + }); + }, + + /* + * observeMethod(calls, object, propertyName, value): + * + * Defines an own property @object.@propertyName with value @value, that + * will log any calls of @value to the array @calls. + */ + observeMethod(calls, object, propertyName, objectName = "") { + const method = object[propertyName]; + object[propertyName] = function () { + calls.push(`call ${formatPropertyName(propertyName, objectName)}`); + return method.apply(object, arguments); + }; + }, + + /* + * Used for substituteMethod to indicate default behavior instead of a + * substituted value + */ + SUBSTITUTE_SKIP: SKIP_SYMBOL, + + /* + * substituteMethod(object, propertyName, values): + * + * Defines an own property @object.@propertyName that will, for each + * subsequent call to the method previously defined as + * @object.@propertyName: + * - Call the method, if no more values remain + * - Call the method, if the value in @values for the corresponding call + * is SUBSTITUTE_SKIP + * - Otherwise, return the corresponding value in @value + */ + substituteMethod(object, propertyName, values) { + let calls = 0; + const method = object[propertyName]; + object[propertyName] = function () { + if (calls >= values.length) { + return method.apply(object, arguments); + } else if (values[calls] === SKIP_SYMBOL) { + calls++; + return method.apply(object, arguments); + } else { + return values[calls++]; + } + }; + }, + + /* + * calendarObserver: + * A custom calendar that behaves exactly like the ISO 8601 calendar but + * tracks calls to any of its methods, and Get/Has operations on its + * properties, by appending messages to an array. This is for the purpose of + * testing order of operations that are observable from user code. + * objectName is used in the log. + */ + calendarObserver(calls, objectName, methodOverrides = {}) { + function removeExtraHasPropertyChecks(objectName, calls) { + // Inserting the tracking calendar into the return values of methods + // that we chain up into the ISO calendar for, causes extra HasProperty + // checks, which we observe. This removes them so that we don't leak + // implementation details of the helper into the test code. + assert.sameValue(calls.pop(), `has ${objectName}.yearOfWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.yearMonthFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.year`); + assert.sameValue(calls.pop(), `has ${objectName}.weekOfYear`); + assert.sameValue(calls.pop(), `has ${objectName}.monthsInYear`); + assert.sameValue(calls.pop(), `has ${objectName}.monthDayFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.monthCode`); + assert.sameValue(calls.pop(), `has ${objectName}.month`); + assert.sameValue(calls.pop(), `has ${objectName}.mergeFields`); + assert.sameValue(calls.pop(), `has ${objectName}.inLeapYear`); + assert.sameValue(calls.pop(), `has ${objectName}.id`); + assert.sameValue(calls.pop(), `has ${objectName}.fields`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInYear`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInMonth`); + assert.sameValue(calls.pop(), `has ${objectName}.dayOfYear`); + assert.sameValue(calls.pop(), `has ${objectName}.dayOfWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.day`); + assert.sameValue(calls.pop(), `has ${objectName}.dateUntil`); + assert.sameValue(calls.pop(), `has ${objectName}.dateFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.dateAdd`); + } + + const iso8601 = new Temporal.Calendar("iso8601"); + const trackingMethods = { + dateFromFields(...args) { + calls.push(`call ${objectName}.dateFromFields`); + if ('dateFromFields' in methodOverrides) { + const value = methodOverrides.dateFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.dateFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainDate(isoYear, isoMonth, isoDay, this); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + yearMonthFromFields(...args) { + calls.push(`call ${objectName}.yearMonthFromFields`); + if ('yearMonthFromFields' in methodOverrides) { + const value = methodOverrides.yearMonthFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.yearMonthFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainYearMonth(isoYear, isoMonth, this, isoDay); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + monthDayFromFields(...args) { + calls.push(`call ${objectName}.monthDayFromFields`); + if ('monthDayFromFields' in methodOverrides) { + const value = methodOverrides.monthDayFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.monthDayFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainMonthDay(isoMonth, isoDay, this, isoYear); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + dateAdd(...args) { + calls.push(`call ${objectName}.dateAdd`); + if ('dateAdd' in methodOverrides) { + const value = methodOverrides.dateAdd; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.dateAdd(...args); + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainDate(isoYear, isoMonth, isoDay, this); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + id: "iso8601", + }; + // Automatically generate the other methods that don't need any custom code + [ + "dateUntil", + "day", + "dayOfWeek", + "dayOfYear", + "daysInMonth", + "daysInWeek", + "daysInYear", + "era", + "eraYear", + "fields", + "inLeapYear", + "mergeFields", + "month", + "monthCode", + "monthsInYear", + "toString", + "weekOfYear", + "year", + "yearOfWeek", + ].forEach((methodName) => { + trackingMethods[methodName] = function (...args) { + calls.push(`call ${formatPropertyName(methodName, objectName)}`); + if (methodName in methodOverrides) { + const value = methodOverrides[methodName]; + return typeof value === "function" ? value(...args) : value; + } + return iso8601[methodName](...args); + }; + }); + return new Proxy(trackingMethods, { + get(target, key, receiver) { + const result = Reflect.get(target, key, receiver); + calls.push(`get ${formatPropertyName(key, objectName)}`); + return result; + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * A custom calendar that does not allow any of its methods to be called, for + * the purpose of asserting that a particular operation does not call into + * user code. + */ + calendarThrowEverything() { + class CalendarThrowEverything extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + toString() { + TemporalHelpers.assertUnreachable("toString should not be called"); + } + dateFromFields() { + TemporalHelpers.assertUnreachable("dateFromFields should not be called"); + } + yearMonthFromFields() { + TemporalHelpers.assertUnreachable("yearMonthFromFields should not be called"); + } + monthDayFromFields() { + TemporalHelpers.assertUnreachable("monthDayFromFields should not be called"); + } + dateAdd() { + TemporalHelpers.assertUnreachable("dateAdd should not be called"); + } + dateUntil() { + TemporalHelpers.assertUnreachable("dateUntil should not be called"); + } + era() { + TemporalHelpers.assertUnreachable("era should not be called"); + } + eraYear() { + TemporalHelpers.assertUnreachable("eraYear should not be called"); + } + year() { + TemporalHelpers.assertUnreachable("year should not be called"); + } + month() { + TemporalHelpers.assertUnreachable("month should not be called"); + } + monthCode() { + TemporalHelpers.assertUnreachable("monthCode should not be called"); + } + day() { + TemporalHelpers.assertUnreachable("day should not be called"); + } + fields() { + TemporalHelpers.assertUnreachable("fields should not be called"); + } + mergeFields() { + TemporalHelpers.assertUnreachable("mergeFields should not be called"); + } + } + + return new CalendarThrowEverything(); + }, + + /* + * oneShiftTimeZone(shiftInstant, shiftNanoseconds): + * + * In the case of a spring-forward time zone offset transition (skipped time), + * and disambiguation === 'earlier', BuiltinTimeZoneGetInstantFor subtracts a + * negative number of nanoseconds from a PlainDateTime, which should balance + * with the microseconds field. + * + * This returns an instance of a custom time zone class which skips a length + * of time equal to shiftNanoseconds (a number), at the Temporal.Instant + * shiftInstant. Before shiftInstant, it's identical to UTC, and after + * shiftInstant it's a constant-offset time zone. + * + * It provides a getPossibleInstantsForCalledWith member which is an array + * with the result of calling toString() on any PlainDateTimes passed to + * getPossibleInstantsFor(). + */ + oneShiftTimeZone(shiftInstant, shiftNanoseconds) { + class OneShiftTimeZone extends Temporal.TimeZone { + constructor(shiftInstant, shiftNanoseconds) { + super("+00:00"); + this._shiftInstant = shiftInstant; + this._epoch1 = shiftInstant.epochNanoseconds; + this._epoch2 = this._epoch1 + BigInt(shiftNanoseconds); + this._shiftNanoseconds = shiftNanoseconds; + this._shift = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, this._shiftNanoseconds); + this.getPossibleInstantsForCalledWith = []; + } + + _isBeforeShift(instant) { + return instant.epochNanoseconds < this._epoch1; + } + + getOffsetNanosecondsFor(instant) { + return this._isBeforeShift(instant) ? 0 : this._shiftNanoseconds; + } + + getPossibleInstantsFor(plainDateTime) { + this.getPossibleInstantsForCalledWith.push(plainDateTime.toString({ calendarName: "never" })); + const [instant] = super.getPossibleInstantsFor(plainDateTime); + if (this._shiftNanoseconds > 0) { + if (this._isBeforeShift(instant)) return [instant]; + if (instant.epochNanoseconds < this._epoch2) return []; + return [instant.subtract(this._shift)]; + } + if (instant.epochNanoseconds < this._epoch2) return [instant]; + const shifted = instant.subtract(this._shift); + if (this._isBeforeShift(instant)) return [instant, shifted]; + return [shifted]; + } + + getNextTransition(instant) { + return this._isBeforeShift(instant) ? this._shiftInstant : null; + } + + getPreviousTransition(instant) { + return this._isBeforeShift(instant) ? null : this._shiftInstant; + } + + toString() { + return "Custom/One_Shift"; + } + } + return new OneShiftTimeZone(shiftInstant, shiftNanoseconds); + }, + + /* + * propertyBagObserver(): + * Returns an object that behaves like the given propertyBag but tracks Get + * and Has operations on any of its properties, by appending messages to an + * array. If the value of a property in propertyBag is a primitive, the value + * of the returned object's property will additionally be a + * TemporalHelpers.toPrimitiveObserver that will track calls to its toString + * and valueOf methods in the same array. This is for the purpose of testing + * order of operations that are observable from user code. objectName is used + * in the log. + */ + propertyBagObserver(calls, propertyBag, objectName) { + return new Proxy(propertyBag, { + ownKeys(target) { + calls.push(`ownKeys ${objectName}`); + return Reflect.ownKeys(target); + }, + getOwnPropertyDescriptor(target, key) { + calls.push(`getOwnPropertyDescriptor ${formatPropertyName(key, objectName)}`); + return Reflect.getOwnPropertyDescriptor(target, key); + }, + get(target, key, receiver) { + calls.push(`get ${formatPropertyName(key, objectName)}`); + const result = Reflect.get(target, key, receiver); + if (result === undefined) { + return undefined; + } + if ((result !== null && typeof result === "object") || typeof result === "function") { + return result; + } + return TemporalHelpers.toPrimitiveObserver(calls, result, `${formatPropertyName(key, objectName)}`); + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * specificOffsetTimeZone(): + * + * This returns an instance of a custom time zone class, which returns a + * specific custom value from its getOffsetNanosecondsFrom() method. This is + * for the purpose of testing the validation of what this method returns. + * + * It also returns an empty array from getPossibleInstantsFor(), so as to + * trigger calls to getOffsetNanosecondsFor() when used from the + * BuiltinTimeZoneGetInstantFor operation. + */ + specificOffsetTimeZone(offsetValue) { + class SpecificOffsetTimeZone extends Temporal.TimeZone { + constructor(offsetValue) { + super("UTC"); + this._offsetValue = offsetValue; + } + + getOffsetNanosecondsFor() { + return this._offsetValue; + } + + getPossibleInstantsFor(dt) { + if (typeof this._offsetValue !== 'number' || Math.abs(this._offsetValue) >= 86400e9 || isNaN(this._offsetValue)) return []; + const zdt = dt.toZonedDateTime("UTC").add({ nanoseconds: -this._offsetValue }); + return [zdt.toInstant()]; + } + + get id() { + return this.getOffsetStringFor(new Temporal.Instant(0n)); + } + } + return new SpecificOffsetTimeZone(offsetValue); + }, + + /* + * springForwardFallBackTimeZone(): + * + * This returns an instance of a custom time zone class that implements one + * single spring-forward/fall-back transition, for the purpose of testing the + * disambiguation option, without depending on system time zone data. + * + * The spring-forward occurs at epoch second 954669600 (2000-04-02T02:00 + * local) and goes from offset -08:00 to -07:00. + * + * The fall-back occurs at epoch second 972810000 (2000-10-29T02:00 local) and + * goes from offset -07:00 to -08:00. + */ + springForwardFallBackTimeZone() { + const { compare } = Temporal.PlainDateTime; + const springForwardLocal = new Temporal.PlainDateTime(2000, 4, 2, 2); + const springForwardEpoch = 954669600_000_000_000n; + const fallBackLocal = new Temporal.PlainDateTime(2000, 10, 29, 1); + const fallBackEpoch = 972810000_000_000_000n; + const winterOffset = new Temporal.TimeZone('-08:00'); + const summerOffset = new Temporal.TimeZone('-07:00'); + + class SpringForwardFallBackTimeZone extends Temporal.TimeZone { + constructor() { + super("-08:00"); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < springForwardEpoch || + instant.epochNanoseconds >= fallBackEpoch) { + return winterOffset.getOffsetNanosecondsFor(instant); + } + return summerOffset.getOffsetNanosecondsFor(instant); + } + + getPossibleInstantsFor(datetime) { + if (compare(datetime, springForwardLocal) >= 0 && compare(datetime, springForwardLocal.add({ hours: 1 })) < 0) { + return []; + } + if (compare(datetime, fallBackLocal) >= 0 && compare(datetime, fallBackLocal.add({ hours: 1 })) < 0) { + return [summerOffset.getInstantFor(datetime), winterOffset.getInstantFor(datetime)]; + } + if (compare(datetime, springForwardLocal) < 0 || compare(datetime, fallBackLocal) >= 0) { + return [winterOffset.getInstantFor(datetime)]; + } + return [summerOffset.getInstantFor(datetime)]; + } + + getPreviousTransition(instant) { + if (instant.epochNanoseconds > fallBackEpoch) return new Temporal.Instant(fallBackEpoch); + if (instant.epochNanoseconds > springForwardEpoch) return new Temporal.Instant(springForwardEpoch); + return null; + } + + getNextTransition(instant) { + if (instant.epochNanoseconds < springForwardEpoch) return new Temporal.Instant(springForwardEpoch); + if (instant.epochNanoseconds < fallBackEpoch) return new Temporal.Instant(fallBackEpoch); + return null; + } + + get id() { + return "Custom/Spring_Fall"; + } + + toString() { + return "Custom/Spring_Fall"; + } + } + return new SpringForwardFallBackTimeZone(); + }, + + /* + * timeZoneObserver: + * A custom calendar that behaves exactly like the UTC time zone but tracks + * calls to any of its methods, and Get/Has operations on its properties, by + * appending messages to an array. This is for the purpose of testing order of + * operations that are observable from user code. objectName is used in the + * log. methodOverrides is an optional object containing properties with the + * same name as Temporal.TimeZone methods. If the property value is a function + * it will be called with the proper arguments instead of the UTC method. + * Otherwise, the property value will be returned directly. + */ + timeZoneObserver(calls, objectName, methodOverrides = {}) { + const utc = new Temporal.TimeZone("UTC"); + const trackingMethods = { + id: "UTC", + }; + // Automatically generate the methods + ["getOffsetNanosecondsFor", "getPossibleInstantsFor", "toString"].forEach((methodName) => { + trackingMethods[methodName] = function (...args) { + calls.push(`call ${formatPropertyName(methodName, objectName)}`); + if (methodName in methodOverrides) { + const value = methodOverrides[methodName]; + return typeof value === "function" ? value(...args) : value; + } + return utc[methodName](...args); + }; + }); + return new Proxy(trackingMethods, { + get(target, key, receiver) { + const result = Reflect.get(target, key, receiver); + calls.push(`get ${formatPropertyName(key, objectName)}`); + return result; + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * A custom time zone that does not allow any of its methods to be called, for + * the purpose of asserting that a particular operation does not call into + * user code. + */ + timeZoneThrowEverything() { + class TimeZoneThrowEverything extends Temporal.TimeZone { + constructor() { + super("UTC"); + } + getOffsetNanosecondsFor() { + TemporalHelpers.assertUnreachable("getOffsetNanosecondsFor should not be called"); + } + getPossibleInstantsFor() { + TemporalHelpers.assertUnreachable("getPossibleInstantsFor should not be called"); + } + toString() { + TemporalHelpers.assertUnreachable("toString should not be called"); + } + } + + return new TimeZoneThrowEverything(); + }, + + /* + * Returns an object that will append logs of any Gets or Calls of its valueOf + * or toString properties to the array calls. Both valueOf and toString will + * return the actual primitiveValue. propertyName is used in the log. + */ + toPrimitiveObserver(calls, primitiveValue, propertyName) { + return { + get valueOf() { + calls.push(`get ${propertyName}.valueOf`); + return function () { + calls.push(`call ${propertyName}.valueOf`); + return primitiveValue; + }; + }, + get toString() { + calls.push(`get ${propertyName}.toString`); + return function () { + calls.push(`call ${propertyName}.toString`); + if (primitiveValue === undefined) return undefined; + return primitiveValue.toString(); + }; + }, + }; + }, + + /* + * An object containing further methods that return arrays of ISO strings, for + * testing parsers. + */ + ISO: { + /* + * PlainMonthDay strings that are not valid. + */ + plainMonthDayStringsInvalid() { + return [ + "11-18junk", + "11-18[u-ca=gregory]", + "11-18[u-ca=hebrew]", + ]; + }, + + /* + * PlainMonthDay strings that are valid and that should produce October 1st. + */ + plainMonthDayStringsValid() { + return [ + "10-01", + "1001", + "1965-10-01", + "1976-10-01T152330.1+00:00", + "19761001T15:23:30.1+00:00", + "1976-10-01T15:23:30.1+0000", + "1976-10-01T152330.1+0000", + "19761001T15:23:30.1+0000", + "19761001T152330.1+00:00", + "19761001T152330.1+0000", + "+001976-10-01T152330.1+00:00", + "+0019761001T15:23:30.1+00:00", + "+001976-10-01T15:23:30.1+0000", + "+001976-10-01T152330.1+0000", + "+0019761001T15:23:30.1+0000", + "+0019761001T152330.1+00:00", + "+0019761001T152330.1+0000", + "1976-10-01T15:23:00", + "1976-10-01T15:23", + "1976-10-01T15", + "1976-10-01", + "--10-01", + "--1001", + ]; + }, + + /* + * PlainTime strings that may be mistaken for PlainMonthDay or + * PlainYearMonth strings, and so require a time designator. + */ + plainTimeStringsAmbiguous() { + const ambiguousStrings = [ + "2021-12", // ambiguity between YYYY-MM and HHMM-UU + "2021-12[-12:00]", // ditto, TZ does not disambiguate + "1214", // ambiguity between MMDD and HHMM + "0229", // ditto, including MMDD that doesn't occur every year + "1130", // ditto, including DD that doesn't occur in every month + "12-14", // ambiguity between MM-DD and HH-UU + "12-14[-14:00]", // ditto, TZ does not disambiguate + "202112", // ambiguity between YYYYMM and HHMMSS + "202112[UTC]", // ditto, TZ does not disambiguate + ]; + // Adding a calendar annotation to one of these strings must not cause + // disambiguation in favour of time. + const stringsWithCalendar = ambiguousStrings.map((s) => s + '[u-ca=iso8601]'); + return ambiguousStrings.concat(stringsWithCalendar); + }, + + /* + * PlainTime strings that are of similar form to PlainMonthDay and + * PlainYearMonth strings, but are not ambiguous due to components that + * aren't valid as months or days. + */ + plainTimeStringsUnambiguous() { + return [ + "2021-13", // 13 is not a month + "202113", // ditto + "2021-13[-13:00]", // ditto + "202113[-13:00]", // ditto + "0000-00", // 0 is not a month + "000000", // ditto + "0000-00[UTC]", // ditto + "000000[UTC]", // ditto + "1314", // 13 is not a month + "13-14", // ditto + "1232", // 32 is not a day + "0230", // 30 is not a day in February + "0631", // 31 is not a day in June + "0000", // 0 is neither a month nor a day + "00-00", // ditto + ]; + }, + + /* + * PlainYearMonth-like strings that are not valid. + */ + plainYearMonthStringsInvalid() { + return [ + "2020-13", + ]; + }, + + /* + * PlainYearMonth-like strings that are valid and should produce November + * 1976 in the ISO 8601 calendar. + */ + plainYearMonthStringsValid() { + return [ + "1976-11", + "1976-11-10", + "1976-11-01T09:00:00+00:00", + "1976-11-01T00:00:00+05:00", + "197611", + "+00197611", + "1976-11-18T15:23:30.1\u221202:00", + "1976-11-18T152330.1+00:00", + "19761118T15:23:30.1+00:00", + "1976-11-18T15:23:30.1+0000", + "1976-11-18T152330.1+0000", + "19761118T15:23:30.1+0000", + "19761118T152330.1+00:00", + "19761118T152330.1+0000", + "+001976-11-18T152330.1+00:00", + "+0019761118T15:23:30.1+00:00", + "+001976-11-18T15:23:30.1+0000", + "+001976-11-18T152330.1+0000", + "+0019761118T15:23:30.1+0000", + "+0019761118T152330.1+00:00", + "+0019761118T152330.1+0000", + "1976-11-18T15:23", + "1976-11-18T15", + "1976-11-18", + ]; + }, + + /* + * PlainYearMonth-like strings that are valid and should produce November of + * the ISO year -9999. + */ + plainYearMonthStringsValidNegativeYear() { + return [ + "\u2212009999-11", + ]; + }, + } +}; diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/shell.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/subtract-second-and-nanosecond-from-last-transition.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/subtract-second-and-nanosecond-from-last-transition.js new file mode 100644 index 0000000000..b1b157afb4 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/subtract-second-and-nanosecond-from-last-transition.js @@ -0,0 +1,58 @@ +// |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.timezone.prototype.getnexttransition +description: > + Compute next transition when seconds resp. nanoseconds are subtracted from the last transition. +features: [Temporal] +---*/ + +// From <https://github.com/eggert/tz/blob/main/europe>: +// +// # Zone NAME STDOFF RULES FORMAT [UNTIL] +// Zone Europe/Paris 0:09:21 - LMT 1891 Mar 15 0:01 +// 0:09:21 - PMT 1911 Mar 11 0:01 # Paris MT + +let tz = new Temporal.TimeZone("Europe/Paris"); + +let zdt = new Temporal.PlainDateTime(1800, 1, 1).toZonedDateTime(tz); +assert.sameValue(zdt.toString(), "1800-01-01T00:00:00+00:09[Europe/Paris]"); +assert.sameValue(zdt.offsetNanoseconds, (9 * 60 + 21) * 1_000_000_000); + +// Ensure the first transition was correctly computed. +let first = tz.getNextTransition(zdt); +assert.sameValue(first.toString(), "1911-03-10T23:50:39Z"); +assert.sameValue(new Temporal.ZonedDateTime(first.epochNanoseconds, tz).toString(), + "1911-03-10T23:50:39+00:00[Europe/Paris]"); + +let next; + +// Compute the next transition starting from the first transition minus 1s. +let firstMinus1s = first.add({seconds: -1}); +assert.sameValue(firstMinus1s.toString(), "1911-03-10T23:50:38Z"); +assert.sameValue(new Temporal.ZonedDateTime(firstMinus1s.epochNanoseconds, tz).toString(), + "1911-03-10T23:59:59+00:09[Europe/Paris]"); +assert.sameValue(new Temporal.ZonedDateTime(firstMinus1s.epochNanoseconds, tz).offsetNanoseconds, + (9 * 60 + 21) * 1_000_000_000); + +next = tz.getNextTransition(firstMinus1s); +assert.sameValue(next.toString(), "1911-03-10T23:50:39Z"); +assert.sameValue(new Temporal.ZonedDateTime(next.epochNanoseconds, tz).toString(), + "1911-03-10T23:50:39+00:00[Europe/Paris]"); + +// Compute the next transition starting from the first transition minus 1ns. +let firstMinus1ns = first.add({nanoseconds: -1}); +assert.sameValue(firstMinus1ns.toString(), "1911-03-10T23:50:38.999999999Z"); +assert.sameValue(new Temporal.ZonedDateTime(firstMinus1ns.epochNanoseconds, tz).toString(), + "1911-03-10T23:59:59.999999999+00:09[Europe/Paris]"); +assert.sameValue(new Temporal.ZonedDateTime(firstMinus1ns.epochNanoseconds, tz).offsetNanoseconds, + (9 * 60 + 21) * 1_000_000_000); + +next = tz.getNextTransition(firstMinus1ns); +assert.sameValue(next.toString(), "1911-03-10T23:50:39Z"); +assert.sameValue(new Temporal.ZonedDateTime(next.epochNanoseconds, tz).toString(), + "1911-03-10T23:50:39+00:00[Europe/Paris]"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/transition-at-instant-boundaries.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/transition-at-instant-boundaries.js new file mode 100644 index 0000000000..6eb8645589 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getNextTransition/transition-at-instant-boundaries.js @@ -0,0 +1,29 @@ +// |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.timezone.prototype.getnexttransition +description: > + Test transitions at the instant boundaries. +features: [Temporal, Intl-enumeration] +---*/ + +const min = new Temporal.Instant(-86_40000_00000_00000_00000n); +const max = new Temporal.Instant(86_40000_00000_00000_00000n); + +for (let id of Intl.supportedValuesOf("timeZone")) { + let tz = new Temporal.TimeZone(id); + + // If there's any next transition, it should be after |min|. + let next = tz.getNextTransition(min); + if (next) { + assert(next.epochNanoseconds > min.epochNanoseconds); + } + + // There shouldn't be any next transition after |max|. + next = tz.getNextTransition(max); + assert.sameValue(next, null); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/instant-string.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/instant-string.js new file mode 100644 index 0000000000..943fb73750 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/instant-string.js @@ -0,0 +1,31 @@ +// |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.timezone.prototype.getoffsetnanosecondsfor +description: Conversion of ISO date-time strings to Temporal.Instant instances +features: [Temporal] +---*/ + +const instance = new Temporal.TimeZone("America/Vancouver"); + +let str = "1970-01-01T00:00"; +assert.throws(RangeError, () => instance.getOffsetNanosecondsFor(str), "bare date-time string is not an instant"); +str = "1970-01-01T00:00[America/Vancouver]"; +assert.throws(RangeError, () => instance.getOffsetNanosecondsFor(str), "date-time + IANA annotation is not an instant"); + +// The following are all valid strings so should not throw: + +const valids = [ + "1970-01-01T00:00Z", + "1970-01-01T00:00+01:00", + "1970-01-01T00:00Z[America/Vancouver]", + "1970-01-01T00:00+01:00[America/Vancouver]", +]; +for (const str of valids) { + const result = instance.getOffsetNanosecondsFor(str); + assert.sameValue(result, -28800e9); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/nanoseconds-subtracted-or-added-at-dst-transition.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/nanoseconds-subtracted-or-added-at-dst-transition.js new file mode 100644 index 0000000000..06b28bf8f7 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/nanoseconds-subtracted-or-added-at-dst-transition.js @@ -0,0 +1,40 @@ +// |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.timezone.prototype.getoffsetnanosecondsfor +description: > + Test offset when nanoseconds are subtracted or added from DST transition. +features: [Temporal, exponentiation] +---*/ + +// From <https://github.com/eggert/tz/blob/main/northamerica>: +// +// # Rule NAME FROM TO - IN ON AT SAVE LETTER +// Rule CA 1950 1966 - Apr lastSun 1:00 1:00 D +// +// # Zone NAME STDOFF RULES FORMAT [UNTIL] +// Zone America/Los_Angeles -7:52:58 - LMT 1883 Nov 18 12:07:02 +// -8:00 US P%sT 1946 +// -8:00 CA P%sT 1967 +// -8:00 US P%sT + +let tz = new Temporal.TimeZone("America/Los_Angeles"); +let p = Temporal.Instant.from("1965-04-25T09:00:00Z"); + +const nsPerHour = 60 * 60 * 1000**3; + +assert.sameValue(tz.getOffsetNanosecondsFor(p), + -7 * nsPerHour, + "DST transition"); + +assert.sameValue(tz.getOffsetNanosecondsFor(p.add({nanoseconds: +1})), + -7 * nsPerHour, + "DST transition plus one nanosecond"); + +assert.sameValue(tz.getOffsetNanosecondsFor(p.add({nanoseconds: -1})), + -8 * nsPerHour, + "DST transition minus one nanosecond"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetNanosecondsFor/shell.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/instant-string.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/instant-string.js new file mode 100644 index 0000000000..4454bd6eb3 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/instant-string.js @@ -0,0 +1,31 @@ +// |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.timezone.prototype.getoffsetstringfor +description: Conversion of ISO date-time strings to Temporal.TimeZone instances +features: [Temporal] +---*/ + +const instance = new Temporal.TimeZone("America/Vancouver"); + +let str = "1970-01-01T00:00"; +assert.throws(RangeError, () => instance.getOffsetStringFor(str), "bare date-time string is not an instant"); +str = "1970-01-01T00:00[America/Vancouver]"; +assert.throws(RangeError, () => instance.getOffsetStringFor(str), "date-time + IANA annotation is not an instant"); + +// The following are all valid strings so should not throw: + +const valids = [ + "1970-01-01T00:00Z", + "1970-01-01T00:00+01:00", + "1970-01-01T00:00Z[America/Vancouver]", + "1970-01-01T00:00+01:00[America/Vancouver]", +]; +for (const str of valids) { + const result = instance.getOffsetStringFor(str); + assert.sameValue(result, "-08:00"); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getOffsetStringFor/shell.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/basic.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/basic.js new file mode 100644 index 0000000000..c10f4676c4 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/basic.js @@ -0,0 +1,81 @@ +// |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.timezone.prototype.getplaindatetimefor +description: Sample of results for IANA time zones +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +function test(epochNs, results) { + const instant = new Temporal.Instant(epochNs); + Object.entries(results).forEach(([id, expected]) => { + const tz = new Temporal.TimeZone(id); + const dt = tz.getPlainDateTimeFor(instant); + TemporalHelpers.assertPlainDateTime(dt, ...expected, `Local time of ${instant} in ${id}`); + }); +} + +// Unix epoch +test(0n, { + 'America/Los_Angeles': [1969, 12, "M12", 31, 16, 0, 0, 0, 0, 0], + 'America/New_York': [1969, 12, "M12", 31, 19, 0, 0, 0, 0, 0], + 'Africa/Monrovia': [1969, 12, "M12", 31, 23, 15, 30, 0, 0, 0], + 'Europe/London': [1970, 1, "M01", 1, 1, 0, 0, 0, 0, 0], + 'Europe/Berlin': [1970, 1, "M01", 1, 1, 0, 0, 0, 0, 0], + 'Europe/Moscow': [1970, 1, "M01", 1, 3, 0, 0, 0, 0, 0], + 'Asia/Kolkata': [1970, 1, "M01", 1, 5, 30, 0, 0, 0, 0], + 'Asia/Tokyo': [1970, 1, "M01", 1, 9, 0, 0, 0, 0, 0], +}); + +// Just before epoch +test(-1n, { + 'America/Los_Angeles': [1969, 12, "M12", 31, 15, 59, 59, 999, 999, 999], + 'America/New_York': [1969, 12, "M12", 31, 18, 59, 59, 999, 999, 999], + 'Africa/Monrovia': [1969, 12, "M12", 31, 23, 15, 29, 999, 999, 999], + 'Europe/London': [1970, 1, "M01", 1, 0, 59, 59, 999, 999, 999], + 'Europe/Berlin': [1970, 1, "M01", 1, 0, 59, 59, 999, 999, 999], + 'Europe/Moscow': [1970, 1, "M01", 1, 2, 59, 59, 999, 999, 999], + 'Asia/Kolkata': [1970, 1, "M01", 1, 5, 29, 59, 999, 999, 999], + 'Asia/Tokyo': [1970, 1, "M01", 1, 8, 59, 59, 999, 999, 999], +}); + +// Just after epoch +test(1n, { + 'America/Los_Angeles': [1969, 12, "M12", 31, 16, 0, 0, 0, 0, 1], + 'America/New_York': [1969, 12, "M12", 31, 19, 0, 0, 0, 0, 1], + 'Africa/Monrovia': [1969, 12, "M12", 31, 23, 15, 30, 0, 0, 1], + 'Europe/London': [1970, 1, "M01", 1, 1, 0, 0, 0, 0, 1], + 'Europe/Berlin': [1970, 1, "M01", 1, 1, 0, 0, 0, 0, 1], + 'Europe/Moscow': [1970, 1, "M01", 1, 3, 0, 0, 0, 0, 1], + 'Asia/Kolkata': [1970, 1, "M01", 1, 5, 30, 0, 0, 0, 1], + 'Asia/Tokyo': [1970, 1, "M01", 1, 9, 0, 0, 0, 0, 1], +}); + +// Hours before epoch +test(-6300_000_000_001n, { + 'America/Los_Angeles': [1969, 12, "M12", 31, 14, 14, 59, 999, 999, 999], + 'America/New_York': [1969, 12, "M12", 31, 17, 14, 59, 999, 999, 999], + 'Africa/Monrovia': [1969, 12, "M12", 31, 21, 30, 29, 999, 999, 999], + 'Europe/London': [1969, 12, "M12", 31, 23, 14, 59, 999, 999, 999], + 'Europe/Berlin': [1969, 12, "M12", 31, 23, 14, 59, 999, 999, 999], + 'Europe/Moscow': [1970, 1, "M01", 1, 1, 14, 59, 999, 999, 999], + 'Asia/Kolkata': [1970, 1, "M01", 1, 3, 44, 59, 999, 999, 999], + 'Asia/Tokyo': [1970, 1, "M01", 1, 7, 14, 59, 999, 999, 999], +}); + +// Hours after epoch +test(6300_000_000_001n, { + 'America/Los_Angeles': [1969, 12, "M12", 31, 17, 45, 0, 0, 0, 1], + 'America/New_York': [1969, 12, "M12", 31, 20, 45, 0, 0, 0, 1], + 'Africa/Monrovia': [1970, 1, "M01", 1, 1, 0, 30, 0, 0, 1], + 'Europe/London': [1970, 1, "M01", 1, 2, 45, 0, 0, 0, 1], + 'Europe/Berlin': [1970, 1, "M01", 1, 2, 45, 0, 0, 0, 1], + 'Europe/Moscow': [1970, 1, "M01", 1, 4, 45, 0, 0, 0, 1], + 'Asia/Kolkata': [1970, 1, "M01", 1, 7, 15, 0, 0, 0, 1], + 'Asia/Tokyo': [1970, 1, "M01", 1, 10, 45, 0, 0, 0, 1], +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/dst.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/dst.js new file mode 100644 index 0000000000..4fb19513d4 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/dst.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.timezone.prototype.getplaindatetimefor +description: Sample of results for IANA time zones around DST changes +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +function test(epochNs, id, expected) { + const instant = new Temporal.Instant(epochNs); + const tz = new Temporal.TimeZone(id); + const dt = tz.getPlainDateTimeFor(instant); + TemporalHelpers.assertPlainDateTime(dt, ...expected, `Local time of ${instant} in ${id}`); +} + +// Just before DST forward shift +test(1553993999_999_999_999n, "Europe/London", [2019, 3, "M03", 31, 0, 59, 59, 999, 999, 999]); +// Just after DST forward shift +test(1553994000_000_000_000n, "Europe/London", [2019, 3, "M03", 31, 2, 0, 0, 0, 0, 0]); +// Just before DST backward shift +test(1550368799_999_999_999n, "America/Sao_Paulo", [2019, 2, "M02", 16, 23, 59, 59, 999, 999, 999]); +// Just after DST backward shift +test(1550368800_000_000_000n, "America/Sao_Paulo", [2019, 2, "M02", 16, 23, 0, 0, 0, 0, 0]); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/instant-string.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/instant-string.js new file mode 100644 index 0000000000..57cd6423c2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/instant-string.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.timezone.prototype.getplaindatetimefor +description: Conversion of ISO date-time strings to Temporal.Instant instances +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const instance = new Temporal.TimeZone("America/Vancouver"); + +let str = "1970-01-01T00:00"; +assert.throws(RangeError, () => instance.getPlainDateTimeFor(str), "bare date-time string is not an instant"); +str = "1970-01-01T00:00[America/Vancouver]"; +assert.throws(RangeError, () => instance.getPlainDateTimeFor(str), "date-time + IANA annotation is not an instant"); + +str = "1970-01-01T00:00Z"; +const result1 = instance.getPlainDateTimeFor(str); +TemporalHelpers.assertPlainDateTime(result1, 1969, 12, "M12", 31, 16, 0, 0, 0, 0, 0, "date-time + Z preserves exact time"); + +str = "1970-01-01T00:00+01:00"; +const result2 = instance.getPlainDateTimeFor(str); +TemporalHelpers.assertPlainDateTime(result2, 1969, 12, "M12", 31, 15, 0, 0, 0, 0, 0, "date-time + offset preserves exact time with offset"); + +str = "1970-01-01T00:00Z[America/Vancouver]"; +const result3 = instance.getPlainDateTimeFor(str); +TemporalHelpers.assertPlainDateTime(result3, 1969, 12, "M12", 31, 16, 0, 0, 0, 0, 0, "date-time + Z + IANA annotation ignores the IANA annotation"); + +str = "1970-01-01T00:00+01:00[America/Vancouver]"; +const result4 = instance.getPlainDateTimeFor(str); +TemporalHelpers.assertPlainDateTime(result4, 1969, 12, "M12", 31, 15, 0, 0, 0, 0, 0, "date-time + offset + IANA annotation ignores the IANA annotation"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/shell.js new file mode 100644 index 0000000000..60f74c2518 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPlainDateTimeFor/shell.js @@ -0,0 +1,2158 @@ +// GENERATED, DO NOT EDIT +// file: temporalHelpers.js +// Copyright (C) 2021 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + This defines helper objects and functions for testing Temporal. +defines: [TemporalHelpers] +features: [Symbol.species, Symbol.iterator, Temporal] +---*/ + +const ASCII_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/u; + +function formatPropertyName(propertyKey, objectName = "") { + switch (typeof propertyKey) { + case "symbol": + if (Symbol.keyFor(propertyKey) !== undefined) { + return `${objectName}[Symbol.for('${Symbol.keyFor(propertyKey)}')]`; + } else if (propertyKey.description.startsWith('Symbol.')) { + return `${objectName}[${propertyKey.description}]`; + } else { + return `${objectName}[Symbol('${propertyKey.description}')]` + } + case "string": + if (propertyKey !== String(Number(propertyKey))) { + if (ASCII_IDENTIFIER.test(propertyKey)) { + return objectName ? `${objectName}.${propertyKey}` : propertyKey; + } + return `${objectName}['${propertyKey.replace(/'/g, "\\'")}']` + } + // fall through + default: + // integer or string integer-index + return `${objectName}[${propertyKey}]`; + } +} + +const SKIP_SYMBOL = Symbol("Skip"); + +var TemporalHelpers = { + /* + * Codes and maximum lengths of months in the ISO 8601 calendar. + */ + ISOMonths: [ + { month: 1, monthCode: "M01", daysInMonth: 31 }, + { month: 2, monthCode: "M02", daysInMonth: 29 }, + { month: 3, monthCode: "M03", daysInMonth: 31 }, + { month: 4, monthCode: "M04", daysInMonth: 30 }, + { month: 5, monthCode: "M05", daysInMonth: 31 }, + { month: 6, monthCode: "M06", daysInMonth: 30 }, + { month: 7, monthCode: "M07", daysInMonth: 31 }, + { month: 8, monthCode: "M08", daysInMonth: 31 }, + { month: 9, monthCode: "M09", daysInMonth: 30 }, + { month: 10, monthCode: "M10", daysInMonth: 31 }, + { month: 11, monthCode: "M11", daysInMonth: 30 }, + { month: 12, monthCode: "M12", daysInMonth: 31 } + ], + + /* + * assertDuration(duration, years, ..., nanoseconds[, description]): + * + * Shorthand for asserting that each field of a Temporal.Duration is equal to + * an expected value. + */ + assertDuration(duration, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(duration instanceof Temporal.Duration, `${prefix}instanceof`); + assert.sameValue(duration.years, years, `${prefix}years result:`); + assert.sameValue(duration.months, months, `${prefix}months result:`); + assert.sameValue(duration.weeks, weeks, `${prefix}weeks result:`); + assert.sameValue(duration.days, days, `${prefix}days result:`); + assert.sameValue(duration.hours, hours, `${prefix}hours result:`); + assert.sameValue(duration.minutes, minutes, `${prefix}minutes result:`); + assert.sameValue(duration.seconds, seconds, `${prefix}seconds result:`); + assert.sameValue(duration.milliseconds, milliseconds, `${prefix}milliseconds result:`); + assert.sameValue(duration.microseconds, microseconds, `${prefix}microseconds result:`); + assert.sameValue(duration.nanoseconds, nanoseconds, `${prefix}nanoseconds result`); + }, + + /* + * assertDateDuration(duration, years, months, weeks, days, [, description]): + * + * Shorthand for asserting that each date field of a Temporal.Duration is + * equal to an expected value. + */ + assertDateDuration(duration, years, months, weeks, days, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(duration instanceof Temporal.Duration, `${prefix}instanceof`); + assert.sameValue(duration.years, years, `${prefix}years result:`); + assert.sameValue(duration.months, months, `${prefix}months result:`); + assert.sameValue(duration.weeks, weeks, `${prefix}weeks result:`); + assert.sameValue(duration.days, days, `${prefix}days result:`); + assert.sameValue(duration.hours, 0, `${prefix}hours result should be zero:`); + assert.sameValue(duration.minutes, 0, `${prefix}minutes result should be zero:`); + assert.sameValue(duration.seconds, 0, `${prefix}seconds result should be zero:`); + assert.sameValue(duration.milliseconds, 0, `${prefix}milliseconds result should be zero:`); + assert.sameValue(duration.microseconds, 0, `${prefix}microseconds result should be zero:`); + assert.sameValue(duration.nanoseconds, 0, `${prefix}nanoseconds result should be zero:`); + }, + + /* + * assertDurationsEqual(actual, expected[, description]): + * + * Shorthand for asserting that each field of a Temporal.Duration is equal to + * the corresponding field in another Temporal.Duration. + */ + assertDurationsEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.Duration, `${prefix}expected value should be a Temporal.Duration`); + TemporalHelpers.assertDuration(actual, expected.years, expected.months, expected.weeks, expected.days, expected.hours, expected.minutes, expected.seconds, expected.milliseconds, expected.microseconds, expected.nanoseconds, description); + }, + + /* + * assertInstantsEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.Instants are of the correct type + * and equal according to their equals() methods. + */ + assertInstantsEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.Instant, `${prefix}expected value should be a Temporal.Instant`); + assert(actual instanceof Temporal.Instant, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + }, + + /* + * assertPlainDate(date, year, ..., nanosecond[, description[, era, eraYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainDate is equal to + * an expected value. (Except the `calendar` property, since callers may want + * to assert either object equality with an object they put in there, or the + * value of date.calendarId.) + */ + assertPlainDate(date, year, month, monthCode, day, description = "", era = undefined, eraYear = undefined) { + const prefix = description ? `${description}: ` : ""; + assert(date instanceof Temporal.PlainDate, `${prefix}instanceof`); + assert.sameValue(date.era, era, `${prefix}era result:`); + assert.sameValue(date.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(date.year, year, `${prefix}year result:`); + assert.sameValue(date.month, month, `${prefix}month result:`); + assert.sameValue(date.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(date.day, day, `${prefix}day result:`); + }, + + /* + * assertPlainDateTime(datetime, year, ..., nanosecond[, description[, era, eraYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainDateTime is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of datetime.calendarId.) + */ + assertPlainDateTime(datetime, year, month, monthCode, day, hour, minute, second, millisecond, microsecond, nanosecond, description = "", era = undefined, eraYear = undefined) { + const prefix = description ? `${description}: ` : ""; + assert(datetime instanceof Temporal.PlainDateTime, `${prefix}instanceof`); + assert.sameValue(datetime.era, era, `${prefix}era result:`); + assert.sameValue(datetime.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(datetime.year, year, `${prefix}year result:`); + assert.sameValue(datetime.month, month, `${prefix}month result:`); + assert.sameValue(datetime.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(datetime.day, day, `${prefix}day result:`); + assert.sameValue(datetime.hour, hour, `${prefix}hour result:`); + assert.sameValue(datetime.minute, minute, `${prefix}minute result:`); + assert.sameValue(datetime.second, second, `${prefix}second result:`); + assert.sameValue(datetime.millisecond, millisecond, `${prefix}millisecond result:`); + assert.sameValue(datetime.microsecond, microsecond, `${prefix}microsecond result:`); + assert.sameValue(datetime.nanosecond, nanosecond, `${prefix}nanosecond result:`); + }, + + /* + * assertPlainDateTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.PlainDateTimes are of the correct + * type, equal according to their equals() methods, and additionally that + * their calendar internal slots are the same value. + */ + assertPlainDateTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.PlainDateTime, `${prefix}expected value should be a Temporal.PlainDateTime`); + assert(actual instanceof Temporal.PlainDateTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + assert.sameValue( + actual.getISOFields().calendar, + expected.getISOFields().calendar, + `${prefix}calendar same value:` + ); + }, + + /* + * assertPlainMonthDay(monthDay, monthCode, day[, description [, referenceISOYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainMonthDay is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of monthDay.calendarId().) + */ + assertPlainMonthDay(monthDay, monthCode, day, description = "", referenceISOYear = 1972) { + const prefix = description ? `${description}: ` : ""; + assert(monthDay instanceof Temporal.PlainMonthDay, `${prefix}instanceof`); + assert.sameValue(monthDay.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(monthDay.day, day, `${prefix}day result:`); + assert.sameValue(monthDay.getISOFields().isoYear, referenceISOYear, `${prefix}referenceISOYear result:`); + }, + + /* + * assertPlainTime(time, hour, ..., nanosecond[, description]): + * + * Shorthand for asserting that each field of a Temporal.PlainTime is equal to + * an expected value. + */ + assertPlainTime(time, hour, minute, second, millisecond, microsecond, nanosecond, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(time instanceof Temporal.PlainTime, `${prefix}instanceof`); + assert.sameValue(time.hour, hour, `${prefix}hour result:`); + assert.sameValue(time.minute, minute, `${prefix}minute result:`); + assert.sameValue(time.second, second, `${prefix}second result:`); + assert.sameValue(time.millisecond, millisecond, `${prefix}millisecond result:`); + assert.sameValue(time.microsecond, microsecond, `${prefix}microsecond result:`); + assert.sameValue(time.nanosecond, nanosecond, `${prefix}nanosecond result:`); + }, + + /* + * assertPlainTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.PlainTimes are of the correct + * type and equal according to their equals() methods. + */ + assertPlainTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.PlainTime, `${prefix}expected value should be a Temporal.PlainTime`); + assert(actual instanceof Temporal.PlainTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + }, + + /* + * assertPlainYearMonth(yearMonth, year, month, monthCode[, description[, era, eraYear, referenceISODay]]): + * + * Shorthand for asserting that each field of a Temporal.PlainYearMonth is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of yearMonth.calendarId.) + */ + assertPlainYearMonth(yearMonth, year, month, monthCode, description = "", era = undefined, eraYear = undefined, referenceISODay = 1) { + const prefix = description ? `${description}: ` : ""; + assert(yearMonth instanceof Temporal.PlainYearMonth, `${prefix}instanceof`); + assert.sameValue(yearMonth.era, era, `${prefix}era result:`); + assert.sameValue(yearMonth.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(yearMonth.year, year, `${prefix}year result:`); + assert.sameValue(yearMonth.month, month, `${prefix}month result:`); + assert.sameValue(yearMonth.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(yearMonth.getISOFields().isoDay, referenceISODay, `${prefix}referenceISODay result:`); + }, + + /* + * assertZonedDateTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.ZonedDateTimes are of the correct + * type, equal according to their equals() methods, and additionally that + * their time zones and calendar internal slots are the same value. + */ + assertZonedDateTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.ZonedDateTime, `${prefix}expected value should be a Temporal.ZonedDateTime`); + assert(actual instanceof Temporal.ZonedDateTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + assert.sameValue(actual.timeZone, expected.timeZone, `${prefix}time zone same value:`); + assert.sameValue( + actual.getISOFields().calendar, + expected.getISOFields().calendar, + `${prefix}calendar same value:` + ); + }, + + /* + * assertUnreachable(description): + * + * Helper for asserting that code is not executed. This is useful for + * assertions that methods of user calendars and time zones are not called. + */ + assertUnreachable(description) { + let message = "This code should not be executed"; + if (description) { + message = `${message}: ${description}`; + } + throw new Test262Error(message); + }, + + /* + * checkCalendarDateUntilLargestUnitSingular(func, expectedLargestUnitCalls): + * + * When an options object with a largestUnit property is synthesized inside + * Temporal and passed to user code such as calendar.dateUntil(), the value of + * the largestUnit property should be in the singular form, even if the input + * was given in the plural form. + * (This doesn't apply when the options object is passed through verbatim.) + * + * func(calendar, largestUnit, index) is the operation under test. It's called + * with an instance of a calendar that keeps track of which largestUnit is + * passed to dateUntil(), each key of expectedLargestUnitCalls in turn, and + * the key's numerical index in case the function needs to generate test data + * based on the index. At the end, the actual values passed to dateUntil() are + * compared with the array values of expectedLargestUnitCalls. + */ + checkCalendarDateUntilLargestUnitSingular(func, expectedLargestUnitCalls) { + const actual = []; + + class DateUntilOptionsCalendar extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateUntil(earlier, later, options) { + actual.push(options.largestUnit); + return super.dateUntil(earlier, later, options); + } + + toString() { + return "date-until-options"; + } + } + + const calendar = new DateUntilOptionsCalendar(); + Object.entries(expectedLargestUnitCalls).forEach(([largestUnit, expected], index) => { + func(calendar, largestUnit, index); + assert.compareArray(actual, expected, `largestUnit passed to calendar.dateUntil() for largestUnit ${largestUnit}`); + actual.splice(0); // empty it for the next check + }); + }, + + /* + * checkPlainDateTimeConversionFastPath(func): + * + * ToTemporalDate and ToTemporalTime should both, if given a + * Temporal.PlainDateTime instance, convert to the desired type by reading the + * PlainDateTime's internal slots, rather than calling any getters. + * + * func(datetime, calendar) is the actual operation to test, that must + * internally call the abstract operation ToTemporalDate or ToTemporalTime. + * It is passed a Temporal.PlainDateTime instance, as well as the instance's + * calendar object (so that it doesn't have to call the calendar getter itself + * if it wants to make any assertions about the calendar.) + */ + checkPlainDateTimeConversionFastPath(func, message = "checkPlainDateTimeConversionFastPath") { + const actual = []; + const expected = []; + + const calendar = new Temporal.Calendar("iso8601"); + const datetime = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321, calendar); + const prototypeDescrs = Object.getOwnPropertyDescriptors(Temporal.PlainDateTime.prototype); + ["year", "month", "monthCode", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"].forEach((property) => { + Object.defineProperty(datetime, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + const value = prototypeDescrs[property].get.call(this); + return { + toString() { + actual.push(`toString ${formatPropertyName(property)}`); + return value.toString(); + }, + valueOf() { + actual.push(`valueOf ${formatPropertyName(property)}`); + return value; + }, + }; + }, + }); + }); + Object.defineProperty(datetime, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(datetime, calendar); + assert.compareArray(actual, expected, `${message}: property getters not called`); + }, + + /* + * Check that an options bag that accepts units written in the singular form, + * also accepts the same units written in the plural form. + * func(unit) should call the method with the appropriate options bag + * containing unit as a value. This will be called twice for each element of + * validSingularUnits, once with singular and once with plural, and the + * results of each pair should be the same (whether a Temporal object or a + * primitive value.) + */ + checkPluralUnitsAccepted(func, validSingularUnits) { + const plurals = { + year: 'years', + month: 'months', + week: 'weeks', + day: 'days', + hour: 'hours', + minute: 'minutes', + second: 'seconds', + millisecond: 'milliseconds', + microsecond: 'microseconds', + nanosecond: 'nanoseconds', + }; + + validSingularUnits.forEach((unit) => { + const singularValue = func(unit); + const pluralValue = func(plurals[unit]); + const desc = `Plural ${plurals[unit]} produces the same result as singular ${unit}`; + if (singularValue instanceof Temporal.Duration) { + TemporalHelpers.assertDurationsEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.Instant) { + TemporalHelpers.assertInstantsEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.PlainDateTime) { + TemporalHelpers.assertPlainDateTimesEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.PlainTime) { + TemporalHelpers.assertPlainTimesEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.ZonedDateTime) { + TemporalHelpers.assertZonedDateTimesEqual(pluralValue, singularValue, desc); + } else { + assert.sameValue(pluralValue, singularValue); + } + }); + }, + + /* + * checkRoundingIncrementOptionWrongType(checkFunc, assertTrueResultFunc, assertObjectResultFunc): + * + * Checks the type handling of the roundingIncrement option. + * checkFunc(roundingIncrement) is a function which takes the value of + * roundingIncrement to test, and calls the method under test with it, + * returning the result. assertTrueResultFunc(result, description) should + * assert that result is the expected result with roundingIncrement: true, and + * assertObjectResultFunc(result, description) should assert that result is + * the expected result with roundingIncrement being an object with a valueOf() + * method. + */ + checkRoundingIncrementOptionWrongType(checkFunc, assertTrueResultFunc, assertObjectResultFunc) { + // null converts to 0, which is out of range + assert.throws(RangeError, () => checkFunc(null), "null"); + // Booleans convert to either 0 or 1, and 1 is allowed + const trueResult = checkFunc(true); + assertTrueResultFunc(trueResult, "true"); + assert.throws(RangeError, () => checkFunc(false), "false"); + // Symbols and BigInts cannot convert to numbers + assert.throws(TypeError, () => checkFunc(Symbol()), "symbol"); + assert.throws(TypeError, () => checkFunc(2n), "bigint"); + + // Objects prefer their valueOf() methods when converting to a number + assert.throws(RangeError, () => checkFunc({}), "plain object"); + + const expected = [ + "get roundingIncrement.valueOf", + "call roundingIncrement.valueOf", + ]; + const actual = []; + const observer = TemporalHelpers.toPrimitiveObserver(actual, 2, "roundingIncrement"); + const objectResult = checkFunc(observer); + assertObjectResultFunc(objectResult, "object with valueOf"); + assert.compareArray(actual, expected, "order of operations"); + }, + + /* + * checkStringOptionWrongType(propertyName, value, checkFunc, assertFunc): + * + * Checks the type handling of a string option, of which there are several in + * Temporal. + * propertyName is the name of the option, and value is the value that + * assertFunc should expect it to have. + * checkFunc(value) is a function which takes the value of the option to test, + * and calls the method under test with it, returning the result. + * assertFunc(result, description) should assert that result is the expected + * result with the option value being an object with a toString() method + * which returns the given value. + */ + checkStringOptionWrongType(propertyName, value, checkFunc, assertFunc) { + // null converts to the string "null", which is an invalid string value + assert.throws(RangeError, () => checkFunc(null), "null"); + // Booleans convert to the strings "true" or "false", which are invalid + assert.throws(RangeError, () => checkFunc(true), "true"); + assert.throws(RangeError, () => checkFunc(false), "false"); + // Symbols cannot convert to strings + assert.throws(TypeError, () => checkFunc(Symbol()), "symbol"); + // Numbers convert to strings which are invalid + assert.throws(RangeError, () => checkFunc(2), "number"); + // BigInts convert to strings which are invalid + assert.throws(RangeError, () => checkFunc(2n), "bigint"); + + // Objects prefer their toString() methods when converting to a string + assert.throws(RangeError, () => checkFunc({}), "plain object"); + + const expected = [ + `get ${propertyName}.toString`, + `call ${propertyName}.toString`, + ]; + const actual = []; + const observer = TemporalHelpers.toPrimitiveObserver(actual, value, propertyName); + const result = checkFunc(observer); + assertFunc(result, "object with toString"); + assert.compareArray(actual, expected, "order of operations"); + }, + + /* + * checkSubclassingIgnored(construct, constructArgs, method, methodArgs, + * resultAssertions): + * + * Methods of Temporal classes that return a new instance of the same class, + * must not take the constructor of a subclass into account, nor the @@species + * property. This helper runs tests to ensure this. + * + * construct(...constructArgs) must yield a valid instance of the Temporal + * class. instance[method](...methodArgs) is the method call under test, which + * must also yield a valid instance of the same Temporal class, not a + * subclass. See below for the individual tests that this runs. + * resultAssertions() is a function that performs additional assertions on the + * instance returned by the method under test. + */ + checkSubclassingIgnored(...args) { + this.checkSubclassConstructorNotObject(...args); + this.checkSubclassConstructorUndefined(...args); + this.checkSubclassConstructorThrows(...args); + this.checkSubclassConstructorNotCalled(...args); + this.checkSubclassSpeciesInvalidResult(...args); + this.checkSubclassSpeciesNotAConstructor(...args); + this.checkSubclassSpeciesNull(...args); + this.checkSubclassSpeciesUndefined(...args); + this.checkSubclassSpeciesThrows(...args); + }, + + /* + * Checks that replacing the 'constructor' property of the instance with + * various primitive values does not affect the returned new instance. + */ + checkSubclassConstructorNotObject(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = value; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + }, + + /* + * Checks that replacing the 'constructor' property of the subclass with + * undefined does not affect the returned new instance. + */ + checkSubclassConstructorUndefined(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = undefined; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Checks that making the 'constructor' property of the instance throw when + * called does not affect the returned new instance. + */ + checkSubclassConstructorThrows(construct, constructArgs, method, methodArgs, resultAssertions) { + function CustomError() {} + const instance = new construct(...constructArgs); + Object.defineProperty(instance, "constructor", { + get() { + throw new CustomError(); + } + }); + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Checks that when subclassing, the subclass constructor is not called by + * the method under test. + */ + checkSubclassConstructorNotCalled(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it's a + * constructor that returns a non-object value. + */ + checkSubclassSpeciesInvalidResult(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = { + [Symbol.species]: function() { + return value; + }, + }; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "plain object"); + }, + + /* + * Check that the constructor's @@species property is ignored when it's not a + * constructor. + */ + checkSubclassSpeciesNotAConstructor(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = { + [Symbol.species]: value, + }; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "plain object"); + }, + + /* + * Check that the constructor's @@species property is ignored when it's null. + */ + checkSubclassSpeciesNull(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = { + [Symbol.species]: null, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it's + * undefined. + */ + checkSubclassSpeciesUndefined(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = { + [Symbol.species]: undefined, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it throws, + * i.e. it is not called at all. + */ + checkSubclassSpeciesThrows(construct, constructArgs, method, methodArgs, resultAssertions) { + function CustomError() {} + + const instance = new construct(...constructArgs); + instance.constructor = { + get [Symbol.species]() { + throw new CustomError(); + }, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + }, + + /* + * checkSubclassingIgnoredStatic(construct, method, methodArgs, resultAssertions): + * + * Static methods of Temporal classes that return a new instance of the class, + * must not use the this-value as a constructor. This helper runs tests to + * ensure this. + * + * construct[method](...methodArgs) is the static method call under test, and + * must yield a valid instance of the Temporal class, not a subclass. See + * below for the individual tests that this runs. + * resultAssertions() is a function that performs additional assertions on the + * instance returned by the method under test. + */ + checkSubclassingIgnoredStatic(...args) { + this.checkStaticInvalidReceiver(...args); + this.checkStaticReceiverNotCalled(...args); + this.checkThisValueNotCalled(...args); + }, + + /* + * Check that calling the static method with a receiver that's not callable, + * still calls the intrinsic constructor. + */ + checkStaticInvalidReceiver(construct, method, methodArgs, resultAssertions) { + function check(value, description) { + const result = construct[method].apply(value, methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "Non-callable object"); + }, + + /* + * Check that calling the static method with a receiver that returns a value + * that's not callable, still calls the intrinsic constructor. + */ + checkStaticReceiverNotCalled(construct, method, methodArgs, resultAssertions) { + function check(value, description) { + const receiver = function () { + return value; + }; + const result = construct[method].apply(receiver, methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "Non-callable object"); + }, + + /* + * Check that the receiver isn't called. + */ + checkThisValueNotCalled(construct, method, methodArgs, resultAssertions) { + let called = false; + + class MySubclass extends construct { + constructor(...args) { + called = true; + super(...args); + } + } + + const result = MySubclass[method](...methodArgs); + assert.sameValue(called, false); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that any iterable returned from a custom time zone's + * getPossibleInstantsFor() method is exhausted. + * The custom time zone object is passed in to func(). + * expected is an array of strings representing the expected calls to the + * getPossibleInstantsFor() method. The PlainDateTimes that it is called with, + * are compared (using their toString() results) with the array. + */ + checkTimeZonePossibleInstantsIterable(func, expected) { + // A custom time zone that returns an iterable instead of an array from its + // getPossibleInstantsFor() method, and for testing purposes skips + // 00:00-01:00 UTC on January 1, 2030, and repeats 00:00-01:00 UTC+1 on + // January 3, 2030. Otherwise identical to the UTC time zone. + class TimeZonePossibleInstantsIterable extends Temporal.TimeZone { + constructor() { + super("UTC"); + this.getPossibleInstantsForCallCount = 0; + this.getPossibleInstantsForCalledWith = []; + this.getPossibleInstantsForReturns = []; + this.iteratorExhausted = []; + } + + toString() { + return "Custom/Iterable"; + } + + getOffsetNanosecondsFor(instant) { + if (Temporal.Instant.compare(instant, "2030-01-01T00:00Z") >= 0 && + Temporal.Instant.compare(instant, "2030-01-03T01:00Z") < 0) { + return 3600_000_000_000; + } else { + return 0; + } + } + + getPossibleInstantsFor(dateTime) { + this.getPossibleInstantsForCallCount++; + this.getPossibleInstantsForCalledWith.push(dateTime); + + // Fake DST transition + let retval = super.getPossibleInstantsFor(dateTime); + if (dateTime.toPlainDate().equals("2030-01-01") && dateTime.hour === 0) { + retval = []; + } else if (dateTime.toPlainDate().equals("2030-01-03") && dateTime.hour === 0) { + retval.push(retval[0].subtract({ hours: 1 })); + } else if (dateTime.year === 2030 && dateTime.month === 1 && dateTime.day >= 1 && dateTime.day <= 2) { + retval[0] = retval[0].subtract({ hours: 1 }); + } + + this.getPossibleInstantsForReturns.push(retval); + this.iteratorExhausted.push(false); + return { + callIndex: this.getPossibleInstantsForCallCount - 1, + timeZone: this, + *[Symbol.iterator]() { + yield* this.timeZone.getPossibleInstantsForReturns[this.callIndex]; + this.timeZone.iteratorExhausted[this.callIndex] = true; + }, + }; + } + } + + const timeZone = new TimeZonePossibleInstantsIterable(); + func(timeZone); + + assert.sameValue(timeZone.getPossibleInstantsForCallCount, expected.length, "getPossibleInstantsFor() method called correct number of times"); + + for (let index = 0; index < expected.length; index++) { + assert.sameValue(timeZone.getPossibleInstantsForCalledWith[index].toString(), expected[index], "getPossibleInstantsFor() called with expected PlainDateTime"); + assert(timeZone.iteratorExhausted[index], "iterated through the whole iterable"); + } + }, + + /* + * Check that any calendar-carrying Temporal object has its [[Calendar]] + * internal slot read by ToTemporalCalendar, and does not fetch the calendar + * by calling getters. + * The custom calendar object is passed in to func() so that it can do its + * own additional assertions involving the calendar if necessary. (Sometimes + * there is nothing to assert as the calendar isn't stored anywhere that can + * be asserted about.) + */ + checkToTemporalCalendarFastPath(func) { + class CalendarFastPathCheck extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(...args) { + return super.dateFromFields(...args).withCalendar(this); + } + + monthDayFromFields(...args) { + const { isoYear, isoMonth, isoDay } = super.monthDayFromFields(...args).getISOFields(); + return new Temporal.PlainMonthDay(isoMonth, isoDay, this, isoYear); + } + + yearMonthFromFields(...args) { + const { isoYear, isoMonth, isoDay } = super.yearMonthFromFields(...args).getISOFields(); + return new Temporal.PlainYearMonth(isoYear, isoMonth, this, isoDay); + } + + toString() { + return "fast-path-check"; + } + } + const calendar = new CalendarFastPathCheck(); + + const plainDate = new Temporal.PlainDate(2000, 5, 2, calendar); + const plainDateTime = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321, calendar); + const plainMonthDay = new Temporal.PlainMonthDay(5, 2, calendar); + const plainYearMonth = new Temporal.PlainYearMonth(2000, 5, calendar); + const zonedDateTime = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, "UTC", calendar); + + [plainDate, plainDateTime, plainMonthDay, plainYearMonth, zonedDateTime].forEach((temporalObject) => { + const actual = []; + const expected = []; + + Object.defineProperty(temporalObject, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(temporalObject, calendar); + assert.compareArray(actual, expected, "calendar getter not called"); + }); + }, + + checkToTemporalInstantFastPath(func) { + const actual = []; + const expected = []; + + const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, "UTC"); + Object.defineProperty(datetime, 'toString', { + get() { + actual.push("get toString"); + return function (options) { + actual.push("call toString"); + return Temporal.ZonedDateTime.prototype.toString.call(this, options); + }; + }, + }); + + func(datetime); + assert.compareArray(actual, expected, "toString not called"); + }, + + checkToTemporalPlainDateTimeFastPath(func) { + const actual = []; + const expected = []; + + const calendar = new Temporal.Calendar("iso8601"); + const date = new Temporal.PlainDate(2000, 5, 2, calendar); + const prototypeDescrs = Object.getOwnPropertyDescriptors(Temporal.PlainDate.prototype); + ["year", "month", "monthCode", "day"].forEach((property) => { + Object.defineProperty(date, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + const value = prototypeDescrs[property].get.call(this); + return TemporalHelpers.toPrimitiveObserver(actual, value, property); + }, + }); + }); + ["hour", "minute", "second", "millisecond", "microsecond", "nanosecond"].forEach((property) => { + Object.defineProperty(date, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + return undefined; + }, + }); + }); + Object.defineProperty(date, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(date, calendar); + assert.compareArray(actual, expected, "property getters not called"); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that the + * fromFields methods are always called with a null-prototype fields object. + */ + calendarCheckFieldsPrototypePollution() { + class CalendarCheckFieldsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + } + + // toString must remain "iso8601", so that some methods don't throw due to + // incompatible calendars + + dateFromFields(fields, options = {}) { + this.dateFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "dateFromFields should be called with null-prototype fields object"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options = {}) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "yearMonthFromFields should be called with null-prototype fields object"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options = {}) { + this.monthDayFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "monthDayFromFields should be called with null-prototype fields object"); + return super.monthDayFromFields(fields, options); + } + } + + return new CalendarCheckFieldsPrototypePollution(); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that the + * mergeFields() method is always called with null-prototype fields objects. + */ + calendarCheckMergeFieldsPrototypePollution() { + class CalendarCheckMergeFieldsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.mergeFieldsCallCount = 0; + } + + toString() { + return "merge-fields-null-proto"; + } + + mergeFields(fields, additionalFields) { + this.mergeFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "mergeFields should be called with null-prototype fields object (first argument)"); + assert.sameValue(Object.getPrototypeOf(additionalFields), null, "mergeFields should be called with null-prototype fields object (second argument)"); + return super.mergeFields(fields, additionalFields); + } + } + + return new CalendarCheckMergeFieldsPrototypePollution(); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that methods + * are always called with a null-prototype options object. + */ + calendarCheckOptionsPrototypePollution() { + class CalendarCheckOptionsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.yearMonthFromFieldsCallCount = 0; + this.dateUntilCallCount = 0; + } + + toString() { + return "options-null-proto"; + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(options), null, "yearMonthFromFields should be called with null-prototype options"); + return super.yearMonthFromFields(fields, options); + } + + dateUntil(one, two, options) { + this.dateUntilCallCount++; + assert.sameValue(Object.getPrototypeOf(options), null, "dateUntil should be called with null-prototype options"); + return super.dateUntil(one, two, options); + } + } + + return new CalendarCheckOptionsPrototypePollution(); + }, + + /* + * A custom calendar that asserts its dateAdd() method is called with the + * options parameter having the value undefined. + */ + calendarDateAddUndefinedOptions() { + class CalendarDateAddUndefinedOptions extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateAddCallCount = 0; + } + + toString() { + return "dateadd-undef-options"; + } + + dateAdd(date, duration, options) { + this.dateAddCallCount++; + assert.sameValue(options, undefined, "dateAdd shouldn't be called with options"); + return super.dateAdd(date, duration, options); + } + } + return new CalendarDateAddUndefinedOptions(); + }, + + /* + * A custom calendar that asserts its dateAdd() method is called with a + * PlainDate instance. Optionally, it also asserts that the PlainDate instance + * is the specific object `this.specificPlainDate`, if it is set by the + * calling code. + */ + calendarDateAddPlainDateInstance() { + class CalendarDateAddPlainDateInstance extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateAddCallCount = 0; + this.specificPlainDate = undefined; + } + + toString() { + return "dateadd-plain-date-instance"; + } + + dateFromFields(...args) { + return super.dateFromFields(...args).withCalendar(this); + } + + dateAdd(date, duration, options) { + this.dateAddCallCount++; + assert(date instanceof Temporal.PlainDate, "dateAdd() should be called with a PlainDate instance"); + if (this.dateAddCallCount === 1 && this.specificPlainDate) { + assert.sameValue(date, this.specificPlainDate, `dateAdd() should be called first with the specific PlainDate instance ${this.specificPlainDate}`); + } + return super.dateAdd(date, duration, options).withCalendar(this); + } + } + return new CalendarDateAddPlainDateInstance(); + }, + + /* + * A custom calendar that returns an iterable instead of an array from its + * fields() method, otherwise identical to the ISO calendar. + */ + calendarFieldsIterable() { + class CalendarFieldsIterable extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.fieldsCallCount = 0; + this.fieldsCalledWith = []; + this.iteratorExhausted = []; + } + + toString() { + return "fields-iterable"; + } + + fields(fieldNames) { + this.fieldsCallCount++; + this.fieldsCalledWith.push(fieldNames.slice()); + this.iteratorExhausted.push(false); + return { + callIndex: this.fieldsCallCount - 1, + calendar: this, + *[Symbol.iterator]() { + yield* this.calendar.fieldsCalledWith[this.callIndex]; + this.calendar.iteratorExhausted[this.callIndex] = true; + }, + }; + } + } + return new CalendarFieldsIterable(); + }, + + /* + * A custom calendar that asserts its ...FromFields() methods are called with + * the options parameter having the value undefined. + */ + calendarFromFieldsUndefinedOptions() { + class CalendarFromFieldsUndefinedOptions extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + } + + toString() { + return "from-fields-undef-options"; + } + + dateFromFields(fields, options) { + this.dateFromFieldsCallCount++; + assert.sameValue(options, undefined, "dateFromFields shouldn't be called with options"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(options, undefined, "yearMonthFromFields shouldn't be called with options"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + this.monthDayFromFieldsCallCount++; + assert.sameValue(options, undefined, "monthDayFromFields shouldn't be called with options"); + return super.monthDayFromFields(fields, options); + } + } + return new CalendarFromFieldsUndefinedOptions(); + }, + + /* + * A custom calendar that modifies the fields object passed in to + * dateFromFields, sabotaging its time properties. + */ + calendarMakeInfinityTime() { + class CalendarMakeInfinityTime extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(fields, options) { + const retval = super.dateFromFields(fields, options); + fields.hour = Infinity; + fields.minute = Infinity; + fields.second = Infinity; + fields.millisecond = Infinity; + fields.microsecond = Infinity; + fields.nanosecond = Infinity; + return retval; + } + } + return new CalendarMakeInfinityTime(); + }, + + /* + * A custom calendar that defines getters on the fields object passed into + * dateFromFields that throw, sabotaging its time properties. + */ + calendarMakeInvalidGettersTime() { + class CalendarMakeInvalidGettersTime extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(fields, options) { + const retval = super.dateFromFields(fields, options); + const throwingDescriptor = { + get() { + throw new Test262Error("reading a sabotaged time field"); + }, + }; + Object.defineProperties(fields, { + hour: throwingDescriptor, + minute: throwingDescriptor, + second: throwingDescriptor, + millisecond: throwingDescriptor, + microsecond: throwingDescriptor, + nanosecond: throwingDescriptor, + }); + return retval; + } + } + return new CalendarMakeInvalidGettersTime(); + }, + + /* + * A custom calendar whose mergeFields() method returns a proxy object with + * all of its Get and HasProperty operations observable, as well as adding a + * "shouldNotBeCopied": true property. + */ + calendarMergeFieldsGetters() { + class CalendarMergeFieldsGetters extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.mergeFieldsReturnOperations = []; + } + + toString() { + return "merge-fields-getters"; + } + + dateFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.monthDayFromFields(fields, options); + } + + mergeFields(fields, additionalFields) { + const retval = super.mergeFields(fields, additionalFields); + retval._calendar = this; + retval.shouldNotBeCopied = true; + return new Proxy(retval, { + get(target, key) { + target._calendar.mergeFieldsReturnOperations.push(`get ${key}`); + const result = target[key]; + if (result === undefined) { + return undefined; + } + return TemporalHelpers.toPrimitiveObserver(target._calendar.mergeFieldsReturnOperations, result, key); + }, + has(target, key) { + target._calendar.mergeFieldsReturnOperations.push(`has ${key}`); + return key in target; + }, + }); + } + } + return new CalendarMergeFieldsGetters(); + }, + + /* + * A custom calendar whose mergeFields() method returns a primitive value, + * given by @primitive, and which records the number of calls made to its + * dateFromFields(), yearMonthFromFields(), and monthDayFromFields() methods. + */ + calendarMergeFieldsReturnsPrimitive(primitive) { + class CalendarMergeFieldsPrimitive extends Temporal.Calendar { + constructor(mergeFieldsReturnValue) { + super("iso8601"); + this._mergeFieldsReturnValue = mergeFieldsReturnValue; + this.dateFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + } + + toString() { + return "merge-fields-primitive"; + } + + dateFromFields(fields, options) { + this.dateFromFieldsCallCount++; + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + this.monthDayFromFieldsCallCount++; + return super.monthDayFromFields(fields, options); + } + + mergeFields() { + return this._mergeFieldsReturnValue; + } + } + return new CalendarMergeFieldsPrimitive(primitive); + }, + + /* + * A custom calendar whose fields() method returns the same value as the + * iso8601 calendar, with the addition of extraFields provided as parameter. + */ + calendarWithExtraFields(fields) { + class CalendarWithExtraFields extends Temporal.Calendar { + constructor(extraFields) { + super("iso8601"); + this._extraFields = extraFields; + } + + fields(fieldNames) { + return super.fields(fieldNames).concat(this._extraFields); + } + } + + return new CalendarWithExtraFields(fields); + }, + + /* + * crossDateLineTimeZone(): + * + * This returns an instance of a custom time zone class that implements one + * single transition where the time zone moves from one side of the + * International Date Line to the other, for the purpose of testing time zone + * calculations without depending on system time zone data. + * + * The transition occurs at epoch second 1325239200 and goes from offset + * -10:00 to +14:00. In other words, the time zone skips the whole calendar + * day of 2011-12-30. This is the same as the real-life transition in the + * Pacific/Apia time zone. + */ + crossDateLineTimeZone() { + const { compare } = Temporal.PlainDate; + const skippedDay = new Temporal.PlainDate(2011, 12, 30); + const transitionEpoch = 1325239200_000_000_000n; + const beforeOffset = new Temporal.TimeZone("-10:00"); + const afterOffset = new Temporal.TimeZone("+14:00"); + + class CrossDateLineTimeZone extends Temporal.TimeZone { + constructor() { + super("+14:00"); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < transitionEpoch) { + return beforeOffset.getOffsetNanosecondsFor(instant); + } + return afterOffset.getOffsetNanosecondsFor(instant); + } + + getPossibleInstantsFor(datetime) { + const comparison = compare(datetime.toPlainDate(), skippedDay); + if (comparison === 0) { + return []; + } + if (comparison < 0) { + return [beforeOffset.getInstantFor(datetime)]; + } + return [afterOffset.getInstantFor(datetime)]; + } + + getPreviousTransition(instant) { + if (instant.epochNanoseconds > transitionEpoch) return new Temporal.Instant(transitionEpoch); + return null; + } + + getNextTransition(instant) { + if (instant.epochNanoseconds < transitionEpoch) return new Temporal.Instant(transitionEpoch); + return null; + } + + toString() { + return "Custom/Date_Line"; + } + } + return new CrossDateLineTimeZone(); + }, + + /* + * observeProperty(calls, object, propertyName, value): + * + * Defines an own property @object.@propertyName with value @value, that + * will log any calls to its accessors to the array @calls. + */ + observeProperty(calls, object, propertyName, value, objectName = "") { + Object.defineProperty(object, propertyName, { + get() { + calls.push(`get ${formatPropertyName(propertyName, objectName)}`); + return value; + }, + set(v) { + calls.push(`set ${formatPropertyName(propertyName, objectName)}`); + } + }); + }, + + /* + * observeMethod(calls, object, propertyName, value): + * + * Defines an own property @object.@propertyName with value @value, that + * will log any calls of @value to the array @calls. + */ + observeMethod(calls, object, propertyName, objectName = "") { + const method = object[propertyName]; + object[propertyName] = function () { + calls.push(`call ${formatPropertyName(propertyName, objectName)}`); + return method.apply(object, arguments); + }; + }, + + /* + * Used for substituteMethod to indicate default behavior instead of a + * substituted value + */ + SUBSTITUTE_SKIP: SKIP_SYMBOL, + + /* + * substituteMethod(object, propertyName, values): + * + * Defines an own property @object.@propertyName that will, for each + * subsequent call to the method previously defined as + * @object.@propertyName: + * - Call the method, if no more values remain + * - Call the method, if the value in @values for the corresponding call + * is SUBSTITUTE_SKIP + * - Otherwise, return the corresponding value in @value + */ + substituteMethod(object, propertyName, values) { + let calls = 0; + const method = object[propertyName]; + object[propertyName] = function () { + if (calls >= values.length) { + return method.apply(object, arguments); + } else if (values[calls] === SKIP_SYMBOL) { + calls++; + return method.apply(object, arguments); + } else { + return values[calls++]; + } + }; + }, + + /* + * calendarObserver: + * A custom calendar that behaves exactly like the ISO 8601 calendar but + * tracks calls to any of its methods, and Get/Has operations on its + * properties, by appending messages to an array. This is for the purpose of + * testing order of operations that are observable from user code. + * objectName is used in the log. + */ + calendarObserver(calls, objectName, methodOverrides = {}) { + function removeExtraHasPropertyChecks(objectName, calls) { + // Inserting the tracking calendar into the return values of methods + // that we chain up into the ISO calendar for, causes extra HasProperty + // checks, which we observe. This removes them so that we don't leak + // implementation details of the helper into the test code. + assert.sameValue(calls.pop(), `has ${objectName}.yearOfWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.yearMonthFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.year`); + assert.sameValue(calls.pop(), `has ${objectName}.weekOfYear`); + assert.sameValue(calls.pop(), `has ${objectName}.monthsInYear`); + assert.sameValue(calls.pop(), `has ${objectName}.monthDayFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.monthCode`); + assert.sameValue(calls.pop(), `has ${objectName}.month`); + assert.sameValue(calls.pop(), `has ${objectName}.mergeFields`); + assert.sameValue(calls.pop(), `has ${objectName}.inLeapYear`); + assert.sameValue(calls.pop(), `has ${objectName}.id`); + assert.sameValue(calls.pop(), `has ${objectName}.fields`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInYear`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInMonth`); + assert.sameValue(calls.pop(), `has ${objectName}.dayOfYear`); + assert.sameValue(calls.pop(), `has ${objectName}.dayOfWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.day`); + assert.sameValue(calls.pop(), `has ${objectName}.dateUntil`); + assert.sameValue(calls.pop(), `has ${objectName}.dateFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.dateAdd`); + } + + const iso8601 = new Temporal.Calendar("iso8601"); + const trackingMethods = { + dateFromFields(...args) { + calls.push(`call ${objectName}.dateFromFields`); + if ('dateFromFields' in methodOverrides) { + const value = methodOverrides.dateFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.dateFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainDate(isoYear, isoMonth, isoDay, this); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + yearMonthFromFields(...args) { + calls.push(`call ${objectName}.yearMonthFromFields`); + if ('yearMonthFromFields' in methodOverrides) { + const value = methodOverrides.yearMonthFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.yearMonthFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainYearMonth(isoYear, isoMonth, this, isoDay); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + monthDayFromFields(...args) { + calls.push(`call ${objectName}.monthDayFromFields`); + if ('monthDayFromFields' in methodOverrides) { + const value = methodOverrides.monthDayFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.monthDayFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainMonthDay(isoMonth, isoDay, this, isoYear); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + dateAdd(...args) { + calls.push(`call ${objectName}.dateAdd`); + if ('dateAdd' in methodOverrides) { + const value = methodOverrides.dateAdd; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.dateAdd(...args); + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainDate(isoYear, isoMonth, isoDay, this); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + id: "iso8601", + }; + // Automatically generate the other methods that don't need any custom code + [ + "dateUntil", + "day", + "dayOfWeek", + "dayOfYear", + "daysInMonth", + "daysInWeek", + "daysInYear", + "era", + "eraYear", + "fields", + "inLeapYear", + "mergeFields", + "month", + "monthCode", + "monthsInYear", + "toString", + "weekOfYear", + "year", + "yearOfWeek", + ].forEach((methodName) => { + trackingMethods[methodName] = function (...args) { + calls.push(`call ${formatPropertyName(methodName, objectName)}`); + if (methodName in methodOverrides) { + const value = methodOverrides[methodName]; + return typeof value === "function" ? value(...args) : value; + } + return iso8601[methodName](...args); + }; + }); + return new Proxy(trackingMethods, { + get(target, key, receiver) { + const result = Reflect.get(target, key, receiver); + calls.push(`get ${formatPropertyName(key, objectName)}`); + return result; + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * A custom calendar that does not allow any of its methods to be called, for + * the purpose of asserting that a particular operation does not call into + * user code. + */ + calendarThrowEverything() { + class CalendarThrowEverything extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + toString() { + TemporalHelpers.assertUnreachable("toString should not be called"); + } + dateFromFields() { + TemporalHelpers.assertUnreachable("dateFromFields should not be called"); + } + yearMonthFromFields() { + TemporalHelpers.assertUnreachable("yearMonthFromFields should not be called"); + } + monthDayFromFields() { + TemporalHelpers.assertUnreachable("monthDayFromFields should not be called"); + } + dateAdd() { + TemporalHelpers.assertUnreachable("dateAdd should not be called"); + } + dateUntil() { + TemporalHelpers.assertUnreachable("dateUntil should not be called"); + } + era() { + TemporalHelpers.assertUnreachable("era should not be called"); + } + eraYear() { + TemporalHelpers.assertUnreachable("eraYear should not be called"); + } + year() { + TemporalHelpers.assertUnreachable("year should not be called"); + } + month() { + TemporalHelpers.assertUnreachable("month should not be called"); + } + monthCode() { + TemporalHelpers.assertUnreachable("monthCode should not be called"); + } + day() { + TemporalHelpers.assertUnreachable("day should not be called"); + } + fields() { + TemporalHelpers.assertUnreachable("fields should not be called"); + } + mergeFields() { + TemporalHelpers.assertUnreachable("mergeFields should not be called"); + } + } + + return new CalendarThrowEverything(); + }, + + /* + * oneShiftTimeZone(shiftInstant, shiftNanoseconds): + * + * In the case of a spring-forward time zone offset transition (skipped time), + * and disambiguation === 'earlier', BuiltinTimeZoneGetInstantFor subtracts a + * negative number of nanoseconds from a PlainDateTime, which should balance + * with the microseconds field. + * + * This returns an instance of a custom time zone class which skips a length + * of time equal to shiftNanoseconds (a number), at the Temporal.Instant + * shiftInstant. Before shiftInstant, it's identical to UTC, and after + * shiftInstant it's a constant-offset time zone. + * + * It provides a getPossibleInstantsForCalledWith member which is an array + * with the result of calling toString() on any PlainDateTimes passed to + * getPossibleInstantsFor(). + */ + oneShiftTimeZone(shiftInstant, shiftNanoseconds) { + class OneShiftTimeZone extends Temporal.TimeZone { + constructor(shiftInstant, shiftNanoseconds) { + super("+00:00"); + this._shiftInstant = shiftInstant; + this._epoch1 = shiftInstant.epochNanoseconds; + this._epoch2 = this._epoch1 + BigInt(shiftNanoseconds); + this._shiftNanoseconds = shiftNanoseconds; + this._shift = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, this._shiftNanoseconds); + this.getPossibleInstantsForCalledWith = []; + } + + _isBeforeShift(instant) { + return instant.epochNanoseconds < this._epoch1; + } + + getOffsetNanosecondsFor(instant) { + return this._isBeforeShift(instant) ? 0 : this._shiftNanoseconds; + } + + getPossibleInstantsFor(plainDateTime) { + this.getPossibleInstantsForCalledWith.push(plainDateTime.toString({ calendarName: "never" })); + const [instant] = super.getPossibleInstantsFor(plainDateTime); + if (this._shiftNanoseconds > 0) { + if (this._isBeforeShift(instant)) return [instant]; + if (instant.epochNanoseconds < this._epoch2) return []; + return [instant.subtract(this._shift)]; + } + if (instant.epochNanoseconds < this._epoch2) return [instant]; + const shifted = instant.subtract(this._shift); + if (this._isBeforeShift(instant)) return [instant, shifted]; + return [shifted]; + } + + getNextTransition(instant) { + return this._isBeforeShift(instant) ? this._shiftInstant : null; + } + + getPreviousTransition(instant) { + return this._isBeforeShift(instant) ? null : this._shiftInstant; + } + + toString() { + return "Custom/One_Shift"; + } + } + return new OneShiftTimeZone(shiftInstant, shiftNanoseconds); + }, + + /* + * propertyBagObserver(): + * Returns an object that behaves like the given propertyBag but tracks Get + * and Has operations on any of its properties, by appending messages to an + * array. If the value of a property in propertyBag is a primitive, the value + * of the returned object's property will additionally be a + * TemporalHelpers.toPrimitiveObserver that will track calls to its toString + * and valueOf methods in the same array. This is for the purpose of testing + * order of operations that are observable from user code. objectName is used + * in the log. + */ + propertyBagObserver(calls, propertyBag, objectName) { + return new Proxy(propertyBag, { + ownKeys(target) { + calls.push(`ownKeys ${objectName}`); + return Reflect.ownKeys(target); + }, + getOwnPropertyDescriptor(target, key) { + calls.push(`getOwnPropertyDescriptor ${formatPropertyName(key, objectName)}`); + return Reflect.getOwnPropertyDescriptor(target, key); + }, + get(target, key, receiver) { + calls.push(`get ${formatPropertyName(key, objectName)}`); + const result = Reflect.get(target, key, receiver); + if (result === undefined) { + return undefined; + } + if ((result !== null && typeof result === "object") || typeof result === "function") { + return result; + } + return TemporalHelpers.toPrimitiveObserver(calls, result, `${formatPropertyName(key, objectName)}`); + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * specificOffsetTimeZone(): + * + * This returns an instance of a custom time zone class, which returns a + * specific custom value from its getOffsetNanosecondsFrom() method. This is + * for the purpose of testing the validation of what this method returns. + * + * It also returns an empty array from getPossibleInstantsFor(), so as to + * trigger calls to getOffsetNanosecondsFor() when used from the + * BuiltinTimeZoneGetInstantFor operation. + */ + specificOffsetTimeZone(offsetValue) { + class SpecificOffsetTimeZone extends Temporal.TimeZone { + constructor(offsetValue) { + super("UTC"); + this._offsetValue = offsetValue; + } + + getOffsetNanosecondsFor() { + return this._offsetValue; + } + + getPossibleInstantsFor(dt) { + if (typeof this._offsetValue !== 'number' || Math.abs(this._offsetValue) >= 86400e9 || isNaN(this._offsetValue)) return []; + const zdt = dt.toZonedDateTime("UTC").add({ nanoseconds: -this._offsetValue }); + return [zdt.toInstant()]; + } + + get id() { + return this.getOffsetStringFor(new Temporal.Instant(0n)); + } + } + return new SpecificOffsetTimeZone(offsetValue); + }, + + /* + * springForwardFallBackTimeZone(): + * + * This returns an instance of a custom time zone class that implements one + * single spring-forward/fall-back transition, for the purpose of testing the + * disambiguation option, without depending on system time zone data. + * + * The spring-forward occurs at epoch second 954669600 (2000-04-02T02:00 + * local) and goes from offset -08:00 to -07:00. + * + * The fall-back occurs at epoch second 972810000 (2000-10-29T02:00 local) and + * goes from offset -07:00 to -08:00. + */ + springForwardFallBackTimeZone() { + const { compare } = Temporal.PlainDateTime; + const springForwardLocal = new Temporal.PlainDateTime(2000, 4, 2, 2); + const springForwardEpoch = 954669600_000_000_000n; + const fallBackLocal = new Temporal.PlainDateTime(2000, 10, 29, 1); + const fallBackEpoch = 972810000_000_000_000n; + const winterOffset = new Temporal.TimeZone('-08:00'); + const summerOffset = new Temporal.TimeZone('-07:00'); + + class SpringForwardFallBackTimeZone extends Temporal.TimeZone { + constructor() { + super("-08:00"); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < springForwardEpoch || + instant.epochNanoseconds >= fallBackEpoch) { + return winterOffset.getOffsetNanosecondsFor(instant); + } + return summerOffset.getOffsetNanosecondsFor(instant); + } + + getPossibleInstantsFor(datetime) { + if (compare(datetime, springForwardLocal) >= 0 && compare(datetime, springForwardLocal.add({ hours: 1 })) < 0) { + return []; + } + if (compare(datetime, fallBackLocal) >= 0 && compare(datetime, fallBackLocal.add({ hours: 1 })) < 0) { + return [summerOffset.getInstantFor(datetime), winterOffset.getInstantFor(datetime)]; + } + if (compare(datetime, springForwardLocal) < 0 || compare(datetime, fallBackLocal) >= 0) { + return [winterOffset.getInstantFor(datetime)]; + } + return [summerOffset.getInstantFor(datetime)]; + } + + getPreviousTransition(instant) { + if (instant.epochNanoseconds > fallBackEpoch) return new Temporal.Instant(fallBackEpoch); + if (instant.epochNanoseconds > springForwardEpoch) return new Temporal.Instant(springForwardEpoch); + return null; + } + + getNextTransition(instant) { + if (instant.epochNanoseconds < springForwardEpoch) return new Temporal.Instant(springForwardEpoch); + if (instant.epochNanoseconds < fallBackEpoch) return new Temporal.Instant(fallBackEpoch); + return null; + } + + get id() { + return "Custom/Spring_Fall"; + } + + toString() { + return "Custom/Spring_Fall"; + } + } + return new SpringForwardFallBackTimeZone(); + }, + + /* + * timeZoneObserver: + * A custom calendar that behaves exactly like the UTC time zone but tracks + * calls to any of its methods, and Get/Has operations on its properties, by + * appending messages to an array. This is for the purpose of testing order of + * operations that are observable from user code. objectName is used in the + * log. methodOverrides is an optional object containing properties with the + * same name as Temporal.TimeZone methods. If the property value is a function + * it will be called with the proper arguments instead of the UTC method. + * Otherwise, the property value will be returned directly. + */ + timeZoneObserver(calls, objectName, methodOverrides = {}) { + const utc = new Temporal.TimeZone("UTC"); + const trackingMethods = { + id: "UTC", + }; + // Automatically generate the methods + ["getOffsetNanosecondsFor", "getPossibleInstantsFor", "toString"].forEach((methodName) => { + trackingMethods[methodName] = function (...args) { + calls.push(`call ${formatPropertyName(methodName, objectName)}`); + if (methodName in methodOverrides) { + const value = methodOverrides[methodName]; + return typeof value === "function" ? value(...args) : value; + } + return utc[methodName](...args); + }; + }); + return new Proxy(trackingMethods, { + get(target, key, receiver) { + const result = Reflect.get(target, key, receiver); + calls.push(`get ${formatPropertyName(key, objectName)}`); + return result; + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * A custom time zone that does not allow any of its methods to be called, for + * the purpose of asserting that a particular operation does not call into + * user code. + */ + timeZoneThrowEverything() { + class TimeZoneThrowEverything extends Temporal.TimeZone { + constructor() { + super("UTC"); + } + getOffsetNanosecondsFor() { + TemporalHelpers.assertUnreachable("getOffsetNanosecondsFor should not be called"); + } + getPossibleInstantsFor() { + TemporalHelpers.assertUnreachable("getPossibleInstantsFor should not be called"); + } + toString() { + TemporalHelpers.assertUnreachable("toString should not be called"); + } + } + + return new TimeZoneThrowEverything(); + }, + + /* + * Returns an object that will append logs of any Gets or Calls of its valueOf + * or toString properties to the array calls. Both valueOf and toString will + * return the actual primitiveValue. propertyName is used in the log. + */ + toPrimitiveObserver(calls, primitiveValue, propertyName) { + return { + get valueOf() { + calls.push(`get ${propertyName}.valueOf`); + return function () { + calls.push(`call ${propertyName}.valueOf`); + return primitiveValue; + }; + }, + get toString() { + calls.push(`get ${propertyName}.toString`); + return function () { + calls.push(`call ${propertyName}.toString`); + if (primitiveValue === undefined) return undefined; + return primitiveValue.toString(); + }; + }, + }; + }, + + /* + * An object containing further methods that return arrays of ISO strings, for + * testing parsers. + */ + ISO: { + /* + * PlainMonthDay strings that are not valid. + */ + plainMonthDayStringsInvalid() { + return [ + "11-18junk", + "11-18[u-ca=gregory]", + "11-18[u-ca=hebrew]", + ]; + }, + + /* + * PlainMonthDay strings that are valid and that should produce October 1st. + */ + plainMonthDayStringsValid() { + return [ + "10-01", + "1001", + "1965-10-01", + "1976-10-01T152330.1+00:00", + "19761001T15:23:30.1+00:00", + "1976-10-01T15:23:30.1+0000", + "1976-10-01T152330.1+0000", + "19761001T15:23:30.1+0000", + "19761001T152330.1+00:00", + "19761001T152330.1+0000", + "+001976-10-01T152330.1+00:00", + "+0019761001T15:23:30.1+00:00", + "+001976-10-01T15:23:30.1+0000", + "+001976-10-01T152330.1+0000", + "+0019761001T15:23:30.1+0000", + "+0019761001T152330.1+00:00", + "+0019761001T152330.1+0000", + "1976-10-01T15:23:00", + "1976-10-01T15:23", + "1976-10-01T15", + "1976-10-01", + "--10-01", + "--1001", + ]; + }, + + /* + * PlainTime strings that may be mistaken for PlainMonthDay or + * PlainYearMonth strings, and so require a time designator. + */ + plainTimeStringsAmbiguous() { + const ambiguousStrings = [ + "2021-12", // ambiguity between YYYY-MM and HHMM-UU + "2021-12[-12:00]", // ditto, TZ does not disambiguate + "1214", // ambiguity between MMDD and HHMM + "0229", // ditto, including MMDD that doesn't occur every year + "1130", // ditto, including DD that doesn't occur in every month + "12-14", // ambiguity between MM-DD and HH-UU + "12-14[-14:00]", // ditto, TZ does not disambiguate + "202112", // ambiguity between YYYYMM and HHMMSS + "202112[UTC]", // ditto, TZ does not disambiguate + ]; + // Adding a calendar annotation to one of these strings must not cause + // disambiguation in favour of time. + const stringsWithCalendar = ambiguousStrings.map((s) => s + '[u-ca=iso8601]'); + return ambiguousStrings.concat(stringsWithCalendar); + }, + + /* + * PlainTime strings that are of similar form to PlainMonthDay and + * PlainYearMonth strings, but are not ambiguous due to components that + * aren't valid as months or days. + */ + plainTimeStringsUnambiguous() { + return [ + "2021-13", // 13 is not a month + "202113", // ditto + "2021-13[-13:00]", // ditto + "202113[-13:00]", // ditto + "0000-00", // 0 is not a month + "000000", // ditto + "0000-00[UTC]", // ditto + "000000[UTC]", // ditto + "1314", // 13 is not a month + "13-14", // ditto + "1232", // 32 is not a day + "0230", // 30 is not a day in February + "0631", // 31 is not a day in June + "0000", // 0 is neither a month nor a day + "00-00", // ditto + ]; + }, + + /* + * PlainYearMonth-like strings that are not valid. + */ + plainYearMonthStringsInvalid() { + return [ + "2020-13", + ]; + }, + + /* + * PlainYearMonth-like strings that are valid and should produce November + * 1976 in the ISO 8601 calendar. + */ + plainYearMonthStringsValid() { + return [ + "1976-11", + "1976-11-10", + "1976-11-01T09:00:00+00:00", + "1976-11-01T00:00:00+05:00", + "197611", + "+00197611", + "1976-11-18T15:23:30.1\u221202:00", + "1976-11-18T152330.1+00:00", + "19761118T15:23:30.1+00:00", + "1976-11-18T15:23:30.1+0000", + "1976-11-18T152330.1+0000", + "19761118T15:23:30.1+0000", + "19761118T152330.1+00:00", + "19761118T152330.1+0000", + "+001976-11-18T152330.1+00:00", + "+0019761118T15:23:30.1+00:00", + "+001976-11-18T15:23:30.1+0000", + "+001976-11-18T152330.1+0000", + "+0019761118T15:23:30.1+0000", + "+0019761118T152330.1+00:00", + "+0019761118T152330.1+0000", + "1976-11-18T15:23", + "1976-11-18T15", + "1976-11-18", + ]; + }, + + /* + * PlainYearMonth-like strings that are valid and should produce November of + * the ISO year -9999. + */ + plainYearMonthStringsValidNegativeYear() { + return [ + "\u2212009999-11", + ]; + }, + } +}; diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/infinity-throws-rangeerror.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/infinity-throws-rangeerror.js new file mode 100644 index 0000000000..017b61be76 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/infinity-throws-rangeerror.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. + +/*--- +description: Throws if eraYear in the property bag is Infinity or -Infinity +esid: sec-temporal.timezone.prototype.getpossibleinstantsfor +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +const instance = new Temporal.TimeZone("UTC"); +const base = { era: "ad", month: 5, day: 2, hour: 15, calendar: "gregory" }; + +[Infinity, -Infinity].forEach((inf) => { + assert.throws(RangeError, () => instance.getPossibleInstantsFor({ ...base, eraYear: inf }), `eraYear property cannot be ${inf}`); + + const calls = []; + const obj = TemporalHelpers.toPrimitiveObserver(calls, inf, "eraYear"); + assert.throws(RangeError, () => instance.getPossibleInstantsFor({ ...base, eraYear: obj })); + assert.compareArray(calls, ["get eraYear.valueOf", "call eraYear.valueOf"], "it fails after fetching the primitive value"); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/shell.js new file mode 100644 index 0000000000..60f74c2518 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPossibleInstantsFor/shell.js @@ -0,0 +1,2158 @@ +// GENERATED, DO NOT EDIT +// file: temporalHelpers.js +// Copyright (C) 2021 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + This defines helper objects and functions for testing Temporal. +defines: [TemporalHelpers] +features: [Symbol.species, Symbol.iterator, Temporal] +---*/ + +const ASCII_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/u; + +function formatPropertyName(propertyKey, objectName = "") { + switch (typeof propertyKey) { + case "symbol": + if (Symbol.keyFor(propertyKey) !== undefined) { + return `${objectName}[Symbol.for('${Symbol.keyFor(propertyKey)}')]`; + } else if (propertyKey.description.startsWith('Symbol.')) { + return `${objectName}[${propertyKey.description}]`; + } else { + return `${objectName}[Symbol('${propertyKey.description}')]` + } + case "string": + if (propertyKey !== String(Number(propertyKey))) { + if (ASCII_IDENTIFIER.test(propertyKey)) { + return objectName ? `${objectName}.${propertyKey}` : propertyKey; + } + return `${objectName}['${propertyKey.replace(/'/g, "\\'")}']` + } + // fall through + default: + // integer or string integer-index + return `${objectName}[${propertyKey}]`; + } +} + +const SKIP_SYMBOL = Symbol("Skip"); + +var TemporalHelpers = { + /* + * Codes and maximum lengths of months in the ISO 8601 calendar. + */ + ISOMonths: [ + { month: 1, monthCode: "M01", daysInMonth: 31 }, + { month: 2, monthCode: "M02", daysInMonth: 29 }, + { month: 3, monthCode: "M03", daysInMonth: 31 }, + { month: 4, monthCode: "M04", daysInMonth: 30 }, + { month: 5, monthCode: "M05", daysInMonth: 31 }, + { month: 6, monthCode: "M06", daysInMonth: 30 }, + { month: 7, monthCode: "M07", daysInMonth: 31 }, + { month: 8, monthCode: "M08", daysInMonth: 31 }, + { month: 9, monthCode: "M09", daysInMonth: 30 }, + { month: 10, monthCode: "M10", daysInMonth: 31 }, + { month: 11, monthCode: "M11", daysInMonth: 30 }, + { month: 12, monthCode: "M12", daysInMonth: 31 } + ], + + /* + * assertDuration(duration, years, ..., nanoseconds[, description]): + * + * Shorthand for asserting that each field of a Temporal.Duration is equal to + * an expected value. + */ + assertDuration(duration, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(duration instanceof Temporal.Duration, `${prefix}instanceof`); + assert.sameValue(duration.years, years, `${prefix}years result:`); + assert.sameValue(duration.months, months, `${prefix}months result:`); + assert.sameValue(duration.weeks, weeks, `${prefix}weeks result:`); + assert.sameValue(duration.days, days, `${prefix}days result:`); + assert.sameValue(duration.hours, hours, `${prefix}hours result:`); + assert.sameValue(duration.minutes, minutes, `${prefix}minutes result:`); + assert.sameValue(duration.seconds, seconds, `${prefix}seconds result:`); + assert.sameValue(duration.milliseconds, milliseconds, `${prefix}milliseconds result:`); + assert.sameValue(duration.microseconds, microseconds, `${prefix}microseconds result:`); + assert.sameValue(duration.nanoseconds, nanoseconds, `${prefix}nanoseconds result`); + }, + + /* + * assertDateDuration(duration, years, months, weeks, days, [, description]): + * + * Shorthand for asserting that each date field of a Temporal.Duration is + * equal to an expected value. + */ + assertDateDuration(duration, years, months, weeks, days, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(duration instanceof Temporal.Duration, `${prefix}instanceof`); + assert.sameValue(duration.years, years, `${prefix}years result:`); + assert.sameValue(duration.months, months, `${prefix}months result:`); + assert.sameValue(duration.weeks, weeks, `${prefix}weeks result:`); + assert.sameValue(duration.days, days, `${prefix}days result:`); + assert.sameValue(duration.hours, 0, `${prefix}hours result should be zero:`); + assert.sameValue(duration.minutes, 0, `${prefix}minutes result should be zero:`); + assert.sameValue(duration.seconds, 0, `${prefix}seconds result should be zero:`); + assert.sameValue(duration.milliseconds, 0, `${prefix}milliseconds result should be zero:`); + assert.sameValue(duration.microseconds, 0, `${prefix}microseconds result should be zero:`); + assert.sameValue(duration.nanoseconds, 0, `${prefix}nanoseconds result should be zero:`); + }, + + /* + * assertDurationsEqual(actual, expected[, description]): + * + * Shorthand for asserting that each field of a Temporal.Duration is equal to + * the corresponding field in another Temporal.Duration. + */ + assertDurationsEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.Duration, `${prefix}expected value should be a Temporal.Duration`); + TemporalHelpers.assertDuration(actual, expected.years, expected.months, expected.weeks, expected.days, expected.hours, expected.minutes, expected.seconds, expected.milliseconds, expected.microseconds, expected.nanoseconds, description); + }, + + /* + * assertInstantsEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.Instants are of the correct type + * and equal according to their equals() methods. + */ + assertInstantsEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.Instant, `${prefix}expected value should be a Temporal.Instant`); + assert(actual instanceof Temporal.Instant, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + }, + + /* + * assertPlainDate(date, year, ..., nanosecond[, description[, era, eraYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainDate is equal to + * an expected value. (Except the `calendar` property, since callers may want + * to assert either object equality with an object they put in there, or the + * value of date.calendarId.) + */ + assertPlainDate(date, year, month, monthCode, day, description = "", era = undefined, eraYear = undefined) { + const prefix = description ? `${description}: ` : ""; + assert(date instanceof Temporal.PlainDate, `${prefix}instanceof`); + assert.sameValue(date.era, era, `${prefix}era result:`); + assert.sameValue(date.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(date.year, year, `${prefix}year result:`); + assert.sameValue(date.month, month, `${prefix}month result:`); + assert.sameValue(date.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(date.day, day, `${prefix}day result:`); + }, + + /* + * assertPlainDateTime(datetime, year, ..., nanosecond[, description[, era, eraYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainDateTime is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of datetime.calendarId.) + */ + assertPlainDateTime(datetime, year, month, monthCode, day, hour, minute, second, millisecond, microsecond, nanosecond, description = "", era = undefined, eraYear = undefined) { + const prefix = description ? `${description}: ` : ""; + assert(datetime instanceof Temporal.PlainDateTime, `${prefix}instanceof`); + assert.sameValue(datetime.era, era, `${prefix}era result:`); + assert.sameValue(datetime.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(datetime.year, year, `${prefix}year result:`); + assert.sameValue(datetime.month, month, `${prefix}month result:`); + assert.sameValue(datetime.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(datetime.day, day, `${prefix}day result:`); + assert.sameValue(datetime.hour, hour, `${prefix}hour result:`); + assert.sameValue(datetime.minute, minute, `${prefix}minute result:`); + assert.sameValue(datetime.second, second, `${prefix}second result:`); + assert.sameValue(datetime.millisecond, millisecond, `${prefix}millisecond result:`); + assert.sameValue(datetime.microsecond, microsecond, `${prefix}microsecond result:`); + assert.sameValue(datetime.nanosecond, nanosecond, `${prefix}nanosecond result:`); + }, + + /* + * assertPlainDateTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.PlainDateTimes are of the correct + * type, equal according to their equals() methods, and additionally that + * their calendar internal slots are the same value. + */ + assertPlainDateTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.PlainDateTime, `${prefix}expected value should be a Temporal.PlainDateTime`); + assert(actual instanceof Temporal.PlainDateTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + assert.sameValue( + actual.getISOFields().calendar, + expected.getISOFields().calendar, + `${prefix}calendar same value:` + ); + }, + + /* + * assertPlainMonthDay(monthDay, monthCode, day[, description [, referenceISOYear]]): + * + * Shorthand for asserting that each field of a Temporal.PlainMonthDay is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of monthDay.calendarId().) + */ + assertPlainMonthDay(monthDay, monthCode, day, description = "", referenceISOYear = 1972) { + const prefix = description ? `${description}: ` : ""; + assert(monthDay instanceof Temporal.PlainMonthDay, `${prefix}instanceof`); + assert.sameValue(monthDay.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(monthDay.day, day, `${prefix}day result:`); + assert.sameValue(monthDay.getISOFields().isoYear, referenceISOYear, `${prefix}referenceISOYear result:`); + }, + + /* + * assertPlainTime(time, hour, ..., nanosecond[, description]): + * + * Shorthand for asserting that each field of a Temporal.PlainTime is equal to + * an expected value. + */ + assertPlainTime(time, hour, minute, second, millisecond, microsecond, nanosecond, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(time instanceof Temporal.PlainTime, `${prefix}instanceof`); + assert.sameValue(time.hour, hour, `${prefix}hour result:`); + assert.sameValue(time.minute, minute, `${prefix}minute result:`); + assert.sameValue(time.second, second, `${prefix}second result:`); + assert.sameValue(time.millisecond, millisecond, `${prefix}millisecond result:`); + assert.sameValue(time.microsecond, microsecond, `${prefix}microsecond result:`); + assert.sameValue(time.nanosecond, nanosecond, `${prefix}nanosecond result:`); + }, + + /* + * assertPlainTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.PlainTimes are of the correct + * type and equal according to their equals() methods. + */ + assertPlainTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.PlainTime, `${prefix}expected value should be a Temporal.PlainTime`); + assert(actual instanceof Temporal.PlainTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + }, + + /* + * assertPlainYearMonth(yearMonth, year, month, monthCode[, description[, era, eraYear, referenceISODay]]): + * + * Shorthand for asserting that each field of a Temporal.PlainYearMonth is + * equal to an expected value. (Except the `calendar` property, since callers + * may want to assert either object equality with an object they put in there, + * or the value of yearMonth.calendarId.) + */ + assertPlainYearMonth(yearMonth, year, month, monthCode, description = "", era = undefined, eraYear = undefined, referenceISODay = 1) { + const prefix = description ? `${description}: ` : ""; + assert(yearMonth instanceof Temporal.PlainYearMonth, `${prefix}instanceof`); + assert.sameValue(yearMonth.era, era, `${prefix}era result:`); + assert.sameValue(yearMonth.eraYear, eraYear, `${prefix}eraYear result:`); + assert.sameValue(yearMonth.year, year, `${prefix}year result:`); + assert.sameValue(yearMonth.month, month, `${prefix}month result:`); + assert.sameValue(yearMonth.monthCode, monthCode, `${prefix}monthCode result:`); + assert.sameValue(yearMonth.getISOFields().isoDay, referenceISODay, `${prefix}referenceISODay result:`); + }, + + /* + * assertZonedDateTimesEqual(actual, expected[, description]): + * + * Shorthand for asserting that two Temporal.ZonedDateTimes are of the correct + * type, equal according to their equals() methods, and additionally that + * their time zones and calendar internal slots are the same value. + */ + assertZonedDateTimesEqual(actual, expected, description = "") { + const prefix = description ? `${description}: ` : ""; + assert(expected instanceof Temporal.ZonedDateTime, `${prefix}expected value should be a Temporal.ZonedDateTime`); + assert(actual instanceof Temporal.ZonedDateTime, `${prefix}instanceof`); + assert(actual.equals(expected), `${prefix}equals method`); + assert.sameValue(actual.timeZone, expected.timeZone, `${prefix}time zone same value:`); + assert.sameValue( + actual.getISOFields().calendar, + expected.getISOFields().calendar, + `${prefix}calendar same value:` + ); + }, + + /* + * assertUnreachable(description): + * + * Helper for asserting that code is not executed. This is useful for + * assertions that methods of user calendars and time zones are not called. + */ + assertUnreachable(description) { + let message = "This code should not be executed"; + if (description) { + message = `${message}: ${description}`; + } + throw new Test262Error(message); + }, + + /* + * checkCalendarDateUntilLargestUnitSingular(func, expectedLargestUnitCalls): + * + * When an options object with a largestUnit property is synthesized inside + * Temporal and passed to user code such as calendar.dateUntil(), the value of + * the largestUnit property should be in the singular form, even if the input + * was given in the plural form. + * (This doesn't apply when the options object is passed through verbatim.) + * + * func(calendar, largestUnit, index) is the operation under test. It's called + * with an instance of a calendar that keeps track of which largestUnit is + * passed to dateUntil(), each key of expectedLargestUnitCalls in turn, and + * the key's numerical index in case the function needs to generate test data + * based on the index. At the end, the actual values passed to dateUntil() are + * compared with the array values of expectedLargestUnitCalls. + */ + checkCalendarDateUntilLargestUnitSingular(func, expectedLargestUnitCalls) { + const actual = []; + + class DateUntilOptionsCalendar extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateUntil(earlier, later, options) { + actual.push(options.largestUnit); + return super.dateUntil(earlier, later, options); + } + + toString() { + return "date-until-options"; + } + } + + const calendar = new DateUntilOptionsCalendar(); + Object.entries(expectedLargestUnitCalls).forEach(([largestUnit, expected], index) => { + func(calendar, largestUnit, index); + assert.compareArray(actual, expected, `largestUnit passed to calendar.dateUntil() for largestUnit ${largestUnit}`); + actual.splice(0); // empty it for the next check + }); + }, + + /* + * checkPlainDateTimeConversionFastPath(func): + * + * ToTemporalDate and ToTemporalTime should both, if given a + * Temporal.PlainDateTime instance, convert to the desired type by reading the + * PlainDateTime's internal slots, rather than calling any getters. + * + * func(datetime, calendar) is the actual operation to test, that must + * internally call the abstract operation ToTemporalDate or ToTemporalTime. + * It is passed a Temporal.PlainDateTime instance, as well as the instance's + * calendar object (so that it doesn't have to call the calendar getter itself + * if it wants to make any assertions about the calendar.) + */ + checkPlainDateTimeConversionFastPath(func, message = "checkPlainDateTimeConversionFastPath") { + const actual = []; + const expected = []; + + const calendar = new Temporal.Calendar("iso8601"); + const datetime = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321, calendar); + const prototypeDescrs = Object.getOwnPropertyDescriptors(Temporal.PlainDateTime.prototype); + ["year", "month", "monthCode", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"].forEach((property) => { + Object.defineProperty(datetime, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + const value = prototypeDescrs[property].get.call(this); + return { + toString() { + actual.push(`toString ${formatPropertyName(property)}`); + return value.toString(); + }, + valueOf() { + actual.push(`valueOf ${formatPropertyName(property)}`); + return value; + }, + }; + }, + }); + }); + Object.defineProperty(datetime, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(datetime, calendar); + assert.compareArray(actual, expected, `${message}: property getters not called`); + }, + + /* + * Check that an options bag that accepts units written in the singular form, + * also accepts the same units written in the plural form. + * func(unit) should call the method with the appropriate options bag + * containing unit as a value. This will be called twice for each element of + * validSingularUnits, once with singular and once with plural, and the + * results of each pair should be the same (whether a Temporal object or a + * primitive value.) + */ + checkPluralUnitsAccepted(func, validSingularUnits) { + const plurals = { + year: 'years', + month: 'months', + week: 'weeks', + day: 'days', + hour: 'hours', + minute: 'minutes', + second: 'seconds', + millisecond: 'milliseconds', + microsecond: 'microseconds', + nanosecond: 'nanoseconds', + }; + + validSingularUnits.forEach((unit) => { + const singularValue = func(unit); + const pluralValue = func(plurals[unit]); + const desc = `Plural ${plurals[unit]} produces the same result as singular ${unit}`; + if (singularValue instanceof Temporal.Duration) { + TemporalHelpers.assertDurationsEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.Instant) { + TemporalHelpers.assertInstantsEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.PlainDateTime) { + TemporalHelpers.assertPlainDateTimesEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.PlainTime) { + TemporalHelpers.assertPlainTimesEqual(pluralValue, singularValue, desc); + } else if (singularValue instanceof Temporal.ZonedDateTime) { + TemporalHelpers.assertZonedDateTimesEqual(pluralValue, singularValue, desc); + } else { + assert.sameValue(pluralValue, singularValue); + } + }); + }, + + /* + * checkRoundingIncrementOptionWrongType(checkFunc, assertTrueResultFunc, assertObjectResultFunc): + * + * Checks the type handling of the roundingIncrement option. + * checkFunc(roundingIncrement) is a function which takes the value of + * roundingIncrement to test, and calls the method under test with it, + * returning the result. assertTrueResultFunc(result, description) should + * assert that result is the expected result with roundingIncrement: true, and + * assertObjectResultFunc(result, description) should assert that result is + * the expected result with roundingIncrement being an object with a valueOf() + * method. + */ + checkRoundingIncrementOptionWrongType(checkFunc, assertTrueResultFunc, assertObjectResultFunc) { + // null converts to 0, which is out of range + assert.throws(RangeError, () => checkFunc(null), "null"); + // Booleans convert to either 0 or 1, and 1 is allowed + const trueResult = checkFunc(true); + assertTrueResultFunc(trueResult, "true"); + assert.throws(RangeError, () => checkFunc(false), "false"); + // Symbols and BigInts cannot convert to numbers + assert.throws(TypeError, () => checkFunc(Symbol()), "symbol"); + assert.throws(TypeError, () => checkFunc(2n), "bigint"); + + // Objects prefer their valueOf() methods when converting to a number + assert.throws(RangeError, () => checkFunc({}), "plain object"); + + const expected = [ + "get roundingIncrement.valueOf", + "call roundingIncrement.valueOf", + ]; + const actual = []; + const observer = TemporalHelpers.toPrimitiveObserver(actual, 2, "roundingIncrement"); + const objectResult = checkFunc(observer); + assertObjectResultFunc(objectResult, "object with valueOf"); + assert.compareArray(actual, expected, "order of operations"); + }, + + /* + * checkStringOptionWrongType(propertyName, value, checkFunc, assertFunc): + * + * Checks the type handling of a string option, of which there are several in + * Temporal. + * propertyName is the name of the option, and value is the value that + * assertFunc should expect it to have. + * checkFunc(value) is a function which takes the value of the option to test, + * and calls the method under test with it, returning the result. + * assertFunc(result, description) should assert that result is the expected + * result with the option value being an object with a toString() method + * which returns the given value. + */ + checkStringOptionWrongType(propertyName, value, checkFunc, assertFunc) { + // null converts to the string "null", which is an invalid string value + assert.throws(RangeError, () => checkFunc(null), "null"); + // Booleans convert to the strings "true" or "false", which are invalid + assert.throws(RangeError, () => checkFunc(true), "true"); + assert.throws(RangeError, () => checkFunc(false), "false"); + // Symbols cannot convert to strings + assert.throws(TypeError, () => checkFunc(Symbol()), "symbol"); + // Numbers convert to strings which are invalid + assert.throws(RangeError, () => checkFunc(2), "number"); + // BigInts convert to strings which are invalid + assert.throws(RangeError, () => checkFunc(2n), "bigint"); + + // Objects prefer their toString() methods when converting to a string + assert.throws(RangeError, () => checkFunc({}), "plain object"); + + const expected = [ + `get ${propertyName}.toString`, + `call ${propertyName}.toString`, + ]; + const actual = []; + const observer = TemporalHelpers.toPrimitiveObserver(actual, value, propertyName); + const result = checkFunc(observer); + assertFunc(result, "object with toString"); + assert.compareArray(actual, expected, "order of operations"); + }, + + /* + * checkSubclassingIgnored(construct, constructArgs, method, methodArgs, + * resultAssertions): + * + * Methods of Temporal classes that return a new instance of the same class, + * must not take the constructor of a subclass into account, nor the @@species + * property. This helper runs tests to ensure this. + * + * construct(...constructArgs) must yield a valid instance of the Temporal + * class. instance[method](...methodArgs) is the method call under test, which + * must also yield a valid instance of the same Temporal class, not a + * subclass. See below for the individual tests that this runs. + * resultAssertions() is a function that performs additional assertions on the + * instance returned by the method under test. + */ + checkSubclassingIgnored(...args) { + this.checkSubclassConstructorNotObject(...args); + this.checkSubclassConstructorUndefined(...args); + this.checkSubclassConstructorThrows(...args); + this.checkSubclassConstructorNotCalled(...args); + this.checkSubclassSpeciesInvalidResult(...args); + this.checkSubclassSpeciesNotAConstructor(...args); + this.checkSubclassSpeciesNull(...args); + this.checkSubclassSpeciesUndefined(...args); + this.checkSubclassSpeciesThrows(...args); + }, + + /* + * Checks that replacing the 'constructor' property of the instance with + * various primitive values does not affect the returned new instance. + */ + checkSubclassConstructorNotObject(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = value; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + }, + + /* + * Checks that replacing the 'constructor' property of the subclass with + * undefined does not affect the returned new instance. + */ + checkSubclassConstructorUndefined(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = undefined; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Checks that making the 'constructor' property of the instance throw when + * called does not affect the returned new instance. + */ + checkSubclassConstructorThrows(construct, constructArgs, method, methodArgs, resultAssertions) { + function CustomError() {} + const instance = new construct(...constructArgs); + Object.defineProperty(instance, "constructor", { + get() { + throw new CustomError(); + } + }); + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Checks that when subclassing, the subclass constructor is not called by + * the method under test. + */ + checkSubclassConstructorNotCalled(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it's a + * constructor that returns a non-object value. + */ + checkSubclassSpeciesInvalidResult(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = { + [Symbol.species]: function() { + return value; + }, + }; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "plain object"); + }, + + /* + * Check that the constructor's @@species property is ignored when it's not a + * constructor. + */ + checkSubclassSpeciesNotAConstructor(construct, constructArgs, method, methodArgs, resultAssertions) { + function check(value, description) { + const instance = new construct(...constructArgs); + instance.constructor = { + [Symbol.species]: value, + }; + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype, description); + resultAssertions(result); + } + + check(true, "true"); + check("test", "string"); + check(Symbol(), "Symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "plain object"); + }, + + /* + * Check that the constructor's @@species property is ignored when it's null. + */ + checkSubclassSpeciesNull(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = { + [Symbol.species]: null, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it's + * undefined. + */ + checkSubclassSpeciesUndefined(construct, constructArgs, method, methodArgs, resultAssertions) { + let called = 0; + + class MySubclass extends construct { + constructor() { + ++called; + super(...constructArgs); + } + } + + const instance = new MySubclass(); + assert.sameValue(called, 1); + + MySubclass.prototype.constructor = { + [Symbol.species]: undefined, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(called, 1); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that the constructor's @@species property is ignored when it throws, + * i.e. it is not called at all. + */ + checkSubclassSpeciesThrows(construct, constructArgs, method, methodArgs, resultAssertions) { + function CustomError() {} + + const instance = new construct(...constructArgs); + instance.constructor = { + get [Symbol.species]() { + throw new CustomError(); + }, + }; + + const result = instance[method](...methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + }, + + /* + * checkSubclassingIgnoredStatic(construct, method, methodArgs, resultAssertions): + * + * Static methods of Temporal classes that return a new instance of the class, + * must not use the this-value as a constructor. This helper runs tests to + * ensure this. + * + * construct[method](...methodArgs) is the static method call under test, and + * must yield a valid instance of the Temporal class, not a subclass. See + * below for the individual tests that this runs. + * resultAssertions() is a function that performs additional assertions on the + * instance returned by the method under test. + */ + checkSubclassingIgnoredStatic(...args) { + this.checkStaticInvalidReceiver(...args); + this.checkStaticReceiverNotCalled(...args); + this.checkThisValueNotCalled(...args); + }, + + /* + * Check that calling the static method with a receiver that's not callable, + * still calls the intrinsic constructor. + */ + checkStaticInvalidReceiver(construct, method, methodArgs, resultAssertions) { + function check(value, description) { + const result = construct[method].apply(value, methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "Non-callable object"); + }, + + /* + * Check that calling the static method with a receiver that returns a value + * that's not callable, still calls the intrinsic constructor. + */ + checkStaticReceiverNotCalled(construct, method, methodArgs, resultAssertions) { + function check(value, description) { + const receiver = function () { + return value; + }; + const result = construct[method].apply(receiver, methodArgs); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + } + + check(undefined, "undefined"); + check(null, "null"); + check(true, "true"); + check("test", "string"); + check(Symbol(), "symbol"); + check(7, "number"); + check(7n, "bigint"); + check({}, "Non-callable object"); + }, + + /* + * Check that the receiver isn't called. + */ + checkThisValueNotCalled(construct, method, methodArgs, resultAssertions) { + let called = false; + + class MySubclass extends construct { + constructor(...args) { + called = true; + super(...args); + } + } + + const result = MySubclass[method](...methodArgs); + assert.sameValue(called, false); + assert.sameValue(Object.getPrototypeOf(result), construct.prototype); + resultAssertions(result); + }, + + /* + * Check that any iterable returned from a custom time zone's + * getPossibleInstantsFor() method is exhausted. + * The custom time zone object is passed in to func(). + * expected is an array of strings representing the expected calls to the + * getPossibleInstantsFor() method. The PlainDateTimes that it is called with, + * are compared (using their toString() results) with the array. + */ + checkTimeZonePossibleInstantsIterable(func, expected) { + // A custom time zone that returns an iterable instead of an array from its + // getPossibleInstantsFor() method, and for testing purposes skips + // 00:00-01:00 UTC on January 1, 2030, and repeats 00:00-01:00 UTC+1 on + // January 3, 2030. Otherwise identical to the UTC time zone. + class TimeZonePossibleInstantsIterable extends Temporal.TimeZone { + constructor() { + super("UTC"); + this.getPossibleInstantsForCallCount = 0; + this.getPossibleInstantsForCalledWith = []; + this.getPossibleInstantsForReturns = []; + this.iteratorExhausted = []; + } + + toString() { + return "Custom/Iterable"; + } + + getOffsetNanosecondsFor(instant) { + if (Temporal.Instant.compare(instant, "2030-01-01T00:00Z") >= 0 && + Temporal.Instant.compare(instant, "2030-01-03T01:00Z") < 0) { + return 3600_000_000_000; + } else { + return 0; + } + } + + getPossibleInstantsFor(dateTime) { + this.getPossibleInstantsForCallCount++; + this.getPossibleInstantsForCalledWith.push(dateTime); + + // Fake DST transition + let retval = super.getPossibleInstantsFor(dateTime); + if (dateTime.toPlainDate().equals("2030-01-01") && dateTime.hour === 0) { + retval = []; + } else if (dateTime.toPlainDate().equals("2030-01-03") && dateTime.hour === 0) { + retval.push(retval[0].subtract({ hours: 1 })); + } else if (dateTime.year === 2030 && dateTime.month === 1 && dateTime.day >= 1 && dateTime.day <= 2) { + retval[0] = retval[0].subtract({ hours: 1 }); + } + + this.getPossibleInstantsForReturns.push(retval); + this.iteratorExhausted.push(false); + return { + callIndex: this.getPossibleInstantsForCallCount - 1, + timeZone: this, + *[Symbol.iterator]() { + yield* this.timeZone.getPossibleInstantsForReturns[this.callIndex]; + this.timeZone.iteratorExhausted[this.callIndex] = true; + }, + }; + } + } + + const timeZone = new TimeZonePossibleInstantsIterable(); + func(timeZone); + + assert.sameValue(timeZone.getPossibleInstantsForCallCount, expected.length, "getPossibleInstantsFor() method called correct number of times"); + + for (let index = 0; index < expected.length; index++) { + assert.sameValue(timeZone.getPossibleInstantsForCalledWith[index].toString(), expected[index], "getPossibleInstantsFor() called with expected PlainDateTime"); + assert(timeZone.iteratorExhausted[index], "iterated through the whole iterable"); + } + }, + + /* + * Check that any calendar-carrying Temporal object has its [[Calendar]] + * internal slot read by ToTemporalCalendar, and does not fetch the calendar + * by calling getters. + * The custom calendar object is passed in to func() so that it can do its + * own additional assertions involving the calendar if necessary. (Sometimes + * there is nothing to assert as the calendar isn't stored anywhere that can + * be asserted about.) + */ + checkToTemporalCalendarFastPath(func) { + class CalendarFastPathCheck extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(...args) { + return super.dateFromFields(...args).withCalendar(this); + } + + monthDayFromFields(...args) { + const { isoYear, isoMonth, isoDay } = super.monthDayFromFields(...args).getISOFields(); + return new Temporal.PlainMonthDay(isoMonth, isoDay, this, isoYear); + } + + yearMonthFromFields(...args) { + const { isoYear, isoMonth, isoDay } = super.yearMonthFromFields(...args).getISOFields(); + return new Temporal.PlainYearMonth(isoYear, isoMonth, this, isoDay); + } + + toString() { + return "fast-path-check"; + } + } + const calendar = new CalendarFastPathCheck(); + + const plainDate = new Temporal.PlainDate(2000, 5, 2, calendar); + const plainDateTime = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321, calendar); + const plainMonthDay = new Temporal.PlainMonthDay(5, 2, calendar); + const plainYearMonth = new Temporal.PlainYearMonth(2000, 5, calendar); + const zonedDateTime = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, "UTC", calendar); + + [plainDate, plainDateTime, plainMonthDay, plainYearMonth, zonedDateTime].forEach((temporalObject) => { + const actual = []; + const expected = []; + + Object.defineProperty(temporalObject, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(temporalObject, calendar); + assert.compareArray(actual, expected, "calendar getter not called"); + }); + }, + + checkToTemporalInstantFastPath(func) { + const actual = []; + const expected = []; + + const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, "UTC"); + Object.defineProperty(datetime, 'toString', { + get() { + actual.push("get toString"); + return function (options) { + actual.push("call toString"); + return Temporal.ZonedDateTime.prototype.toString.call(this, options); + }; + }, + }); + + func(datetime); + assert.compareArray(actual, expected, "toString not called"); + }, + + checkToTemporalPlainDateTimeFastPath(func) { + const actual = []; + const expected = []; + + const calendar = new Temporal.Calendar("iso8601"); + const date = new Temporal.PlainDate(2000, 5, 2, calendar); + const prototypeDescrs = Object.getOwnPropertyDescriptors(Temporal.PlainDate.prototype); + ["year", "month", "monthCode", "day"].forEach((property) => { + Object.defineProperty(date, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + const value = prototypeDescrs[property].get.call(this); + return TemporalHelpers.toPrimitiveObserver(actual, value, property); + }, + }); + }); + ["hour", "minute", "second", "millisecond", "microsecond", "nanosecond"].forEach((property) => { + Object.defineProperty(date, property, { + get() { + actual.push(`get ${formatPropertyName(property)}`); + return undefined; + }, + }); + }); + Object.defineProperty(date, "calendar", { + get() { + actual.push("get calendar"); + return calendar; + }, + }); + + func(date, calendar); + assert.compareArray(actual, expected, "property getters not called"); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that the + * fromFields methods are always called with a null-prototype fields object. + */ + calendarCheckFieldsPrototypePollution() { + class CalendarCheckFieldsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + } + + // toString must remain "iso8601", so that some methods don't throw due to + // incompatible calendars + + dateFromFields(fields, options = {}) { + this.dateFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "dateFromFields should be called with null-prototype fields object"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options = {}) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "yearMonthFromFields should be called with null-prototype fields object"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options = {}) { + this.monthDayFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "monthDayFromFields should be called with null-prototype fields object"); + return super.monthDayFromFields(fields, options); + } + } + + return new CalendarCheckFieldsPrototypePollution(); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that the + * mergeFields() method is always called with null-prototype fields objects. + */ + calendarCheckMergeFieldsPrototypePollution() { + class CalendarCheckMergeFieldsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.mergeFieldsCallCount = 0; + } + + toString() { + return "merge-fields-null-proto"; + } + + mergeFields(fields, additionalFields) { + this.mergeFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(fields), null, "mergeFields should be called with null-prototype fields object (first argument)"); + assert.sameValue(Object.getPrototypeOf(additionalFields), null, "mergeFields should be called with null-prototype fields object (second argument)"); + return super.mergeFields(fields, additionalFields); + } + } + + return new CalendarCheckMergeFieldsPrototypePollution(); + }, + + /* + * A custom calendar used in prototype pollution checks. Verifies that methods + * are always called with a null-prototype options object. + */ + calendarCheckOptionsPrototypePollution() { + class CalendarCheckOptionsPrototypePollution extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.yearMonthFromFieldsCallCount = 0; + this.dateUntilCallCount = 0; + } + + toString() { + return "options-null-proto"; + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(Object.getPrototypeOf(options), null, "yearMonthFromFields should be called with null-prototype options"); + return super.yearMonthFromFields(fields, options); + } + + dateUntil(one, two, options) { + this.dateUntilCallCount++; + assert.sameValue(Object.getPrototypeOf(options), null, "dateUntil should be called with null-prototype options"); + return super.dateUntil(one, two, options); + } + } + + return new CalendarCheckOptionsPrototypePollution(); + }, + + /* + * A custom calendar that asserts its dateAdd() method is called with the + * options parameter having the value undefined. + */ + calendarDateAddUndefinedOptions() { + class CalendarDateAddUndefinedOptions extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateAddCallCount = 0; + } + + toString() { + return "dateadd-undef-options"; + } + + dateAdd(date, duration, options) { + this.dateAddCallCount++; + assert.sameValue(options, undefined, "dateAdd shouldn't be called with options"); + return super.dateAdd(date, duration, options); + } + } + return new CalendarDateAddUndefinedOptions(); + }, + + /* + * A custom calendar that asserts its dateAdd() method is called with a + * PlainDate instance. Optionally, it also asserts that the PlainDate instance + * is the specific object `this.specificPlainDate`, if it is set by the + * calling code. + */ + calendarDateAddPlainDateInstance() { + class CalendarDateAddPlainDateInstance extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateAddCallCount = 0; + this.specificPlainDate = undefined; + } + + toString() { + return "dateadd-plain-date-instance"; + } + + dateFromFields(...args) { + return super.dateFromFields(...args).withCalendar(this); + } + + dateAdd(date, duration, options) { + this.dateAddCallCount++; + assert(date instanceof Temporal.PlainDate, "dateAdd() should be called with a PlainDate instance"); + if (this.dateAddCallCount === 1 && this.specificPlainDate) { + assert.sameValue(date, this.specificPlainDate, `dateAdd() should be called first with the specific PlainDate instance ${this.specificPlainDate}`); + } + return super.dateAdd(date, duration, options).withCalendar(this); + } + } + return new CalendarDateAddPlainDateInstance(); + }, + + /* + * A custom calendar that returns an iterable instead of an array from its + * fields() method, otherwise identical to the ISO calendar. + */ + calendarFieldsIterable() { + class CalendarFieldsIterable extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.fieldsCallCount = 0; + this.fieldsCalledWith = []; + this.iteratorExhausted = []; + } + + toString() { + return "fields-iterable"; + } + + fields(fieldNames) { + this.fieldsCallCount++; + this.fieldsCalledWith.push(fieldNames.slice()); + this.iteratorExhausted.push(false); + return { + callIndex: this.fieldsCallCount - 1, + calendar: this, + *[Symbol.iterator]() { + yield* this.calendar.fieldsCalledWith[this.callIndex]; + this.calendar.iteratorExhausted[this.callIndex] = true; + }, + }; + } + } + return new CalendarFieldsIterable(); + }, + + /* + * A custom calendar that asserts its ...FromFields() methods are called with + * the options parameter having the value undefined. + */ + calendarFromFieldsUndefinedOptions() { + class CalendarFromFieldsUndefinedOptions extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.dateFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + } + + toString() { + return "from-fields-undef-options"; + } + + dateFromFields(fields, options) { + this.dateFromFieldsCallCount++; + assert.sameValue(options, undefined, "dateFromFields shouldn't be called with options"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + assert.sameValue(options, undefined, "yearMonthFromFields shouldn't be called with options"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + this.monthDayFromFieldsCallCount++; + assert.sameValue(options, undefined, "monthDayFromFields shouldn't be called with options"); + return super.monthDayFromFields(fields, options); + } + } + return new CalendarFromFieldsUndefinedOptions(); + }, + + /* + * A custom calendar that modifies the fields object passed in to + * dateFromFields, sabotaging its time properties. + */ + calendarMakeInfinityTime() { + class CalendarMakeInfinityTime extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(fields, options) { + const retval = super.dateFromFields(fields, options); + fields.hour = Infinity; + fields.minute = Infinity; + fields.second = Infinity; + fields.millisecond = Infinity; + fields.microsecond = Infinity; + fields.nanosecond = Infinity; + return retval; + } + } + return new CalendarMakeInfinityTime(); + }, + + /* + * A custom calendar that defines getters on the fields object passed into + * dateFromFields that throw, sabotaging its time properties. + */ + calendarMakeInvalidGettersTime() { + class CalendarMakeInvalidGettersTime extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + + dateFromFields(fields, options) { + const retval = super.dateFromFields(fields, options); + const throwingDescriptor = { + get() { + throw new Test262Error("reading a sabotaged time field"); + }, + }; + Object.defineProperties(fields, { + hour: throwingDescriptor, + minute: throwingDescriptor, + second: throwingDescriptor, + millisecond: throwingDescriptor, + microsecond: throwingDescriptor, + nanosecond: throwingDescriptor, + }); + return retval; + } + } + return new CalendarMakeInvalidGettersTime(); + }, + + /* + * A custom calendar whose mergeFields() method returns a proxy object with + * all of its Get and HasProperty operations observable, as well as adding a + * "shouldNotBeCopied": true property. + */ + calendarMergeFieldsGetters() { + class CalendarMergeFieldsGetters extends Temporal.Calendar { + constructor() { + super("iso8601"); + this.mergeFieldsReturnOperations = []; + } + + toString() { + return "merge-fields-getters"; + } + + dateFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + assert.sameValue(fields.shouldNotBeCopied, undefined, "extra fields should not be copied"); + return super.monthDayFromFields(fields, options); + } + + mergeFields(fields, additionalFields) { + const retval = super.mergeFields(fields, additionalFields); + retval._calendar = this; + retval.shouldNotBeCopied = true; + return new Proxy(retval, { + get(target, key) { + target._calendar.mergeFieldsReturnOperations.push(`get ${key}`); + const result = target[key]; + if (result === undefined) { + return undefined; + } + return TemporalHelpers.toPrimitiveObserver(target._calendar.mergeFieldsReturnOperations, result, key); + }, + has(target, key) { + target._calendar.mergeFieldsReturnOperations.push(`has ${key}`); + return key in target; + }, + }); + } + } + return new CalendarMergeFieldsGetters(); + }, + + /* + * A custom calendar whose mergeFields() method returns a primitive value, + * given by @primitive, and which records the number of calls made to its + * dateFromFields(), yearMonthFromFields(), and monthDayFromFields() methods. + */ + calendarMergeFieldsReturnsPrimitive(primitive) { + class CalendarMergeFieldsPrimitive extends Temporal.Calendar { + constructor(mergeFieldsReturnValue) { + super("iso8601"); + this._mergeFieldsReturnValue = mergeFieldsReturnValue; + this.dateFromFieldsCallCount = 0; + this.monthDayFromFieldsCallCount = 0; + this.yearMonthFromFieldsCallCount = 0; + } + + toString() { + return "merge-fields-primitive"; + } + + dateFromFields(fields, options) { + this.dateFromFieldsCallCount++; + return super.dateFromFields(fields, options); + } + + yearMonthFromFields(fields, options) { + this.yearMonthFromFieldsCallCount++; + return super.yearMonthFromFields(fields, options); + } + + monthDayFromFields(fields, options) { + this.monthDayFromFieldsCallCount++; + return super.monthDayFromFields(fields, options); + } + + mergeFields() { + return this._mergeFieldsReturnValue; + } + } + return new CalendarMergeFieldsPrimitive(primitive); + }, + + /* + * A custom calendar whose fields() method returns the same value as the + * iso8601 calendar, with the addition of extraFields provided as parameter. + */ + calendarWithExtraFields(fields) { + class CalendarWithExtraFields extends Temporal.Calendar { + constructor(extraFields) { + super("iso8601"); + this._extraFields = extraFields; + } + + fields(fieldNames) { + return super.fields(fieldNames).concat(this._extraFields); + } + } + + return new CalendarWithExtraFields(fields); + }, + + /* + * crossDateLineTimeZone(): + * + * This returns an instance of a custom time zone class that implements one + * single transition where the time zone moves from one side of the + * International Date Line to the other, for the purpose of testing time zone + * calculations without depending on system time zone data. + * + * The transition occurs at epoch second 1325239200 and goes from offset + * -10:00 to +14:00. In other words, the time zone skips the whole calendar + * day of 2011-12-30. This is the same as the real-life transition in the + * Pacific/Apia time zone. + */ + crossDateLineTimeZone() { + const { compare } = Temporal.PlainDate; + const skippedDay = new Temporal.PlainDate(2011, 12, 30); + const transitionEpoch = 1325239200_000_000_000n; + const beforeOffset = new Temporal.TimeZone("-10:00"); + const afterOffset = new Temporal.TimeZone("+14:00"); + + class CrossDateLineTimeZone extends Temporal.TimeZone { + constructor() { + super("+14:00"); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < transitionEpoch) { + return beforeOffset.getOffsetNanosecondsFor(instant); + } + return afterOffset.getOffsetNanosecondsFor(instant); + } + + getPossibleInstantsFor(datetime) { + const comparison = compare(datetime.toPlainDate(), skippedDay); + if (comparison === 0) { + return []; + } + if (comparison < 0) { + return [beforeOffset.getInstantFor(datetime)]; + } + return [afterOffset.getInstantFor(datetime)]; + } + + getPreviousTransition(instant) { + if (instant.epochNanoseconds > transitionEpoch) return new Temporal.Instant(transitionEpoch); + return null; + } + + getNextTransition(instant) { + if (instant.epochNanoseconds < transitionEpoch) return new Temporal.Instant(transitionEpoch); + return null; + } + + toString() { + return "Custom/Date_Line"; + } + } + return new CrossDateLineTimeZone(); + }, + + /* + * observeProperty(calls, object, propertyName, value): + * + * Defines an own property @object.@propertyName with value @value, that + * will log any calls to its accessors to the array @calls. + */ + observeProperty(calls, object, propertyName, value, objectName = "") { + Object.defineProperty(object, propertyName, { + get() { + calls.push(`get ${formatPropertyName(propertyName, objectName)}`); + return value; + }, + set(v) { + calls.push(`set ${formatPropertyName(propertyName, objectName)}`); + } + }); + }, + + /* + * observeMethod(calls, object, propertyName, value): + * + * Defines an own property @object.@propertyName with value @value, that + * will log any calls of @value to the array @calls. + */ + observeMethod(calls, object, propertyName, objectName = "") { + const method = object[propertyName]; + object[propertyName] = function () { + calls.push(`call ${formatPropertyName(propertyName, objectName)}`); + return method.apply(object, arguments); + }; + }, + + /* + * Used for substituteMethod to indicate default behavior instead of a + * substituted value + */ + SUBSTITUTE_SKIP: SKIP_SYMBOL, + + /* + * substituteMethod(object, propertyName, values): + * + * Defines an own property @object.@propertyName that will, for each + * subsequent call to the method previously defined as + * @object.@propertyName: + * - Call the method, if no more values remain + * - Call the method, if the value in @values for the corresponding call + * is SUBSTITUTE_SKIP + * - Otherwise, return the corresponding value in @value + */ + substituteMethod(object, propertyName, values) { + let calls = 0; + const method = object[propertyName]; + object[propertyName] = function () { + if (calls >= values.length) { + return method.apply(object, arguments); + } else if (values[calls] === SKIP_SYMBOL) { + calls++; + return method.apply(object, arguments); + } else { + return values[calls++]; + } + }; + }, + + /* + * calendarObserver: + * A custom calendar that behaves exactly like the ISO 8601 calendar but + * tracks calls to any of its methods, and Get/Has operations on its + * properties, by appending messages to an array. This is for the purpose of + * testing order of operations that are observable from user code. + * objectName is used in the log. + */ + calendarObserver(calls, objectName, methodOverrides = {}) { + function removeExtraHasPropertyChecks(objectName, calls) { + // Inserting the tracking calendar into the return values of methods + // that we chain up into the ISO calendar for, causes extra HasProperty + // checks, which we observe. This removes them so that we don't leak + // implementation details of the helper into the test code. + assert.sameValue(calls.pop(), `has ${objectName}.yearOfWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.yearMonthFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.year`); + assert.sameValue(calls.pop(), `has ${objectName}.weekOfYear`); + assert.sameValue(calls.pop(), `has ${objectName}.monthsInYear`); + assert.sameValue(calls.pop(), `has ${objectName}.monthDayFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.monthCode`); + assert.sameValue(calls.pop(), `has ${objectName}.month`); + assert.sameValue(calls.pop(), `has ${objectName}.mergeFields`); + assert.sameValue(calls.pop(), `has ${objectName}.inLeapYear`); + assert.sameValue(calls.pop(), `has ${objectName}.id`); + assert.sameValue(calls.pop(), `has ${objectName}.fields`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInYear`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.daysInMonth`); + assert.sameValue(calls.pop(), `has ${objectName}.dayOfYear`); + assert.sameValue(calls.pop(), `has ${objectName}.dayOfWeek`); + assert.sameValue(calls.pop(), `has ${objectName}.day`); + assert.sameValue(calls.pop(), `has ${objectName}.dateUntil`); + assert.sameValue(calls.pop(), `has ${objectName}.dateFromFields`); + assert.sameValue(calls.pop(), `has ${objectName}.dateAdd`); + } + + const iso8601 = new Temporal.Calendar("iso8601"); + const trackingMethods = { + dateFromFields(...args) { + calls.push(`call ${objectName}.dateFromFields`); + if ('dateFromFields' in methodOverrides) { + const value = methodOverrides.dateFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.dateFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainDate(isoYear, isoMonth, isoDay, this); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + yearMonthFromFields(...args) { + calls.push(`call ${objectName}.yearMonthFromFields`); + if ('yearMonthFromFields' in methodOverrides) { + const value = methodOverrides.yearMonthFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.yearMonthFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainYearMonth(isoYear, isoMonth, this, isoDay); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + monthDayFromFields(...args) { + calls.push(`call ${objectName}.monthDayFromFields`); + if ('monthDayFromFields' in methodOverrides) { + const value = methodOverrides.monthDayFromFields; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.monthDayFromFields(...args); + // Replace the calendar in the result with the call-tracking calendar + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainMonthDay(isoMonth, isoDay, this, isoYear); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + dateAdd(...args) { + calls.push(`call ${objectName}.dateAdd`); + if ('dateAdd' in methodOverrides) { + const value = methodOverrides.dateAdd; + return typeof value === "function" ? value(...args) : value; + } + const originalResult = iso8601.dateAdd(...args); + const {isoYear, isoMonth, isoDay} = originalResult.getISOFields(); + const result = new Temporal.PlainDate(isoYear, isoMonth, isoDay, this); + removeExtraHasPropertyChecks(objectName, calls); + return result; + }, + id: "iso8601", + }; + // Automatically generate the other methods that don't need any custom code + [ + "dateUntil", + "day", + "dayOfWeek", + "dayOfYear", + "daysInMonth", + "daysInWeek", + "daysInYear", + "era", + "eraYear", + "fields", + "inLeapYear", + "mergeFields", + "month", + "monthCode", + "monthsInYear", + "toString", + "weekOfYear", + "year", + "yearOfWeek", + ].forEach((methodName) => { + trackingMethods[methodName] = function (...args) { + calls.push(`call ${formatPropertyName(methodName, objectName)}`); + if (methodName in methodOverrides) { + const value = methodOverrides[methodName]; + return typeof value === "function" ? value(...args) : value; + } + return iso8601[methodName](...args); + }; + }); + return new Proxy(trackingMethods, { + get(target, key, receiver) { + const result = Reflect.get(target, key, receiver); + calls.push(`get ${formatPropertyName(key, objectName)}`); + return result; + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * A custom calendar that does not allow any of its methods to be called, for + * the purpose of asserting that a particular operation does not call into + * user code. + */ + calendarThrowEverything() { + class CalendarThrowEverything extends Temporal.Calendar { + constructor() { + super("iso8601"); + } + toString() { + TemporalHelpers.assertUnreachable("toString should not be called"); + } + dateFromFields() { + TemporalHelpers.assertUnreachable("dateFromFields should not be called"); + } + yearMonthFromFields() { + TemporalHelpers.assertUnreachable("yearMonthFromFields should not be called"); + } + monthDayFromFields() { + TemporalHelpers.assertUnreachable("monthDayFromFields should not be called"); + } + dateAdd() { + TemporalHelpers.assertUnreachable("dateAdd should not be called"); + } + dateUntil() { + TemporalHelpers.assertUnreachable("dateUntil should not be called"); + } + era() { + TemporalHelpers.assertUnreachable("era should not be called"); + } + eraYear() { + TemporalHelpers.assertUnreachable("eraYear should not be called"); + } + year() { + TemporalHelpers.assertUnreachable("year should not be called"); + } + month() { + TemporalHelpers.assertUnreachable("month should not be called"); + } + monthCode() { + TemporalHelpers.assertUnreachable("monthCode should not be called"); + } + day() { + TemporalHelpers.assertUnreachable("day should not be called"); + } + fields() { + TemporalHelpers.assertUnreachable("fields should not be called"); + } + mergeFields() { + TemporalHelpers.assertUnreachable("mergeFields should not be called"); + } + } + + return new CalendarThrowEverything(); + }, + + /* + * oneShiftTimeZone(shiftInstant, shiftNanoseconds): + * + * In the case of a spring-forward time zone offset transition (skipped time), + * and disambiguation === 'earlier', BuiltinTimeZoneGetInstantFor subtracts a + * negative number of nanoseconds from a PlainDateTime, which should balance + * with the microseconds field. + * + * This returns an instance of a custom time zone class which skips a length + * of time equal to shiftNanoseconds (a number), at the Temporal.Instant + * shiftInstant. Before shiftInstant, it's identical to UTC, and after + * shiftInstant it's a constant-offset time zone. + * + * It provides a getPossibleInstantsForCalledWith member which is an array + * with the result of calling toString() on any PlainDateTimes passed to + * getPossibleInstantsFor(). + */ + oneShiftTimeZone(shiftInstant, shiftNanoseconds) { + class OneShiftTimeZone extends Temporal.TimeZone { + constructor(shiftInstant, shiftNanoseconds) { + super("+00:00"); + this._shiftInstant = shiftInstant; + this._epoch1 = shiftInstant.epochNanoseconds; + this._epoch2 = this._epoch1 + BigInt(shiftNanoseconds); + this._shiftNanoseconds = shiftNanoseconds; + this._shift = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, this._shiftNanoseconds); + this.getPossibleInstantsForCalledWith = []; + } + + _isBeforeShift(instant) { + return instant.epochNanoseconds < this._epoch1; + } + + getOffsetNanosecondsFor(instant) { + return this._isBeforeShift(instant) ? 0 : this._shiftNanoseconds; + } + + getPossibleInstantsFor(plainDateTime) { + this.getPossibleInstantsForCalledWith.push(plainDateTime.toString({ calendarName: "never" })); + const [instant] = super.getPossibleInstantsFor(plainDateTime); + if (this._shiftNanoseconds > 0) { + if (this._isBeforeShift(instant)) return [instant]; + if (instant.epochNanoseconds < this._epoch2) return []; + return [instant.subtract(this._shift)]; + } + if (instant.epochNanoseconds < this._epoch2) return [instant]; + const shifted = instant.subtract(this._shift); + if (this._isBeforeShift(instant)) return [instant, shifted]; + return [shifted]; + } + + getNextTransition(instant) { + return this._isBeforeShift(instant) ? this._shiftInstant : null; + } + + getPreviousTransition(instant) { + return this._isBeforeShift(instant) ? null : this._shiftInstant; + } + + toString() { + return "Custom/One_Shift"; + } + } + return new OneShiftTimeZone(shiftInstant, shiftNanoseconds); + }, + + /* + * propertyBagObserver(): + * Returns an object that behaves like the given propertyBag but tracks Get + * and Has operations on any of its properties, by appending messages to an + * array. If the value of a property in propertyBag is a primitive, the value + * of the returned object's property will additionally be a + * TemporalHelpers.toPrimitiveObserver that will track calls to its toString + * and valueOf methods in the same array. This is for the purpose of testing + * order of operations that are observable from user code. objectName is used + * in the log. + */ + propertyBagObserver(calls, propertyBag, objectName) { + return new Proxy(propertyBag, { + ownKeys(target) { + calls.push(`ownKeys ${objectName}`); + return Reflect.ownKeys(target); + }, + getOwnPropertyDescriptor(target, key) { + calls.push(`getOwnPropertyDescriptor ${formatPropertyName(key, objectName)}`); + return Reflect.getOwnPropertyDescriptor(target, key); + }, + get(target, key, receiver) { + calls.push(`get ${formatPropertyName(key, objectName)}`); + const result = Reflect.get(target, key, receiver); + if (result === undefined) { + return undefined; + } + if ((result !== null && typeof result === "object") || typeof result === "function") { + return result; + } + return TemporalHelpers.toPrimitiveObserver(calls, result, `${formatPropertyName(key, objectName)}`); + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * specificOffsetTimeZone(): + * + * This returns an instance of a custom time zone class, which returns a + * specific custom value from its getOffsetNanosecondsFrom() method. This is + * for the purpose of testing the validation of what this method returns. + * + * It also returns an empty array from getPossibleInstantsFor(), so as to + * trigger calls to getOffsetNanosecondsFor() when used from the + * BuiltinTimeZoneGetInstantFor operation. + */ + specificOffsetTimeZone(offsetValue) { + class SpecificOffsetTimeZone extends Temporal.TimeZone { + constructor(offsetValue) { + super("UTC"); + this._offsetValue = offsetValue; + } + + getOffsetNanosecondsFor() { + return this._offsetValue; + } + + getPossibleInstantsFor(dt) { + if (typeof this._offsetValue !== 'number' || Math.abs(this._offsetValue) >= 86400e9 || isNaN(this._offsetValue)) return []; + const zdt = dt.toZonedDateTime("UTC").add({ nanoseconds: -this._offsetValue }); + return [zdt.toInstant()]; + } + + get id() { + return this.getOffsetStringFor(new Temporal.Instant(0n)); + } + } + return new SpecificOffsetTimeZone(offsetValue); + }, + + /* + * springForwardFallBackTimeZone(): + * + * This returns an instance of a custom time zone class that implements one + * single spring-forward/fall-back transition, for the purpose of testing the + * disambiguation option, without depending on system time zone data. + * + * The spring-forward occurs at epoch second 954669600 (2000-04-02T02:00 + * local) and goes from offset -08:00 to -07:00. + * + * The fall-back occurs at epoch second 972810000 (2000-10-29T02:00 local) and + * goes from offset -07:00 to -08:00. + */ + springForwardFallBackTimeZone() { + const { compare } = Temporal.PlainDateTime; + const springForwardLocal = new Temporal.PlainDateTime(2000, 4, 2, 2); + const springForwardEpoch = 954669600_000_000_000n; + const fallBackLocal = new Temporal.PlainDateTime(2000, 10, 29, 1); + const fallBackEpoch = 972810000_000_000_000n; + const winterOffset = new Temporal.TimeZone('-08:00'); + const summerOffset = new Temporal.TimeZone('-07:00'); + + class SpringForwardFallBackTimeZone extends Temporal.TimeZone { + constructor() { + super("-08:00"); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < springForwardEpoch || + instant.epochNanoseconds >= fallBackEpoch) { + return winterOffset.getOffsetNanosecondsFor(instant); + } + return summerOffset.getOffsetNanosecondsFor(instant); + } + + getPossibleInstantsFor(datetime) { + if (compare(datetime, springForwardLocal) >= 0 && compare(datetime, springForwardLocal.add({ hours: 1 })) < 0) { + return []; + } + if (compare(datetime, fallBackLocal) >= 0 && compare(datetime, fallBackLocal.add({ hours: 1 })) < 0) { + return [summerOffset.getInstantFor(datetime), winterOffset.getInstantFor(datetime)]; + } + if (compare(datetime, springForwardLocal) < 0 || compare(datetime, fallBackLocal) >= 0) { + return [winterOffset.getInstantFor(datetime)]; + } + return [summerOffset.getInstantFor(datetime)]; + } + + getPreviousTransition(instant) { + if (instant.epochNanoseconds > fallBackEpoch) return new Temporal.Instant(fallBackEpoch); + if (instant.epochNanoseconds > springForwardEpoch) return new Temporal.Instant(springForwardEpoch); + return null; + } + + getNextTransition(instant) { + if (instant.epochNanoseconds < springForwardEpoch) return new Temporal.Instant(springForwardEpoch); + if (instant.epochNanoseconds < fallBackEpoch) return new Temporal.Instant(fallBackEpoch); + return null; + } + + get id() { + return "Custom/Spring_Fall"; + } + + toString() { + return "Custom/Spring_Fall"; + } + } + return new SpringForwardFallBackTimeZone(); + }, + + /* + * timeZoneObserver: + * A custom calendar that behaves exactly like the UTC time zone but tracks + * calls to any of its methods, and Get/Has operations on its properties, by + * appending messages to an array. This is for the purpose of testing order of + * operations that are observable from user code. objectName is used in the + * log. methodOverrides is an optional object containing properties with the + * same name as Temporal.TimeZone methods. If the property value is a function + * it will be called with the proper arguments instead of the UTC method. + * Otherwise, the property value will be returned directly. + */ + timeZoneObserver(calls, objectName, methodOverrides = {}) { + const utc = new Temporal.TimeZone("UTC"); + const trackingMethods = { + id: "UTC", + }; + // Automatically generate the methods + ["getOffsetNanosecondsFor", "getPossibleInstantsFor", "toString"].forEach((methodName) => { + trackingMethods[methodName] = function (...args) { + calls.push(`call ${formatPropertyName(methodName, objectName)}`); + if (methodName in methodOverrides) { + const value = methodOverrides[methodName]; + return typeof value === "function" ? value(...args) : value; + } + return utc[methodName](...args); + }; + }); + return new Proxy(trackingMethods, { + get(target, key, receiver) { + const result = Reflect.get(target, key, receiver); + calls.push(`get ${formatPropertyName(key, objectName)}`); + return result; + }, + has(target, key) { + calls.push(`has ${formatPropertyName(key, objectName)}`); + return Reflect.has(target, key); + }, + }); + }, + + /* + * A custom time zone that does not allow any of its methods to be called, for + * the purpose of asserting that a particular operation does not call into + * user code. + */ + timeZoneThrowEverything() { + class TimeZoneThrowEverything extends Temporal.TimeZone { + constructor() { + super("UTC"); + } + getOffsetNanosecondsFor() { + TemporalHelpers.assertUnreachable("getOffsetNanosecondsFor should not be called"); + } + getPossibleInstantsFor() { + TemporalHelpers.assertUnreachable("getPossibleInstantsFor should not be called"); + } + toString() { + TemporalHelpers.assertUnreachable("toString should not be called"); + } + } + + return new TimeZoneThrowEverything(); + }, + + /* + * Returns an object that will append logs of any Gets or Calls of its valueOf + * or toString properties to the array calls. Both valueOf and toString will + * return the actual primitiveValue. propertyName is used in the log. + */ + toPrimitiveObserver(calls, primitiveValue, propertyName) { + return { + get valueOf() { + calls.push(`get ${propertyName}.valueOf`); + return function () { + calls.push(`call ${propertyName}.valueOf`); + return primitiveValue; + }; + }, + get toString() { + calls.push(`get ${propertyName}.toString`); + return function () { + calls.push(`call ${propertyName}.toString`); + if (primitiveValue === undefined) return undefined; + return primitiveValue.toString(); + }; + }, + }; + }, + + /* + * An object containing further methods that return arrays of ISO strings, for + * testing parsers. + */ + ISO: { + /* + * PlainMonthDay strings that are not valid. + */ + plainMonthDayStringsInvalid() { + return [ + "11-18junk", + "11-18[u-ca=gregory]", + "11-18[u-ca=hebrew]", + ]; + }, + + /* + * PlainMonthDay strings that are valid and that should produce October 1st. + */ + plainMonthDayStringsValid() { + return [ + "10-01", + "1001", + "1965-10-01", + "1976-10-01T152330.1+00:00", + "19761001T15:23:30.1+00:00", + "1976-10-01T15:23:30.1+0000", + "1976-10-01T152330.1+0000", + "19761001T15:23:30.1+0000", + "19761001T152330.1+00:00", + "19761001T152330.1+0000", + "+001976-10-01T152330.1+00:00", + "+0019761001T15:23:30.1+00:00", + "+001976-10-01T15:23:30.1+0000", + "+001976-10-01T152330.1+0000", + "+0019761001T15:23:30.1+0000", + "+0019761001T152330.1+00:00", + "+0019761001T152330.1+0000", + "1976-10-01T15:23:00", + "1976-10-01T15:23", + "1976-10-01T15", + "1976-10-01", + "--10-01", + "--1001", + ]; + }, + + /* + * PlainTime strings that may be mistaken for PlainMonthDay or + * PlainYearMonth strings, and so require a time designator. + */ + plainTimeStringsAmbiguous() { + const ambiguousStrings = [ + "2021-12", // ambiguity between YYYY-MM and HHMM-UU + "2021-12[-12:00]", // ditto, TZ does not disambiguate + "1214", // ambiguity between MMDD and HHMM + "0229", // ditto, including MMDD that doesn't occur every year + "1130", // ditto, including DD that doesn't occur in every month + "12-14", // ambiguity between MM-DD and HH-UU + "12-14[-14:00]", // ditto, TZ does not disambiguate + "202112", // ambiguity between YYYYMM and HHMMSS + "202112[UTC]", // ditto, TZ does not disambiguate + ]; + // Adding a calendar annotation to one of these strings must not cause + // disambiguation in favour of time. + const stringsWithCalendar = ambiguousStrings.map((s) => s + '[u-ca=iso8601]'); + return ambiguousStrings.concat(stringsWithCalendar); + }, + + /* + * PlainTime strings that are of similar form to PlainMonthDay and + * PlainYearMonth strings, but are not ambiguous due to components that + * aren't valid as months or days. + */ + plainTimeStringsUnambiguous() { + return [ + "2021-13", // 13 is not a month + "202113", // ditto + "2021-13[-13:00]", // ditto + "202113[-13:00]", // ditto + "0000-00", // 0 is not a month + "000000", // ditto + "0000-00[UTC]", // ditto + "000000[UTC]", // ditto + "1314", // 13 is not a month + "13-14", // ditto + "1232", // 32 is not a day + "0230", // 30 is not a day in February + "0631", // 31 is not a day in June + "0000", // 0 is neither a month nor a day + "00-00", // ditto + ]; + }, + + /* + * PlainYearMonth-like strings that are not valid. + */ + plainYearMonthStringsInvalid() { + return [ + "2020-13", + ]; + }, + + /* + * PlainYearMonth-like strings that are valid and should produce November + * 1976 in the ISO 8601 calendar. + */ + plainYearMonthStringsValid() { + return [ + "1976-11", + "1976-11-10", + "1976-11-01T09:00:00+00:00", + "1976-11-01T00:00:00+05:00", + "197611", + "+00197611", + "1976-11-18T15:23:30.1\u221202:00", + "1976-11-18T152330.1+00:00", + "19761118T15:23:30.1+00:00", + "1976-11-18T15:23:30.1+0000", + "1976-11-18T152330.1+0000", + "19761118T15:23:30.1+0000", + "19761118T152330.1+00:00", + "19761118T152330.1+0000", + "+001976-11-18T152330.1+00:00", + "+0019761118T15:23:30.1+00:00", + "+001976-11-18T15:23:30.1+0000", + "+001976-11-18T152330.1+0000", + "+0019761118T15:23:30.1+0000", + "+0019761118T152330.1+00:00", + "+0019761118T152330.1+0000", + "1976-11-18T15:23", + "1976-11-18T15", + "1976-11-18", + ]; + }, + + /* + * PlainYearMonth-like strings that are valid and should produce November of + * the ISO year -9999. + */ + plainYearMonthStringsValidNegativeYear() { + return [ + "\u2212009999-11", + ]; + }, + } +}; diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/browser.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/browser.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/nanoseconds-subtracted-or-added-at-dst-transition.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/nanoseconds-subtracted-or-added-at-dst-transition.js new file mode 100644 index 0000000000..1e978d0936 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/nanoseconds-subtracted-or-added-at-dst-transition.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.timezone.prototype.getprevioustransition +description: > + Test previous transition when nanoseconds are subtracted resp. added to the DST transition. +features: [Temporal] +---*/ + +let tz = new Temporal.TimeZone("Europe/Berlin"); +let p = Temporal.Instant.from("2021-03-28T01:00:00Z"); + +assert.sameValue(tz.getPreviousTransition(p.add({nanoseconds: -1})).toString(), + "2020-10-25T01:00:00Z", + "DST transition minus one nanosecond"); + +assert.sameValue(tz.getPreviousTransition(p).toString(), + "2020-10-25T01:00:00Z", + "DST transition"); + +assert.sameValue(tz.getPreviousTransition(p.add({nanoseconds: +1})).toString(), + "2021-03-28T01:00:00Z", + "DST transition plus one nanosecond"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/shell.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/transition-at-instant-boundaries.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/transition-at-instant-boundaries.js new file mode 100644 index 0000000000..21c4da725b --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/getPreviousTransition/transition-at-instant-boundaries.js @@ -0,0 +1,29 @@ +// |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.timezone.prototype.getprevioustransition +description: > + Test transitions at the instant boundaries. +features: [Temporal, Intl-enumeration] +---*/ + +const min = new Temporal.Instant(-86_40000_00000_00000_00000n); +const max = new Temporal.Instant(86_40000_00000_00000_00000n); + +for (let id of Intl.supportedValuesOf("timeZone")) { + let tz = new Temporal.TimeZone(id); + + // If there's any previous transition, it should be before |max|. + let prev = tz.getPreviousTransition(max); + if (prev) { + assert(prev.epochNanoseconds < max.epochNanoseconds); + } + + // There shouldn't be any previous transition before |min|. + prev = tz.getPreviousTransition(min); + assert.sameValue(prev, null); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/prototype/shell.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/shell.js b/js/src/tests/test262/intl402/Temporal/TimeZone/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/shell.js diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/supported-values-of.js b/js/src/tests/test262/intl402/Temporal/TimeZone/supported-values-of.js new file mode 100644 index 0000000000..c8ea68cfa4 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/supported-values-of.js @@ -0,0 +1,19 @@ +// |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.timezone +description: > + TimeZone constructor accepts all time zone identifiers from Intl.supportedValuesOf. +features: [Temporal, Intl-enumeration] +---*/ + +// Ensure all identifiers are valid and canonical. +for (let id of Intl.supportedValuesOf("timeZone")) { + let tz = new Temporal.TimeZone(id); + + assert.sameValue(tz.id, id); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/intl402/Temporal/TimeZone/timezone-case-insensitive.js b/js/src/tests/test262/intl402/Temporal/TimeZone/timezone-case-insensitive.js new file mode 100644 index 0000000000..bf0b77caf7 --- /dev/null +++ b/js/src/tests/test262/intl402/Temporal/TimeZone/timezone-case-insensitive.js @@ -0,0 +1,15 @@ +// |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.timezone +description: Time zone names are case insensitive +features: [Temporal] +---*/ + +const timeZone = 'eTc/gMt+1'; +const result = new Temporal.TimeZone(timeZone); +assert.sameValue(result.toString(), 'Etc/GMT+1', `Time zone created from string "${timeZone}"`); + +reportCompare(0, 0); |