.agents/skills/playwright-best-practices/advanced/clock-mocking.md
test('mock current time', async ({page}) => {
// Install clock before navigating
await page.clock.install({time: new Date('2025-01-15T09:00:00')})
await page.goto('/dashboard')
// Page sees January 15, 2025 as current date
await expect(page.getByText('January 15, 2025')).toBeVisible()
})
// fixtures/clock.fixture.ts
import {test as base} from '@playwright/test'
type ClockFixtures = {
mockTime: (date: Date | string) => Promise<void>
}
export const test = base.extend<ClockFixtures>({
mockTime: async ({page}, use) => {
await use(async (date) => {
const time = typeof date === 'string' ? new Date(date) : date
await page.clock.install({time})
})
},
})
// Usage
test('subscription expiry', async ({page, mockTime}) => {
await mockTime('2025-12-31T23:59:00')
await page.goto('/subscription')
await expect(page.getByText('Expires today')).toBeVisible()
})
test('show holiday banner in December', async ({page}) => {
await page.clock.install({time: new Date('2025-12-20T10:00:00')})
await page.goto('/')
await expect(page.getByRole('banner', {name: /holiday/i})).toBeVisible()
})
test('no holiday banner in January', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15T10:00:00')})
await page.goto('/')
await expect(page.getByRole('banner', {name: /holiday/i})).toBeHidden()
})
test('shows relative time correctly', async ({page}) => {
// Fix time to control "posted 2 hours ago" text
await page.clock.install({time: new Date('2025-06-15T14:00:00')})
// Mock API to return post with known timestamp
await page.route('**/api/posts/1', (route) =>
route.fulfill({
json: {
id: 1,
title: 'Test Post',
createdAt: '2025-06-15T12:00:00Z', // 2 hours before mock time
},
}),
)
await page.goto('/posts/1')
await expect(page.getByText('2 hours ago')).toBeVisible()
})
test.describe('end of month billing', () => {
test('shows billing on last day of month', async ({page}) => {
await page.clock.install({time: new Date('2025-01-31T10:00:00')})
await page.goto('/billing')
await expect(page.getByText('Payment due today')).toBeVisible()
})
test('shows days remaining mid-month', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15T10:00:00')})
await page.goto('/billing')
await expect(page.getByText('16 days until payment')).toBeVisible()
})
})
test('session timeout warning', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15T09:00:00')})
await page.goto('/dashboard')
// Advance 25 minutes (session timeout at 30 min)
await page.clock.fastForward('25:00')
await expect(page.getByText('Session expires in 5 minutes')).toBeVisible()
// Advance 5 more minutes
await page.clock.fastForward('05:00')
await expect(page.getByText('Session expired')).toBeVisible()
})
test('countdown timer', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15T09:00:00')})
await page.goto('/sale')
// Initial state
await expect(page.getByText('Sale ends in 2:00:00')).toBeVisible()
// Advance 1 hour
await page.clock.fastForward('01:00:00')
await expect(page.getByText('Sale ends in 1:00:00')).toBeVisible()
// Advance past end
await page.clock.fastForward('01:00:01')
await expect(page.getByText('Sale ended')).toBeVisible()
})
test('debounced search', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15T09:00:00')})
await page.goto('/search')
await page.getByLabel('Search').fill('playwright')
// Search is debounced by 300ms, won't fire yet
await expect(page.getByTestId('search-results')).toBeHidden()
// Fast forward past debounce
await page.clock.fastForward(300)
// Now search should execute
await expect(page.getByTestId('search-results')).toBeVisible()
})
test.describe('timezone display', () => {
test('shows correct time in PST', async ({browser}) => {
const context = await browser.newContext({
timezoneId: 'America/Los_Angeles',
})
const page = await context.newPage()
await page.clock.install({time: new Date('2025-01-15T17:00:00Z')}) // 5 PM UTC
await page.goto('/schedule')
// Should show 9 AM PST
await expect(page.getByText('9:00 AM')).toBeVisible()
await context.close()
})
test('shows correct time in JST', async ({browser}) => {
const context = await browser.newContext({
timezoneId: 'Asia/Tokyo',
})
const page = await context.newPage()
await page.clock.install({time: new Date('2025-01-15T17:00:00Z')}) // 5 PM UTC
await page.goto('/schedule')
// Should show 2 AM next day JST
await expect(page.getByText('2:00 AM')).toBeVisible()
await context.close()
})
})
// fixtures/timezone.fixture.ts
import {test as base} from '@playwright/test'
type TimezoneFixtures = {
pageInTimezone: (timezone: string) => Promise<Page>
}
export const test = base.extend<TimezoneFixtures>({
pageInTimezone: async ({browser}, use) => {
const pages: Page[] = []
await use(async (timezone) => {
const context = await browser.newContext({timezoneId: timezone})
const page = await context.newPage()
pages.push(page)
return page
})
// Cleanup
for (const page of pages) {
await page.context().close()
}
},
})
test('auto-refresh data', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15T09:00:00')})
let apiCalls = 0
await page.route('**/api/data', (route) => {
apiCalls++
route.fulfill({json: {value: apiCalls}})
})
await page.goto('/live-data') // Sets up 30s refresh interval
expect(apiCalls).toBe(1) // Initial load
// Advance 30 seconds
await page.clock.fastForward('00:30')
expect(apiCalls).toBe(2) // First refresh
// Advance another 30 seconds
await page.clock.fastForward('00:30')
expect(apiCalls).toBe(3) // Second refresh
})
test('notification queue', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15T09:00:00')})
await page.goto('/notifications')
// Trigger 3 notifications that show sequentially
await page.getByRole('button', {name: 'Show All'}).click()
// First notification appears immediately
await expect(page.getByText('Notification 1')).toBeVisible()
// Second appears after 2 seconds
await page.clock.fastForward('00:02')
await expect(page.getByText('Notification 2')).toBeVisible()
// Third appears after 2 more seconds
await page.clock.fastForward('00:02')
await expect(page.getByText('Notification 3')).toBeVisible()
})
test('animation completes', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15T09:00:00')})
await page.goto('/animation-demo')
await page.getByRole('button', {name: 'Animate'}).click()
// Animation runs for 500ms
const element = page.getByTestId('animated-box')
await expect(element).toHaveCSS('opacity', '0')
// Fast forward through animation
await page.clock.fastForward(500)
await expect(element).toHaveCSS('opacity', '1')
})
// Good
test('date test', async ({page}) => {
await page.clock.install({time: new Date('2025-01-15')})
await page.goto('/') // Page loads with mocked time
})
// Bad - time already captured by page
test('date test', async ({page}) => {
await page.goto('/')
await page.clock.install({time: new Date('2025-01-15')}) // Too late!
})
// Good - explicit timezone
await page.clock.install({time: new Date('2025-01-15T09:00:00Z')})
// Ambiguous - uses local timezone
await page.clock.install({time: new Date('2025-01-15T09:00:00')})
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Installing clock after navigation | Page already captured real time | Install clock before goto() |
| Hardcoded relative dates | Tests break over time | Use fixed dates with clock mock |
| Not accounting for timezone | Tests fail in different regions | Use explicit UTC times or set timezone |
Using waitForTimeout with mocked clock | Conflicts with mocked timers | Use fastForward instead |