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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/app/legacy/containers/ArticleTimestamp/index.stories.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import WithTimeMachine from '../../../../testHelpers/withTimeMachine';
import { ServiceContextProvider } from '#app/contexts/ServiceContext';
import ArticleTimestamp from '.';
import { timestampGenerator } from './testHelpers';

Expand Down Expand Up @@ -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: '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)' },
];

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 TimezoneAndLocaleChecks = () => (
<div>
{timezoneLocaleServices.map(({ service, label }) => (
<ServiceContextProvider key={service} service={service}>
<div
style={{
marginBottom: '2rem',
padding: '1rem',
border: '1px solid #ccc',
}}
>
<strong>{label}</strong>
<div style={{ marginTop: '0.5rem' }}>
<ArticleTimestamp
firstPublished={fixedOlderTimestamp}
lastPublished={dstBoundaryTimestamp}
/>
</div>
</div>
</ServiceContextProvider>
))}
</div>
);
TimezoneAndLocaleChecks.parameters = {
chromatic: {
disable: false,
},
};
TimezoneAndLocaleChecks.tags = ['!dev'];
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,23 @@ export const format = ({ datetimeLocale, timezone, fixture, altCalendar }) => {

return formatted;
};

// allows precise checks for edge cases without fixture data
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;
};
Original file line number Diff line number Diff line change
@@ -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 => {
Expand All @@ -22,4 +22,81 @@ describe('Timestamp Formats', () => {
});
});
});

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: services.news.default.datetimeLocale,
timezone: services.news.default.timezone,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, gets datetimeLocale and timezone from the service config (similar to the other tests above)

time: beforeDSTStart,
}),
).toEqual('28 March 2021, 00:30 GMT');

expect(
formatTimestamp({
datetimeLocale: services.news.default.datetimeLocale,
timezone: services.news.default.timezone,
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: services.news.default.datetimeLocale,
timezone: services.news.default.timezone,
time: beforeDSTEnd,
}),
).toEqual('31 October 2021, 01:30 BST');

expect(
formatTimestamp({
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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,143 @@ 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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, uses default locale since other tests in this file use this

}),
).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');
});

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 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
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();
}
});
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/app/lib/config/services/azeri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading