edgegoat Guides

How to test daylight saving time in Jest

Twice a year a local day is 23 or 25 hours long, a wall clock time is skipped or repeated, and code that treats a day as 86,400,000 milliseconds drifts by an hour.

The bug: one day is not always 24 hours

On 2026-03-08 in New York, clocks jump from 02:00 to 03:00. That local day lasts 23 hours. On 2026-11-01 they fall back from 02:00 to 01:00, so 01:30 happens twice and the day lasts 25 hours. Reminders, recurring events, "same time tomorrow" and date pickers all break here.

Some zones are stranger: Australia/Lord_Howe shifts by 30 minutes, and America/Sao_Paulo used to change at midnight, so on 2018-11-04 local midnight did not exist and new Date(2018, 10, 4) returns 01:00.

A naive implementation that fails

example.js
// Same time tomorrow, or so it seems.
function addDays(date, days) {
  return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
}

The edgegoat test that catches it

dates({ tags: ['dst'] }) returns the last instant before each spring forward, the first instant after it, and both occurrences of the repeated time at each fall back, for every zone. Each case carries its zone:

example.test.js
const { dates, withTimeZone } = require('edgegoat');

describe('addDays keeps the wall clock time', () => {
  test.each(dates({ tags: ['dst'] }))('$label', ({ value, zone }) => {
    withTimeZone(zone, () => {
      const next = addDays(value, 1);
      expect([next.getHours(), next.getMinutes()]).toEqual([value.getHours(), value.getMinutes()]);
    });
  });
});
Fails: every case whose next 24 hours cross a transition, for example "America/New_York 2026: 1 ms before spring forward" (01:59 becomes 02:59) and "fall back, first 01:30" (01:30 becomes 00:30).

The fix is to move the calendar date and let the engine resolve the wall clock time:

example.js
function addDays(date, days) {
  const d = new Date(date);
  d.setDate(d.getDate() + days);
  return d;
}

Set the time zone for the whole Jest run

Terminal
TZ=America/New_York npx jest

Or set it once in a global setup file so every worker inherits it:

example.js
// jest.config.js
module.exports = { globalSetup: './jest.global-setup.js' };

// jest.global-setup.js
module.exports = async () => {
  process.env.TZ = 'America/New_York';
};

Mock the current date during a DST change

To test code that reads "now", freeze the clock at a DST instant. withFrozenTime needs no fake timers, and it nests inside withTimeZone:

example.test.js
const { dates, withFrozenTime, withTimeZone } = require('edgegoat');

for (const { label, value, zone } of dates({ tags: ['overlap'] })) {
  test(label, () => {
    withTimeZone(zone, () =>
      withFrozenTime(value, () => {
        expect(minutesUntilMidnight()).toBeGreaterThan(0);
      }),
    );
  });
}

With Jest's own fake timers the equivalent is jest.useFakeTimers({ now: value }).

FAQ

Use real transition instants (dates({ tags: ['dst'] })), run each one in its zone with withTimeZone() or by starting Jest with TZ, and assert on local wall clock values, not on millisecond differences.

Either jest.useFakeTimers({ now: date }), or withFrozenTime(date, fn) from edgegoat, which only replaces Date and leaves timers alone. Combine it with withTimeZone() to control the zone too.

No. edgegoat scans UTC offsets with Intl.DateTimeFormat and binary searches each change down to the minute, so the instants match the tz database of the runtime. Its test suite checks that every case is a real transition.

Australia/Lord_Howe shifts by 30 minutes, Antarctica/Troll by two hours, America/Sao_Paulo switched at midnight until 2019, and the tz database models Europe/Dublin with a negative DST save.

More guides