What the clock API controls
Playwright's page.clock replaces the browser's time functions: Date, Date.now(), setTimeout, setInterval, requestAnimationFrame. When you control the clock, timers don't fire in real time — they fire when you tell them to. This makes time-dependent tests instant.
Three modes: setFixedTime (simplest — freeze Date.now to a specific timestamp), install with fastForward or pauseAt (for more complex scenarios), and runFor (manually tick milliseconds).
setFixedTime — freeze the date
The simplest case: you need Date.now() to return a specific value so that dates display consistently in tests. If your order list shows "Created 2 days ago" and the text depends on the current date — tests become flaky because "2 days ago" changes every day. Fix: freeze the clock before loading the page.
test('order shows correct relative date', async ({ page }) => {
// Фіксуємо час ПЕРЕД завантаженням сторінки
await page.clock.setFixedTime(new Date('2026-05-14T12:00:00'))
await page.goto('/orders')
// Замовлення створене 2026-05-12 завжди показуватиме "2 days ago"
await expect(page.getByTestId('order-date').first()).toContainText('2 days ago')
})
test('session expiry shows correct countdown', async ({ page }) => {
await page.clock.setFixedTime(new Date('2026-05-14T09:00:00'))
await page.goto('/dashboard')
// Сесія закінчується о 17:00 — показує "8 hours remaining"
await expect(page.getByTestId('session-expiry')).toContainText('8 hours remaining')
})
fastForward — skip through time
For features like auto-logout, session expiry, or debounced search — where something should happen after a time delay — fastForward fires all pending timers instantly. It's like closing the laptop lid and opening it N minutes later.
test('auto-logout after 30 minutes inactivity', async ({ page }) => {
await page.clock.install()
await page.goto('/dashboard')
// Перевіряємо що залогінені
await expect(page.getByTestId('user-menu')).toBeVisible()
// Перемотуємо 30 хвилин — жодного реального очікування
await page.clock.fastForward('30:00')
// Застосунок має перенаправити на логін
await expect(page).toHaveURL('/login')
await expect(page.getByText('Session expired')).toBeVisible()
})
test('debounced search fires after 500ms', async ({ page }) => {
await page.clock.install()
await page.goto('/orders')
await page.getByRole('searchbox', { name: 'Search orders' }).fill('laptop')
// Без перемотки — запит ще не відправлений (debounce 500ms)
// Перемотуємо 500ms
await page.clock.fastForward(500)
// Тепер запит має відправитися
await expect(page.getByRole('row')).toHaveCount(3)
})
pauseAt — stop at a specific moment
pauseAt lets you install the clock, let the page load naturally (with timers firing), and then freeze time at a specific timestamp. Useful when you need to test a specific point in time — like what happens at exactly midnight, or at a deadline.
test('countdown shows zero at deadline', async ({ page }) => {
// Починаємо задовго до дедлайну
await page.clock.install({ time: new Date('2026-05-14T08:00:00') })
await page.goto('/orders/42')
// Перевіряємо початковий countdown
await expect(page.getByTestId('delivery-countdown')).toContainText('9 hours remaining')
// Переходимо до моменту дедлайну
await page.clock.pauseAt(new Date('2026-05-14T17:00:00'))
await expect(page.getByTestId('delivery-countdown')).toContainText('Deadline passed')
})
runFor — tick manually
runFor ticks time by an exact number of milliseconds, firing each timer that falls within that window in order. Unlike fastForward (which jumps), runFor steps through — useful when you need to verify intermediate states between timer firings.
test('progress bar updates every second', async ({ page }) => {
await page.clock.install({ time: new Date('2026-05-14T10:00:00') })
await page.clock.pauseAt(new Date('2026-05-14T10:00:00'))
await page.goto('/import-job/123')
await expect(page.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0')
// Tick 1 секунду — перший апдейт прогресу
await page.clock.runFor(1000)
await expect(page.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '10')
// Ще 4 секунди
await page.clock.runFor(4000)
await expect(page.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '50')
})