Clock API (Đóng băng và điều khiển thời gian trong trang)
automationplaywright
page.clock (Playwright 1.45+) cho phép thay thế Date, setTimeout, setInterval, requestAnimationFrame của trang bằng fake clock điều khiển từ test — đóng băng thời điểm, nhảy qua 30 giây/5 phút tức thì, chạy hết timer giữa chừng. Giải quyết triệt để test logic phụ thuộc thời gian (OTP hết hạn, countdown flash sale, '5 phút trước') vốn flake hoặc phải chờ thật.
Khái niệm & Bản chất
- Fake clock bên trong browser:
page.clock.install()thay các time API của trang (Date.now,new Date,setTimeout,setInterval,clearTimeout,requestAnimationFrame,performance.now) bằng bản điều khiển được — code trang chạy trên 'giờ giả'. - Giải quyết flake kinh điển: assert 'đơn tạo 5 phút trước', OTP hết hạn sau 30s, banner 'Flash sale kết thúc lúc 10:00' — trước đây phải sleep thật (chậm) hoặc seed data với timestamp (phức tạp).
- Chỉ ảnh hưởng code TRONG trang: timeout của chính Playwright (auto-wait,
expect,waitForSelector) vẫn dùng real time → test không bao giờ kẹt vì fake clock. - Khác fake timers Node (Sinon/Jest):
page.clockchạy trong browser context, điều khiển code trang web — không liên quan timer của process test.
Cách hoạt động & API chính
page.clock.install({ time? }): cài fake clock — PHẢI gọi trước khi trang load/script đọcDate(thường ngay saunewPage, trướcgoto). Không truyềntime→ bắt đầu từ giờ hiện tại.page.clock.setFixedTime(time): đóng băng hoàn toàn —Date.nowluôn trả đúng mốc đó;setSystemTime(time)thì set mốc rồi clock vẫn chạy tiếp.page.clock.runFor(ms | 'mm:ss'): nhảy qua khoảng thời gian, FIRE tuần tự mọi timer đã lên lịch trong khoảng đó — countdown, debounce, polling đều chạy.page.clock.fastForward(ms | 'mm:ss'): nhảy tới tương lai nhanh, fire các timer dọc đường — dùng cho 'đợi' 30s hết hạn mà không sleep.page.clock.pauseAt(time)+resume(): chạy tới 1 mốc rồi DỪNG hẳn (timer không tự fire nữa) —resume()mở lại;tick(ms)advance thủ công khi đang pause.- Time chấp nhận:
Date, epoch ms, hoặc string'2025-01-01T08:00:00'/'05:00'(mm:ss cho duration).
Ví dụ & Cú pháp thực tế
- OTP hết hạn — nhảy 31 giây tức thì:
``
ts test('OTP hết hạn sau 30s', async ({ page }) => { await page.clock.install(); // PHẢI trước goto await page.goto('/verify-otp'); await page.getByLabel('Mã OTP').fill('123456'); await page.clock.fastForward(31_000); // nhảy 31s, không sleep thật await expect(page.getByText('Mã đã hết hạn')).toBeVisible(); });`` - Đóng băng 'hôm nay' cho UI deterministic:
``
ts await page.clock.install({ time: new Date('2025-01-15T08:00:00') }); await page.clock.pauseAt(new Date('2025-01-15T10:00:00')); await page.goto('/dashboard'); // mọi new Date() trong trang = 10:00 ngày 15/01/2025 — screenshot ổn định`` - Countdown flash sale — chạy hết timer trong 5 phút:
``
ts await page.clock.install(); await page.goto('/flash-sale'); await expect(page.getByTestId('countdown')).toHaveText('05:00'); await page.clock.runFor('05:00'); // fire tuần tự tất cả interval await expect(page.getByTestId('countdown')).toHaveText('00:00'); await expect(page.getByText('Đã kết thúc')).toBeVisible();`` - Kiểm 'gửi lại OTP sau 60s':
``
ts await page.clock.install(); await page.goto('/otp'); const resend = page.getByRole('button', { name: 'Gửi lại' }); await expect(resend).toBeDisabled(); await page.clock.runFor(60_000); await expect(resend).toBeEnabled();``
Best practices & Khi nào dùng
- Dùng khi logic phụ thuộc thời gian: countdown, token/OTP expiry, 'x phút trước', lịch hẹn, debounce input, polling interval, cache TTL phía client.
installtrướcgoto— luôn luôn: app cache bindingDatelúc khởi động; install sau khi trang chạy có thể không apply cho script đã nạp.pauseAtcho screenshot/snapshot: đóng băng mọi timer + rAF → trang 'đứng yên', visual test hết flake vì clock/animation.runForkhi cần timer GIỮA chừng chạy (countdown từng giây, polling mỗi 5s);fastForwardkhi chỉ quan tâm trạng thái cuối.- Kết hợp route mock cho server time: response API chứa timestamp server vẫn là giờ thật — mock bằng
page.routenếu UI hiển thị giờ server.
Gotchas, So sánh & Lưu ý
- Server-side time không fake được: JWT exp server ký,
created_attừ DB, signed URL — vẫn giờ thật; clock chỉ fake phía browser. Test hết hạn phải mock server response. - Phân biệt
setFixedTimevssetSystemTime: fixed = đóng băng vĩnh viễn; system = set mốc hiện tại rồi clock chảy tiếp bình thường. requestAnimationFramecũng bị điều khiển: animation dựa trên rAF 'đứng' khi pause — cái này vừa là feature (stable screenshot) vừa là bẫy (trang chờ rAF callback không bao giờ chạy).- Cookie
expires/max-agevà storage expiry không theo fake clock ở tầng network — đừng rely clock để test cookie hết hạn. - WebSocket/SSE heartbeat có thể phụ thuộc real time: fake clock đôi khi làm connection treo — nếu test dùng realtime, cân nhắc chỉ install clock sau khi channel đã mở, hoặc mock.
- So với cách cũ (
page.addInitScriptghi đèDatethủ công): Clock API đầy đủ hơn — bao cả timers, rAF, performance.now — nên dùng API chính thức thay vì tự patch.
