How to Test Dates, Timezones, and Scheduling
Short answer
Date and timezone bugs shift appointments an hour, bill on wrong days, and pass CI in UTC while failing for Sydney users. Use Playwright clock.install() for deterministic instants, test DST spring/fall boundaries explicitly, and probe stored UTC values—not localized display strings alone.
Part of Testing Guides by UI patterns.
Who this is for
Teams shipping scheduling, billing cycles, SLA timers, subscription renewals, or global dashboards where wall-clock time matters. Typical stacks: date-fns/Luxon/Day.js, browser Intl, backend UTC storage with locale display.
Why testing dates and timezones matters
Time bugs are silent until calendar edges:
- DST transitions — 2 AM spring forward removes an hour; fall back creates duplicate local times; appointments double-book or vanish.
- Billing errors — invoice generated a day early for APAC users; churn disputes spike.
- Compliance — retention "delete after 30 days" fires at wrong instant; audit timestamps wrong timezone in exports.
- CI false confidence — GitHub Actions runs UTC; prod users in
America/Chicagohit untested paths. - Locale display —
MM/DDvsDD/MMmisread in forms; wrong date stored.
Freeze time with Playwright clock and assert probe UTC ISO strings plus selected locale display.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| DST spring forward | Missing hour | Slot picker skips | clock at Mar 10 2024 US |
| DST fall back | Duplicate 1:30 AM | Wrong slot stored | Two bookings probe |
| UTC storage | Local display only | Off-by-one day | Probe 2024-03-15T00:00:00Z |
| User TZ preference | Account vs browser | Mismatch | Seed user TZ; set context TZ |
| All-day events | Midnight boundary | Spans two UTC days | Probe date-only field |
| Relative labels | "Tomorrow" | Changes at midnight | clock.tick(24h) |
| Billing anchor | Month-end Feb 29 | Skipped renewal | clock at leap year |
| Scheduler cron | Job at 00:00 UTC | Never runs in test | Trigger job route |
| Date picker widget | Portal in overlay | Click miss | getByRole gridcell |
| ISO input | type vs picker | Partial date | keyboard fill + blur |
| Range pickers | End before start | Validation gap | Cross-field probe |
| Server/client skew | NTP drift | Token expiry flake | clock on both sides |
Playwright clock setup
import { test, expect } from '@playwright/test';
test('books slot across DST spring forward', async ({ page, context }) => {
await context.clock.install({ time: new Date('2024-03-09T10:00:00-06:00') }); // America/Chicago
await page.emulateMedia({ reducedMotion: 'reduce' });
// Seed calendar with slots in America/Chicago
await page.goto('/schedule');
await page.getByRole('button', { name: 'Mar 10, 2:30 AM' }).click(); // may not exist — negative
await page.getByRole('button', { name: 'Mar 10, 3:30 AM' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
const booking = await request.get(`/api/test/booking?runId=${runId}`).then(r => r.json());
expect(booking.startsAtUtc).toMatch(/2024-03-10T08:30:00/);
});
Advance time for relative UI:
await context.clock.runFor(86_400_000); // +24h
await expect(page.getByText('Due today')).toBeHidden();
Timezone matrix (prioritize by TrueCoverage)
| Dimension | Example values |
|---|---|
timezone | America/New_York, Europe/London, Australia/Sydney |
locale | en-US, en-GB, de-DE |
Compare prod user timezone distribution to test runs. If 25% of prod sessions are Australia/Sydney but tests only use UTC, run /testchimp evolve for Sydney DST scenarios—link SmartTests with // @Scenario: (requirement traceability).
Date picker interaction
await page.getByLabel('Start date').click();
await page.getByRole('gridcell', { name: '15' }).click();
await page.getByLabel('Start date').blur();
const probe = await request.post('/api/test/parse-date-field', {
data: { runId, field: 'startDate', display: '03/15/2024' },
});
expect((await probe.json()).utc).toBe('2024-03-15T00:00:00.000Z');
Prefer probe parsing over visible text when formats vary by locale.
CI checklist
clock.installbefore navigation for time-sensitive specs- At least one DST boundary spec per supported region
- Probe UTC fields on create/update
- Set
timezoneIdin Playwright config project for locale-critical apps - Trigger cron via test route instead of waiting real hours
- Pair with calendar scheduling guide for slot availability
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
new Date() in asserts | Nondeterministic | clock.install |
| Assert localized string only | Locale drift | Probe UTC |
| Skip DST | Hour-shift prod bugs | Boundary fixtures |
Real setTimeout for cron | Slow CI | Test job trigger |
| Single UTC project | Hides TZ bugs | TZ project matrix |
| Hard-code US holidays only | Global gaps | TrueCoverage TZ slices |
Example scenario
Situation: Sydney user books telehealth slot on DST transition day.
Expected outcome: Stored UTC instant matches clinician availability; no duplicate local hour.
Why UI-only automation breaks: UI shows 2:30 AM slot; probe stores wrong UTC—calendar off one hour.
- Arrange: clock at 2024-10-05 Australia/Sydney; seed provider availability.
- Act: Select slot on Oct 6 DST change; confirm booking.
- Assert: Probe startsAtUtc matches expected offset; conflict probe shows single booking.
TestChimp workflow: Instrument booking_complete with timezone; expand DST specs when prod timezone slice lacks test coverage.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Calendar scheduling — slot double-booking
- Localization — date format display
- Healthcare portals — appointment policies
- Subscriptions billing — renewal anchors
External references
Frequently asked questions
How do I test DST transitions in E2E?
Install Playwright clock at instant before DST change in target timezone, select slots across boundary, probe stored UTC. Include spring forward (missing hour) and fall back (ambiguous hour) where applicable.
Should I assert displayed date or server UTC?
Both for display-critical UX, but probe UTC is authoritative for billing and scheduling. Display strings change with locale.
How do I set user timezone in Playwright?
Seed user profile timezone in Arrange, set browser context timezoneId in project config, and install clock for wall-time UI like relative labels.
How do I test cron or scheduled jobs?
Expose test-only trigger route for job runner, advance clock with clock.runFor or jump to next cron instant, probe side effects—never wait real hours in CI.
How do date pickers differ from text inputs?
Pickers use gridcell roles and portals—click open, select cell, blur. Text inputs need locale-specific typing and probe parse endpoint.
Which timezones should I prioritize?
Use TrueCoverage prod timezone distribution—cover top 3 zones plus one DST-heavy region your prod chart shows undertested.
How does TestChimp evolve help timezone coverage?
When TrueCoverage shows booking spikes in untested zones, evolve adds SmartTests with clock fixtures and // @Scenario: links in compliance plans.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.