# Clock and Time Mocking > **When to use**: Testing time-dependent features -- countdown timers, scheduled events, expiration dates, age gates, session timeouts, or any UI that behaves differently based on the current time. Playwright's `page.clock` API lets you control time without waiting in real-time. > **Prerequisites**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/configuration.md](configuration.md) ## Quick Reference ```typescript // Freeze time at a specific moment await page.clock.install({ time: new Date('2025-03-15T10:00:00Z') }); await page.goto('/dashboard'); // Advance time by 5 minutes await page.clock.fastForward('05:00'); // Set time to a specific point (jumps, does not tick through) await page.clock.setFixedTime(new Date('2025-12-31T23:59:59Z')); // Let time resume ticking from current mocked point await page.clock.resume(); ``` **Key concept**: `page.clock.install()` replaces `Date`, `setTimeout`, `setInterval`, and `requestAnimationFrame` in the page. Call it before `page.goto()` so the page loads with mocked time from the start. ## Patterns ### Frozen Time with `install()` and `setFixedTime()` **Use when**: Your test needs time to stand still at a specific moment -- verifying what the UI shows at a particular date/time. **Avoid when**: The feature under test depends on timers ticking (use `fastForward` instead). **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('dashboard shows correct greeting based on time of day', async ({ page }) => { // Install clock BEFORE navigating await page.clock.install({ time: new Date('2025-06-15T08:30:00') }); await page.goto('/dashboard'); await expect(page.getByText('Good morning')).toBeVisible(); // Jump to afternoon await page.clock.setFixedTime(new Date('2025-06-15T14:00:00')); await page.reload(); await expect(page.getByText('Good afternoon')).toBeVisible(); // Jump to evening await page.clock.setFixedTime(new Date('2025-06-15T20:00:00')); await page.reload(); await expect(page.getByText('Good evening')).toBeVisible(); }); test('subscription shows correct expiration status', async ({ page }) => { // Freeze time to a date when subscription is active await page.clock.install({ time: new Date('2025-06-01T12:00:00Z') }); await page.goto('/account'); await expect(page.getByTestId('subscription-status')).toHaveText('Active'); await expect(page.getByTestId('days-remaining')).toContainText('29'); // Jump to the expiration date await page.clock.setFixedTime(new Date('2025-06-30T12:00:00Z')); await page.reload(); await expect(page.getByTestId('subscription-status')).toHaveText('Expiring today'); }); test('content displays correctly on a specific holiday', async ({ page }) => { await page.clock.install({ time: new Date('2025-12-25T10:00:00') }); await page.goto('/'); await expect(page.getByText('Happy Holidays')).toBeVisible(); await expect(page.getByTestId('holiday-banner')).toBeVisible(); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('dashboard shows correct greeting based on time of day', async ({ page }) => { await page.clock.install({ time: new Date('2025-06-15T08:30:00') }); await page.goto('/dashboard'); await expect(page.getByText('Good morning')).toBeVisible(); await page.clock.setFixedTime(new Date('2025-06-15T14:00:00')); await page.reload(); await expect(page.getByText('Good afternoon')).toBeVisible(); }); test('subscription shows correct expiration status', async ({ page }) => { await page.clock.install({ time: new Date('2025-06-01T12:00:00Z') }); await page.goto('/account'); await expect(page.getByTestId('subscription-status')).toHaveText('Active'); }); ``` ### Fast-Forwarding Time with `fastForward()` **Use when**: Testing timers, countdowns, debounced actions, or any feature that reacts to elapsed time. `fastForward` fires all pending timers up to the specified duration. **Avoid when**: You just need to check a static time-dependent display -- use `setFixedTime` instead. **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('countdown timer reaches zero', async ({ page }) => { await page.clock.install({ time: new Date('2025-03-15T10:00:00Z') }); await page.goto('/sale'); // Sale countdown starts at 2 hours await expect(page.getByTestId('countdown')).toContainText('2:00:00'); // Fast-forward 1 hour await page.clock.fastForward('01:00:00'); await expect(page.getByTestId('countdown')).toContainText('1:00:00'); // Fast-forward remaining time await page.clock.fastForward('01:00:00'); await expect(page.getByTestId('countdown')).toContainText('0:00:00'); await expect(page.getByText('Sale ended')).toBeVisible(); }); test('auto-save triggers after 30 seconds of inactivity', async ({ page }) => { await page.clock.install({ time: new Date('2025-03-15T10:00:00Z') }); await page.goto('/editor'); // Type something await page.getByRole('textbox', { name: 'Content' }).fill('Draft content'); // Fast-forward past the auto-save interval await page.clock.fastForward('00:30'); await expect(page.getByText('Saved')).toBeVisible(); }); test('session timeout warning appears after 25 minutes', async ({ page }) => { await page.clock.install({ time: new Date('2025-03-15T10:00:00Z') }); await page.goto('/dashboard'); // Fast-forward to just before the warning (25 min) await page.clock.fastForward('24:59'); await expect(page.getByRole('dialog', { name: 'Session timeout' })).not.toBeVisible(); // One more minute triggers the warning await page.clock.fastForward('00:01'); await expect(page.getByRole('dialog', { name: 'Session timeout' })).toBeVisible(); await expect(page.getByText('Your session will expire in 5 minutes')).toBeVisible(); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('countdown timer reaches zero', async ({ page }) => { await page.clock.install({ time: new Date('2025-03-15T10:00:00Z') }); await page.goto('/sale'); await expect(page.getByTestId('countdown')).toContainText('2:00:00'); await page.clock.fastForward('01:00:00'); await expect(page.getByTestId('countdown')).toContainText('1:00:00'); await page.clock.fastForward('01:00:00'); await expect(page.getByText('Sale ended')).toBeVisible(); }); test('auto-save triggers after inactivity', async ({ page }) => { await page.clock.install({ time: new Date('2025-03-15T10:00:00Z') }); await page.goto('/editor'); await page.getByRole('textbox', { name: 'Content' }).fill('Draft content'); await page.clock.fastForward('00:30'); await expect(page.getByText('Saved')).toBeVisible(); }); ``` ### Resuming Time with `resume()` **Use when**: You need to start with a mocked time, then let time flow normally for interaction-dependent behavior. **Avoid when**: The entire test should use mocked time. **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('notification appears in real-time after scheduled trigger', async ({ page }) => { // Start at a known time await page.clock.install({ time: new Date('2025-03-15T09:59:55Z') }); await page.goto('/dashboard'); // Notification is scheduled for 10:00:00 — advance to 5 seconds before await expect(page.getByTestId('notification-bell')).not.toHaveAttribute('data-count'); // Let real time tick from this point await page.clock.resume(); // The notification should appear within a few seconds await expect(page.getByTestId('notification-bell')).toHaveAttribute('data-count', '1', { timeout: 10000, }); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('notification appears after scheduled trigger', async ({ page }) => { await page.clock.install({ time: new Date('2025-03-15T09:59:55Z') }); await page.goto('/dashboard'); await page.clock.resume(); await expect(page.getByTestId('notification-bell')).toHaveAttribute('data-count', '1', { timeout: 10000, }); }); ``` ### Testing Date-Dependent UI **Use when**: Features that change based on the current date -- age verification, expiration warnings, seasonal content, date pickers. **Avoid when**: The date is passed from the server and does not depend on client-side `Date`. **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('age gate blocks users under 18', async ({ page }) => { // User born on 2010-01-15 — under 18 as of 2025-06-01 await page.clock.install({ time: new Date('2025-06-01T12:00:00') }); await page.goto('/age-restricted'); await page.getByLabel('Date of birth').fill('2010-01-15'); await page.getByRole('button', { name: 'Verify age' }).click(); await expect(page.getByText('You must be 18 or older')).toBeVisible(); }); test('age gate allows users 18 and older', async ({ page }) => { await page.clock.install({ time: new Date('2025-06-01T12:00:00') }); await page.goto('/age-restricted'); await page.getByLabel('Date of birth').fill('2005-01-15'); await page.getByRole('button', { name: 'Verify age' }).click(); await expect(page.getByText('Welcome')).toBeVisible(); }); test('trial expiration banner shows at correct times', async ({ page }) => { // Day 1 of 14-day trial await page.clock.install({ time: new Date('2025-03-01T12:00:00Z') }); await page.goto('/dashboard'); await expect(page.getByTestId('trial-banner')).toContainText('13 days remaining'); // Day 12 — warning state await page.clock.setFixedTime(new Date('2025-03-12T12:00:00Z')); await page.reload(); await expect(page.getByTestId('trial-banner')).toContainText('2 days remaining'); await expect(page.getByTestId('trial-banner')).toHaveCSS('background-color', /rgb\(255/); // Red/warning // Day 15 — expired await page.clock.setFixedTime(new Date('2025-03-15T12:00:00Z')); await page.reload(); await expect(page.getByText('Your trial has expired')).toBeVisible(); await expect(page.getByRole('button', { name: 'Upgrade now' })).toBeVisible(); }); test('date picker defaults to current mocked date', async ({ page }) => { await page.clock.install({ time: new Date('2025-07-04T12:00:00') }); await page.goto('/booking'); await page.getByLabel('Check-in date').click(); // Calendar should open to July 2025 await expect(page.getByText('July 2025')).toBeVisible(); // Today (July 4) should be highlighted const today = page.locator('[aria-current="date"]'); await expect(today).toHaveText('4'); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('age gate blocks users under 18', async ({ page }) => { await page.clock.install({ time: new Date('2025-06-01T12:00:00') }); await page.goto('/age-restricted'); await page.getByLabel('Date of birth').fill('2010-01-15'); await page.getByRole('button', { name: 'Verify age' }).click(); await expect(page.getByText('You must be 18 or older')).toBeVisible(); }); test('trial expiration shows correct days remaining', async ({ page }) => { await page.clock.install({ time: new Date('2025-03-01T12:00:00Z') }); await page.goto('/dashboard'); await expect(page.getByTestId('trial-banner')).toContainText('13 days remaining'); await page.clock.setFixedTime(new Date('2025-03-15T12:00:00Z')); await page.reload(); await expect(page.getByText('Your trial has expired')).toBeVisible(); }); ``` ### Timezone-Dependent Features **Use when**: Testing features that combine mocked time with specific timezones. **Avoid when**: The feature uses only UTC and does not render local times. **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('business hours banner shows open/closed status', async ({ browser }) => { // Business hours: 9 AM - 5 PM Eastern const context = await browser.newContext({ timezoneId: 'America/New_York' }); const page = await context.newPage(); // 10 AM Eastern — should show "Open" await page.clock.install({ time: new Date('2025-03-15T14:00:00Z') }); // 10 AM ET await page.goto('/contact'); await expect(page.getByTestId('business-hours')).toContainText('Open'); // 6 PM Eastern — should show "Closed" await page.clock.setFixedTime(new Date('2025-03-15T22:00:00Z')); // 6 PM ET await page.reload(); await expect(page.getByTestId('business-hours')).toContainText('Closed'); await context.close(); }); test('scheduled event shows correct local time', async ({ browser }) => { // Event at 2025-03-20T18:00:00Z // User in Tokyo (UTC+9) const tokyoCtx = await browser.newContext({ timezoneId: 'Asia/Tokyo' }); const tokyoPage = await tokyoCtx.newPage(); await tokyoPage.clock.install({ time: new Date('2025-03-20T10:00:00Z') }); await tokyoPage.goto('/events/upcoming'); // 18:00 UTC = 03:00 AM next day in Tokyo (UTC+9) await expect(tokyoPage.getByTestId('event-time')).toContainText('3:00 AM'); await tokyoCtx.close(); // User in London (UTC+0 in March, before DST) const londonCtx = await browser.newContext({ timezoneId: 'Europe/London' }); const londonPage = await londonCtx.newPage(); await londonPage.clock.install({ time: new Date('2025-03-20T10:00:00Z') }); await londonPage.goto('/events/upcoming'); await expect(londonPage.getByTestId('event-time')).toContainText('6:00 PM'); await londonCtx.close(); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('business hours banner shows open/closed status', async ({ browser }) => { const context = await browser.newContext({ timezoneId: 'America/New_York' }); const page = await context.newPage(); await page.clock.install({ time: new Date('2025-03-15T14:00:00Z') }); await page.goto('/contact'); await expect(page.getByTestId('business-hours')).toContainText('Open'); await page.clock.setFixedTime(new Date('2025-03-15T22:00:00Z')); await page.reload(); await expect(page.getByTestId('business-hours')).toContainText('Closed'); await context.close(); }); ``` ## Decision Guide | Scenario | API | Why | |---|---|---| | Check UI at a specific date/time | `clock.install()` + `clock.setFixedTime()` | Time is frozen; no timer ticking | | Test countdown or timer behavior | `clock.install()` + `clock.fastForward()` | Fires timers as time advances without real waiting | | Test after a long idle period | `clock.install()` + `clock.fastForward('30:00')` | Simulates 30 minutes without waiting 30 minutes | | Start mocked, then tick normally | `clock.install()` + `clock.resume()` | Useful when you need real `requestAnimationFrame` after setup | | Different timezone display | `browser.newContext({ timezoneId })` | Affects `Date` timezone rendering | | Timezone + mocked time | `newContext({ timezoneId })` + `clock.install()` | Both timezone and absolute time are controlled | | Test date picker defaults | `clock.install()` with target date | Calendar opens to the mocked "today" | | Test DST transitions | Set `timezoneId` + `install` at DST boundary | Tests the most common timezone bugs | ## Anti-Patterns | Don't Do This | Problem | Do This Instead | |---|---|---| | Calling `clock.install()` after `page.goto()` | Page already loaded with real `Date`; timers already fired | Call `clock.install()` BEFORE `page.goto()` | | Using `page.waitForTimeout(30000)` to test a 30-second timer | Wastes 30 real seconds per test run | `page.clock.fastForward('00:30')` completes instantly | | Testing time without mocking the clock | Results depend on when the test runs (morning vs evening, Monday vs Sunday) | Always mock time for time-dependent assertions | | Using `setFixedTime` when you need timers to fire | `setFixedTime` freezes time; `setInterval`/`setTimeout` will not trigger | Use `fastForward` to advance time and fire pending timers | | Mocking only `Date.now()` via `page.evaluate` | Does not affect `setTimeout`, `setInterval`, or `requestAnimationFrame` | Use `page.clock.install()` which mocks all time APIs | | Forgetting timezone when testing dates | Test passes locally but fails in CI (different timezone) | Always set `timezoneId` in context or use UTC dates | | Advancing time in very small increments | Slow test; many unnecessary timer firings | Advance to the exact time of interest in one call | | Not calling `resume()` before real-time-dependent assertions | Mocked timers will not fire naturally; assertions time out | Call `clock.resume()` when you need real time to flow | ## Troubleshooting | Symptom | Likely Cause | Fix | |---|---|---| | `clock.install()` has no effect | Called after `page.goto()`; page already has real `Date` | Move `clock.install()` before navigation | | Timer callbacks never fire | Time is frozen with `setFixedTime`; timers need advancing | Use `fastForward()` to advance past the timer delay | | `fastForward` does not trigger timer | Timer was registered with a delay longer than the fast-forward amount | Fast-forward by at least the timer's delay | | Date is correct but timezone display is wrong | `clock.install` sets time in UTC; `timezoneId` not set on context | Create context with `{ timezoneId: 'Your/Timezone' }` | | Animations break with mocked clock | `requestAnimationFrame` is mocked and does not fire naturally | Call `clock.resume()` before animation-dependent assertions | | Test passes locally but fails in CI | Local timezone differs from CI timezone | Always set `timezoneId` explicitly in the context | | `setFixedTime` throws "clock not installed" | `install()` was not called first | Call `page.clock.install()` before any other clock methods | | Page makes fetch requests with wrong timestamps | Server sees mocked `Date.now()` in request payloads | This is expected; mock server responses if needed | ## Related - [core/i18n-and-localization.md](i18n-and-localization.md) -- timezone and locale testing patterns - [core/assertions-and-waiting.md](assertions-and-waiting.md) -- auto-waiting vs time-based assertions - [core/error-and-edge-cases.md](error-and-edge-cases.md) -- testing timeout and expiration edge cases - [core/performance-testing.md](performance-testing.md) -- timing-related performance measurement - [core/websockets-and-realtime.md](websockets-and-realtime.md) -- real-time features that depend on timing