# 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