From a54c3f81f77483a8ffac6183d19174db47e77b6d Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 5 Jun 2026 15:05:04 +0100 Subject: [PATCH 01/13] WS-NA: Kick off prompt, add tests for missing edge cases --- .../timeFormatTests/testUtils.js | 19 ++++ .../timeFormatTests/timeFormat.test.js | 46 +++++++++- .../src/utilities/index.test.js | 89 +++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js index 8d774147929..65def6c4d1f 100644 --- a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js +++ b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js @@ -51,3 +51,22 @@ export const format = ({ datetimeLocale, timezone, fixture, altCalendar }) => { return formatted; }; + +export const formatTimestamp = ({ + datetimeLocale, + timezone, + time, + altCalendar, + dateTimeFormat = formatDateAndTime, +}) => { + const dateWithTimezone = moment.tz(time, timezone).locale(datetimeLocale); + const formatted = dateWithTimezone.format(dateTimeFormat(datetimeLocale)); + + if (altCalendar) { + const altCalendarFormatted = altCalendar.formatDate(dateWithTimezone); + + return `${altCalendarFormatted} - ${formatted}`; + } + + return formatted; +}; diff --git a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js index 8d2d8e33d48..c97e765d66c 100644 --- a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js +++ b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js @@ -1,6 +1,6 @@ import services from '#utilities/serviceConfigs'; import expectedFormats from './expectedFormats.json'; -import { format, timestampsFixtures } from './testUtils'; +import { format, formatTimestamp, timestampsFixtures } from './testUtils'; describe('Timestamp Formats', () => { Object.keys(services).forEach(service => { @@ -22,4 +22,48 @@ describe('Timestamp Formats', () => { }); }); }); + + describe('characterization edge cases', () => { + it('should preserve timezone output around DST start for Europe/London', () => { + const beforeDSTStart = Date.UTC(2021, 2, 28, 0, 30, 0); // 00:30 GMT + const afterDSTStart = Date.UTC(2021, 2, 28, 1, 30, 0); // 02:30 BST + + expect( + formatTimestamp({ + datetimeLocale: 'en-gb', + timezone: 'Europe/London', + time: beforeDSTStart, + }), + ).toEqual('28 March 2021, 00:30 GMT'); + + expect( + formatTimestamp({ + datetimeLocale: 'en-gb', + timezone: 'Europe/London', + time: afterDSTStart, + }), + ).toEqual('28 March 2021, 02:30 BST'); + }); + + it('should preserve timezone output around DST end for Europe/London', () => { + const beforeDSTEnd = Date.UTC(2021, 9, 31, 0, 30, 0); // 01:30 BST + const afterDSTEnd = Date.UTC(2021, 9, 31, 1, 30, 0); // 01:30 GMT + + expect( + formatTimestamp({ + datetimeLocale: 'en-gb', + timezone: 'Europe/London', + time: beforeDSTEnd, + }), + ).toEqual('31 October 2021, 01:30 BST'); + + expect( + formatTimestamp({ + datetimeLocale: 'en-gb', + timezone: 'Europe/London', + time: afterDSTEnd, + }), + ).toEqual('31 October 2021, 01:30 GMT'); + }); + }); }); diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js index b08ed082a76..c298baa961c 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js @@ -88,6 +88,7 @@ describe('Timestamp utility functions', () => { expect(result).toEqual('1 January 2017'); }); it('should return relative timestamp if isRelative is true', () => { + const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(1704110400000); // 1 January 2024 12:00:00 UTC const nineHoursAgo = timestampGenerator({ hours: 9 }); const output = formatUnixTimestamp({ timestamp: nineHoursAgo, @@ -98,6 +99,7 @@ describe('Timestamp utility functions', () => { }); const expectedOutput = '9 hours ago'; expect(output).toEqual(expectedOutput); + nowSpy.mockRestore(); }); it('should return timestamp with format if format is provided', () => { @@ -132,10 +134,60 @@ describe('Timestamp utility functions', () => { }); expect(result).toBeUndefined(); }); + + it('should correctly format timezone labels around DST start in Europe/London', () => { + const beforeDSTStart = Date.UTC(2021, 2, 28, 0, 30, 0); // 00:30 GMT + const afterDSTStart = Date.UTC(2021, 2, 28, 1, 30, 0); // 02:30 BST + + expect( + formatUnixTimestamp({ + timestamp: beforeDSTStart, + format: 'D MMMM YYYY, HH:mm z', + timezone, + locale, + }), + ).toEqual('28 March 2021, 00:30 GMT'); + + expect( + formatUnixTimestamp({ + timestamp: afterDSTStart, + format: 'D MMMM YYYY, HH:mm z', + timezone, + locale, + }), + ).toEqual('28 March 2021, 02:30 BST'); + }); + + it('should correctly format timezone labels around DST end in Europe/London', () => { + const beforeDSTEnd = Date.UTC(2021, 9, 31, 0, 30, 0); // 01:30 BST + const afterDSTEnd = Date.UTC(2021, 9, 31, 1, 30, 0); // 01:30 GMT + + expect( + formatUnixTimestamp({ + timestamp: beforeDSTEnd, + format: 'D MMMM YYYY, HH:mm z', + timezone, + locale, + }), + ).toEqual('31 October 2021, 01:30 BST'); + + expect( + formatUnixTimestamp({ + timestamp: afterDSTEnd, + format: 'D MMMM YYYY, HH:mm z', + timezone, + locale, + }), + ).toEqual('31 October 2021, 01:30 GMT'); + }); }); }); describe('Moment configuration', () => { + afterEach(() => { + moment.now = () => Date.now(); + }); + it('rounds down', () => { const wouldOtherwiseRoundUp = moment() .subtract(59, 'minutes') @@ -239,6 +291,43 @@ describe('Moment configuration', () => { expect(allButAYear.fromNow()).toEqual('11 months ago'); }); + it('uses configured threshold boundaries between minutes/hours/days/months/years', () => { + const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); + moment.now = jest.fn().mockImplementation(() => fixedNow); + + const fiftyNineMinutesFiftyNineSeconds = moment( + fixedNow - (59 * 60 + 59) * 1000, + ); + expect(fiftyNineMinutesFiftyNineSeconds.fromNow()).toEqual( + '59 minutes ago', + ); + + const sixtyMinutes = moment(fixedNow - 60 * 60 * 1000); + expect(sixtyMinutes.fromNow()).toEqual('an hour ago'); + + const allButOneDay = moment( + fixedNow - (23 * 60 * 60 + 59 * 60 + 59) * 1000, + ); + expect(allButOneDay.fromNow()).toEqual('23 hours ago'); + + const oneDay = moment(fixedNow - 24 * 60 * 60 * 1000); + expect(oneDay.fromNow()).toEqual('a day ago'); + + const allButOneMonth = moment( + fixedNow - (29 * 24 * 60 * 60 + 23 * 60 * 60 + 59 * 60 + 59) * 1000, + ); + expect(allButOneMonth.fromNow()).toEqual('29 days ago'); + + const thirtyDays = moment(fixedNow - 30 * 24 * 60 * 60 * 1000); + expect(thirtyDays.fromNow()).toEqual('a month ago'); + + const elevenMonths = moment(fixedNow).subtract(11, 'months'); + expect(elevenMonths.fromNow()).toEqual('11 months ago'); + + const twelveMonths = moment(fixedNow).subtract(12, 'months'); + expect(twelveMonths.fromNow()).toEqual('a year ago'); + }); + describe('formatDuration', () => { it('should return duration in default format', () => { const duration = 'PT30M'; // 30:00 From 44e6c0548587dea1678994858738877eef801632 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 10:47:44 +0100 Subject: [PATCH 02/13] WS-NA-Temporal: Cleans tests --- .../src/utilities/index.test.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js index c298baa961c..df21708f863 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js @@ -291,7 +291,7 @@ describe('Moment configuration', () => { expect(allButAYear.fromNow()).toEqual('11 months ago'); }); - it('uses configured threshold boundaries between minutes/hours/days/months/years', () => { + it('transitions from minutes to hours at exactly 60 minutes', () => { const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); moment.now = jest.fn().mockImplementation(() => fixedNow); @@ -304,6 +304,11 @@ describe('Moment configuration', () => { const sixtyMinutes = moment(fixedNow - 60 * 60 * 1000); expect(sixtyMinutes.fromNow()).toEqual('an hour ago'); + }); + + it('transitions from hours to days at exactly 24 hours', () => { + const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); + moment.now = jest.fn().mockImplementation(() => fixedNow); const allButOneDay = moment( fixedNow - (23 * 60 * 60 + 59 * 60 + 59) * 1000, @@ -312,6 +317,11 @@ describe('Moment configuration', () => { const oneDay = moment(fixedNow - 24 * 60 * 60 * 1000); expect(oneDay.fromNow()).toEqual('a day ago'); + }); + + it('transitions from days to months at exactly 30 days', () => { + const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); + moment.now = jest.fn().mockImplementation(() => fixedNow); const allButOneMonth = moment( fixedNow - (29 * 24 * 60 * 60 + 23 * 60 * 60 + 59 * 60 + 59) * 1000, @@ -320,6 +330,11 @@ describe('Moment configuration', () => { const thirtyDays = moment(fixedNow - 30 * 24 * 60 * 60 * 1000); expect(thirtyDays.fromNow()).toEqual('a month ago'); + }); + + it('transitions from months to years at exactly 12 months', () => { + const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); + moment.now = jest.fn().mockImplementation(() => fixedNow); const elevenMonths = moment(fixedNow).subtract(11, 'months'); expect(elevenMonths.fromNow()).toEqual('11 months ago'); From 1f631f00aa22d8b487f5a62f52d3604d4d2a6684 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 10:53:31 +0100 Subject: [PATCH 03/13] WS-NA-Temporal: Adds tests for further timezones --- .../src/utilities/index.test.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js index df21708f863..422f95e55f5 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js @@ -180,6 +180,71 @@ describe('Timestamp utility functions', () => { }), ).toEqual('31 October 2021, 01:30 GMT'); }); + + it('should apply a fixed UTC+1 offset for Africa/Lagos (no DST)', () => { + const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC + + expect( + formatUnixTimestamp({ + timestamp: utcTimestamp, + format: 'D MMMM YYYY, HH:mm z', + timezone: 'Africa/Lagos', + locale, + }), + ).toEqual('15 June 2021, 13:00 WAT'); + }); + + it('should apply a +05:45 offset for Asia/Kathmandu (no DST, non-whole-hour offset)', () => { + const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC + + expect( + formatUnixTimestamp({ + timestamp: utcTimestamp, + format: 'D MMMM YYYY, HH:mm z', + timezone: 'Asia/Kathmandu', + locale, + }), + ).toEqual('15 June 2021, 17:45 +0545'); + }); + + it('should apply a negative UTC-3 offset for America/Sao_Paulo', () => { + const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC + + expect( + formatUnixTimestamp({ + timestamp: utcTimestamp, + format: 'D MMMM YYYY, HH:mm z', + timezone: 'America/Sao_Paulo', + locale, + }), + ).toEqual('15 June 2021, 09:00 -03'); + }); + + it('should format correctly for GMT (non-region-style IANA identifier)', () => { + const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC + + expect( + formatUnixTimestamp({ + timestamp: utcTimestamp, + format: 'D MMMM YYYY, HH:mm z', + timezone: 'GMT', + locale, + }), + ).toEqual('15 June 2021, 12:00 GMT'); + }); + + it('should format correctly for Asia/baku (non-canonical casing accepted by moment, will need handling in Temporal)', () => { + const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC + + expect( + formatUnixTimestamp({ + timestamp: utcTimestamp, + format: 'D MMMM YYYY, HH:mm z', + timezone: 'Asia/baku', + locale, + }), + ).toEqual('15 June 2021, 16:00 +04'); + }); }); }); From 50424db6b02df5dec0cb3a68c05a924833ca3b7f Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 11:05:42 +0100 Subject: [PATCH 04/13] WS-NA-Temporal: Adds test coverage for locales on formatUnixTimestamp --- .../src/utilities/index.test.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js index 422f95e55f5..4f05fc86c2c 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js @@ -245,6 +245,42 @@ describe('Timestamp utility functions', () => { }), ).toEqual('15 June 2021, 16:00 +04'); }); + + it('should translate month names for a non-Latin locale', () => { + expect( + formatUnixTimestamp({ + timestamp, + format: 'D MMMM YYYY', + timezone: 'GMT', + locale: 'ar', + }), + ).toEqual('١٩ أكتوبر ٢٠١٨'); + }); + + it('should apply locale-sensitive format tokens (LL, LT) for a non-Latin locale', () => { + expect( + formatUnixTimestamp({ + timestamp, + format: null, + timezone: 'GMT', + locale: 'ar', + }), + ).toEqual('١٩ أكتوبر ٢٠١٨، ١٧:١٠ GMT'); + }); + + it('should return relative timestamp in the provided locale', () => { + const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(1704110400000); // 1 January 2024 12:00:00 UTC + const nineHoursAgo = timestampGenerator({ hours: 9 }); + const output = formatUnixTimestamp({ + timestamp: nineHoursAgo, + format: 'D MMMM YYYY', + timezone: 'GMT', + locale: 'ar', + isRelative: true, + }); + expect(output).toEqual('منذ ٩ ساعات'); + nowSpy.mockRestore(); + }); }); }); From c6fdcd375bc9dca2ec3a2ab7a18309e880f4ca50 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 11:34:53 +0100 Subject: [PATCH 05/13] WS-NA-Temporal: Tidies article timestamp tests --- .../timeFormatTests/testUtils.js | 1 + .../timeFormatTests/timeFormat.test.js | 51 +++++++++++++++---- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js index 65def6c4d1f..769ca1f96da 100644 --- a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js +++ b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/testUtils.js @@ -52,6 +52,7 @@ export const format = ({ datetimeLocale, timezone, fixture, altCalendar }) => { return formatted; }; +// allows precise checks for edge cases without fixture data export const formatTimestamp = ({ datetimeLocale, timezone, diff --git a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js index c97e765d66c..ae40af92ca1 100644 --- a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js +++ b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/timeFormat.test.js @@ -23,23 +23,23 @@ describe('Timestamp Formats', () => { }); }); - describe('characterization edge cases', () => { + describe('Timestamp Edge Cases', () => { it('should preserve timezone output around DST start for Europe/London', () => { const beforeDSTStart = Date.UTC(2021, 2, 28, 0, 30, 0); // 00:30 GMT const afterDSTStart = Date.UTC(2021, 2, 28, 1, 30, 0); // 02:30 BST expect( formatTimestamp({ - datetimeLocale: 'en-gb', - timezone: 'Europe/London', + datetimeLocale: services.news.default.datetimeLocale, + timezone: services.news.default.timezone, time: beforeDSTStart, }), ).toEqual('28 March 2021, 00:30 GMT'); expect( formatTimestamp({ - datetimeLocale: 'en-gb', - timezone: 'Europe/London', + datetimeLocale: services.news.default.datetimeLocale, + timezone: services.news.default.timezone, time: afterDSTStart, }), ).toEqual('28 March 2021, 02:30 BST'); @@ -51,19 +51,52 @@ describe('Timestamp Formats', () => { expect( formatTimestamp({ - datetimeLocale: 'en-gb', - timezone: 'Europe/London', + datetimeLocale: services.news.default.datetimeLocale, + timezone: services.news.default.timezone, time: beforeDSTEnd, }), ).toEqual('31 October 2021, 01:30 BST'); expect( formatTimestamp({ - datetimeLocale: 'en-gb', - timezone: 'Europe/London', + datetimeLocale: services.news.default.datetimeLocale, + timezone: services.news.default.timezone, time: afterDSTEnd, }), ).toEqual('31 October 2021, 01:30 GMT'); }); + + it('should preserve half-hour offset output for Asia/Kabul', () => { + const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC + + expect( + formatTimestamp({ + datetimeLocale: services.dari.default.datetimeLocale, + timezone: services.dari.default.timezone, + time: utcTimestamp, + }), + ).toEqual('۱۵ جون ۲۰۲۱ ۱۶:۳۰'); + }); + + it('should preserve timezone output around DST start for Europe/Bucharest', () => { + const beforeDSTStart = Date.UTC(2021, 2, 28, 0, 30, 0); // 02:30 EET + const afterDSTStart = Date.UTC(2021, 2, 28, 1, 30, 0); // 04:30 EEST + + expect( + formatTimestamp({ + datetimeLocale: services.romania.default.datetimeLocale, + timezone: services.romania.default.timezone, + time: beforeDSTStart, + }), + ).toEqual('28 martie 2021, 02:30 EET'); + + expect( + formatTimestamp({ + datetimeLocale: services.romania.default.datetimeLocale, + timezone: services.romania.default.timezone, + time: afterDSTStart, + }), + ).toEqual('28 martie 2021, 04:30 EEST'); + }); }); }); From e0838d5375956896167409b8bd156b374c811ebc Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 11:49:56 +0100 Subject: [PATCH 06/13] WS-NA-temporal: Fixes typo in service config --- src/app/lib/config/services/azeri.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/lib/config/services/azeri.ts b/src/app/lib/config/services/azeri.ts index 72d91879179..0a2790f51dd 100644 --- a/src/app/lib/config/services/azeri.ts +++ b/src/app/lib/config/services/azeri.ts @@ -464,7 +464,7 @@ export const service: DefaultServiceConfig = { ], copyrightText: 'BBC. BBC kənar saytların məzmununa məsul deyil.', }, - timezone: 'Asia/baku', + timezone: 'Asia/Baku', navigation: [ { title: 'Xəbərlər', From 4febfc54cdc9fe4a54ec7a1477cf1f7c7f55ad5c Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 11:50:20 +0100 Subject: [PATCH 07/13] WS-NA-temporal: Adds un-abbreviated form of DST to readme --- .../containers/ArticleTimestamp/timeFormatTests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/README.md b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/README.md index 5b5b23d8e8c..81f21d60b07 100644 --- a/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/README.md +++ b/src/app/legacy/containers/ArticleTimestamp/timeFormatTests/README.md @@ -12,7 +12,7 @@ A couple of scenarios that we expect these tests would fail and need updating wo - Change in the timeformat logic [here](../timeFormats) -- Changes in DST times across different timezones. +- Changes in Daylight Saving Time (DST) times across different timezones. - ... please add more if you find any From 81a7bd65bf29ace30f400971f321c10d96df9f95 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 11:50:45 +0100 Subject: [PATCH 08/13] WS-NA-temporal: tidies tests --- .../src/utilities/index.test.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js index 4f05fc86c2c..96f3d09f717 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js @@ -233,19 +233,6 @@ describe('Timestamp utility functions', () => { ).toEqual('15 June 2021, 12:00 GMT'); }); - it('should format correctly for Asia/baku (non-canonical casing accepted by moment, will need handling in Temporal)', () => { - const utcTimestamp = Date.UTC(2021, 5, 15, 12, 0, 0); // 15 June 2021 12:00 UTC - - expect( - formatUnixTimestamp({ - timestamp: utcTimestamp, - format: 'D MMMM YYYY, HH:mm z', - timezone: 'Asia/baku', - locale, - }), - ).toEqual('15 June 2021, 16:00 +04'); - }); - it('should translate month names for a non-Latin locale', () => { expect( formatUnixTimestamp({ From 4866b6a27995b5ffcae0063eb1f695d41ef6a31e Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 11:56:21 +0100 Subject: [PATCH 09/13] WS-NA-Temporal: removes tests I think are extra: --- .../src/utilities/index.test.js | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js index 96f3d09f717..0f96d26154a 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js @@ -379,58 +379,6 @@ describe('Moment configuration', () => { expect(allButAYear.fromNow()).toEqual('11 months ago'); }); - it('transitions from minutes to hours at exactly 60 minutes', () => { - const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); - moment.now = jest.fn().mockImplementation(() => fixedNow); - - const fiftyNineMinutesFiftyNineSeconds = moment( - fixedNow - (59 * 60 + 59) * 1000, - ); - expect(fiftyNineMinutesFiftyNineSeconds.fromNow()).toEqual( - '59 minutes ago', - ); - - const sixtyMinutes = moment(fixedNow - 60 * 60 * 1000); - expect(sixtyMinutes.fromNow()).toEqual('an hour ago'); - }); - - it('transitions from hours to days at exactly 24 hours', () => { - const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); - moment.now = jest.fn().mockImplementation(() => fixedNow); - - const allButOneDay = moment( - fixedNow - (23 * 60 * 60 + 59 * 60 + 59) * 1000, - ); - expect(allButOneDay.fromNow()).toEqual('23 hours ago'); - - const oneDay = moment(fixedNow - 24 * 60 * 60 * 1000); - expect(oneDay.fromNow()).toEqual('a day ago'); - }); - - it('transitions from days to months at exactly 30 days', () => { - const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); - moment.now = jest.fn().mockImplementation(() => fixedNow); - - const allButOneMonth = moment( - fixedNow - (29 * 24 * 60 * 60 + 23 * 60 * 60 + 59 * 60 + 59) * 1000, - ); - expect(allButOneMonth.fromNow()).toEqual('29 days ago'); - - const thirtyDays = moment(fixedNow - 30 * 24 * 60 * 60 * 1000); - expect(thirtyDays.fromNow()).toEqual('a month ago'); - }); - - it('transitions from months to years at exactly 12 months', () => { - const fixedNow = Date.UTC(2022, 6, 15, 12, 0, 0); - moment.now = jest.fn().mockImplementation(() => fixedNow); - - const elevenMonths = moment(fixedNow).subtract(11, 'months'); - expect(elevenMonths.fromNow()).toEqual('11 months ago'); - - const twelveMonths = moment(fixedNow).subtract(12, 'months'); - expect(twelveMonths.fromNow()).toEqual('a year ago'); - }); - describe('formatDuration', () => { it('should return duration in default format', () => { const duration = 'PT30M'; // 30:00 From 25557f36a2651a7de4492708d052009e85a54965 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 12:40:04 +0100 Subject: [PATCH 10/13] WS-NA: Adds initial storybook checks --- .../ArticleTimestamp/index.stories.jsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx b/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx index 17568ac090f..e878f20e406 100644 --- a/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx +++ b/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx @@ -1,4 +1,5 @@ import WithTimeMachine from '../../../../testHelpers/withTimeMachine'; +import { ServiceContextProvider } from '#app/contexts/ServiceContext'; import ArticleTimestamp from '.'; import { timestampGenerator } from './testHelpers'; @@ -93,3 +94,46 @@ export const H = () => ( ); H.storyName = 'lastPublished today and more than 10 hours ago and firstPublished before today'; + +// Timezone and locale migration checks for Moment → Temporal +const timezoneLocaleServices = [ + { service: 'news', label: 'English (Europe/London)' }, + { service: 'azeri', label: 'Azeri (Asia/Baku)' }, + { service: 'arabic', label: 'Arabic (GMT)' }, + { service: 'portuguese', label: 'Portuguese (America/Sao_Paulo)' }, + { service: 'nepali', label: 'Nepali (Asia/Kathmandu)' }, +]; + +const dstBoundaryTimestamp = Date.UTC(2021, 2, 28, 1, 0, 0); // 28 March 2021 01:00 UTC (DST boundary) +const fixedOlderTimestamp = Date.UTC(2021, 2, 27, 12, 0, 0); // 27 March 2021 12:00 UTC + +export const TimezoneAndLocaleMigrationChecks = () => ( +
+ {timezoneLocaleServices.map(({ service, label }) => ( + +
+ {label} +
+ +
+
+
+ ))} +
+); +TimezoneAndLocaleMigrationChecks.storyName = + 'Timezone and Locale Migration Checks (Pre-Temporal)'; +TimezoneAndLocaleMigrationChecks.parameters = { + chromatic: { + disable: false, + }, +}; From b6cbabe34728e4fe82628caedcca286346455263 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 12:53:23 +0100 Subject: [PATCH 11/13] WS-NA: Adds some chromatic tests for DST and timezones/ locales --- .../legacy/containers/ArticleTimestamp/index.stories.jsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx b/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx index e878f20e406..f385ab07ff8 100644 --- a/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx +++ b/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx @@ -99,7 +99,7 @@ H.storyName = const timezoneLocaleServices = [ { service: 'news', label: 'English (Europe/London)' }, { service: 'azeri', label: 'Azeri (Asia/Baku)' }, - { service: 'arabic', label: 'Arabic (GMT)' }, + { service: 'arabic', label: 'Arabic (GMT, RTL)' }, { service: 'portuguese', label: 'Portuguese (America/Sao_Paulo)' }, { service: 'nepali', label: 'Nepali (Asia/Kathmandu)' }, ]; @@ -107,7 +107,7 @@ const timezoneLocaleServices = [ const dstBoundaryTimestamp = Date.UTC(2021, 2, 28, 1, 0, 0); // 28 March 2021 01:00 UTC (DST boundary) const fixedOlderTimestamp = Date.UTC(2021, 2, 27, 12, 0, 0); // 27 March 2021 12:00 UTC -export const TimezoneAndLocaleMigrationChecks = () => ( +export const TimezoneAndLocaleChecks = () => (
{timezoneLocaleServices.map(({ service, label }) => ( @@ -130,10 +130,9 @@ export const TimezoneAndLocaleMigrationChecks = () => ( ))}
); -TimezoneAndLocaleMigrationChecks.storyName = - 'Timezone and Locale Migration Checks (Pre-Temporal)'; -TimezoneAndLocaleMigrationChecks.parameters = { +TimezoneAndLocaleChecks.parameters = { chromatic: { disable: false, }, }; +TimezoneAndLocaleChecks.tags = ['!dev']; From 4d15b0de506a731abc5a4ea12b9d6123a668a686 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 16:31:04 +0100 Subject: [PATCH 12/13] WS-NA-Temporal: Adds story for Persian Jalali --- src/app/legacy/containers/ArticleTimestamp/index.stories.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx b/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx index f385ab07ff8..9995cbbf4fb 100644 --- a/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx +++ b/src/app/legacy/containers/ArticleTimestamp/index.stories.jsx @@ -99,6 +99,7 @@ H.storyName = const timezoneLocaleServices = [ { service: 'news', label: 'English (Europe/London)' }, { service: 'azeri', label: 'Azeri (Asia/Baku)' }, + { service: 'persian', label: 'Persian (GMT, Jalali calendar)' }, { service: 'arabic', label: 'Arabic (GMT, RTL)' }, { service: 'portuguese', label: 'Portuguese (America/Sao_Paulo)' }, { service: 'nepali', label: 'Nepali (Asia/Kathmandu)' }, From bb34a8cabcf13033239588515b52a02559c66846 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 3 Jul 2026 17:21:52 +0100 Subject: [PATCH 13/13] WS-NA-temporal: Commit CR suggestion --- .../src/utilities/index.test.js | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js index 0f96d26154a..92d84eda326 100644 --- a/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js +++ b/src/app/legacy/psammead/psammead-timestamp-container/src/utilities/index.test.js @@ -88,7 +88,6 @@ describe('Timestamp utility functions', () => { expect(result).toEqual('1 January 2017'); }); it('should return relative timestamp if isRelative is true', () => { - const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(1704110400000); // 1 January 2024 12:00:00 UTC const nineHoursAgo = timestampGenerator({ hours: 9 }); const output = formatUnixTimestamp({ timestamp: nineHoursAgo, @@ -99,7 +98,6 @@ describe('Timestamp utility functions', () => { }); const expectedOutput = '9 hours ago'; expect(output).toEqual(expectedOutput); - nowSpy.mockRestore(); }); it('should return timestamp with format if format is provided', () => { @@ -257,25 +255,24 @@ describe('Timestamp utility functions', () => { it('should return relative timestamp in the provided locale', () => { const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(1704110400000); // 1 January 2024 12:00:00 UTC - const nineHoursAgo = timestampGenerator({ hours: 9 }); - const output = formatUnixTimestamp({ - timestamp: nineHoursAgo, - format: 'D MMMM YYYY', - timezone: 'GMT', - locale: 'ar', - isRelative: true, - }); - expect(output).toEqual('منذ ٩ ساعات'); - nowSpy.mockRestore(); + try { + const nineHoursAgo = timestampGenerator({ hours: 9 }); + const output = formatUnixTimestamp({ + timestamp: nineHoursAgo, + format: 'D MMMM YYYY', + timezone: 'GMT', + locale: 'ar', + isRelative: true, + }); + expect(output).toEqual('منذ ٩ ساعات'); + } finally { + nowSpy.mockRestore(); + } }); }); }); describe('Moment configuration', () => { - afterEach(() => { - moment.now = () => Date.now(); - }); - it('rounds down', () => { const wouldOtherwiseRoundUp = moment() .subtract(59, 'minutes')