Files
Supra4E8C 3eb9f3cc71 refactor(quota): unify Codex reset-credit expiry on browser-local time
Reset-credit expiry was formatted in a hardcoded Asia/Shanghai while
every other timestamp on the page — window resets, the timeline — used
the browser's timezone. The same credit therefore appeared twice on one
screen in two different timezones: GMT+8 on the card, local in the
timeline tooltip.

Credits now use the same local MM-DD HH:mm shape as every other reset
label, and the "(GMT+8)" that was baked into the heading in all four
locales becomes a runtime-resolved offset, so the timezone is stated
rather than assumed.

The offset is computed arithmetically rather than via Intl's
timeZoneName: 'shortOffset' — that is ES2022-only, needs its locale
pinned to stay ASCII across the four UI languages, and disagrees at UTC
itself (GMT+0 vs GMT).
2026-08-02 01:54:20 +08:00

72 lines
2.7 KiB
TypeScript

/**
* Timezone labelling, and the guard against the Asia/Shanghai hardcode
* returning.
*
* Codex reset-credit expiry was rendered in a fixed GMT+8 while every other
* timestamp on the same page used the browser's timezone — so one credit
* appeared twice, in two timezones, on one screen.
*/
import { describe, expect, test } from 'bun:test';
import i18n from '@/i18n';
import * as resetCredits from '@/utils/quota/resetCredits';
import { formatUtcOffsetLabel, resolveTimeZoneLabel } from '@/utils/time/timezone';
describe('formatUtcOffsetLabel', () => {
test('renders whole-hour offsets east and west of UTC', () => {
expect(formatUtcOffsetLabel(480)).toBe('GMT+8');
expect(formatUtcOffsetLabel(-300)).toBe('GMT-5');
expect(formatUtcOffsetLabel(720)).toBe('GMT+12');
expect(formatUtcOffsetLabel(-720)).toBe('GMT-12');
});
test('renders half- and quarter-hour offsets', () => {
expect(formatUtcOffsetLabel(330)).toBe('GMT+5:30');
expect(formatUtcOffsetLabel(-570)).toBe('GMT-9:30');
expect(formatUtcOffsetLabel(345)).toBe('GMT+5:45');
});
test('UTC itself carries no sign', () => {
expect(formatUtcOffsetLabel(0)).toBe('GMT');
expect(formatUtcOffsetLabel(-0)).toBe('GMT');
});
test('a non-finite offset degrades to a bare GMT rather than "GMT+NaN"', () => {
expect(formatUtcOffsetLabel(Number.NaN)).toBe('GMT');
});
});
describe('resolveTimeZoneLabel', () => {
test('produces a well-formed label whatever timezone the runner is in', () => {
expect(resolveTimeZoneLabel()).toMatch(/^GMT([+-]\d{1,2}(:\d{2})?)?$/);
});
test('reflects the offset of the instant it is given, not of "now"', () => {
// A DST-observing zone answers differently in January than in July, so the
// label has to follow the date the timestamps are being rendered for.
const january = new Date(2026, 0, 15, 12);
const july = new Date(2026, 6, 15, 12);
expect(resolveTimeZoneLabel(january)).toBe(
formatUtcOffsetLabel(-january.getTimezoneOffset())
);
expect(resolveTimeZoneLabel(july)).toBe(formatUtcOffsetLabel(-july.getTimezoneOffset()));
});
});
describe('Asia/Shanghai hardcode', () => {
test('is gone from the reset-credit module', () => {
expect('formatShanghaiDateTime' in resetCredits).toBe(false);
expect(resetCredits.normalizeCodexResetCreditsPayload).toBeDefined();
});
test('the expiry heading interpolates a timezone in all four locales', async () => {
for (const locale of ['en', 'zh-CN', 'zh-TW', 'ru']) {
await i18n.changeLanguage(locale);
const label = i18n.t('codex_quota.reset_credits_expiry_label', { timezone: 'GMT+8' });
expect(label).toContain('GMT+8');
expect(label).not.toContain('{{');
}
await i18n.changeLanguage('en');
});
});