时钟模拟:Clock 冻结与快进时间
本教程共 59 篇 · 第 29 篇 · 更新于 2026-08-04 · 约 11 分钟阅读
29. 时钟模拟:Clock 冻结与快进时间
本节目标:能用
page.clock把页面里的时间「冻住」或「快进」,从而快速测出倒计时、超时、定时任务这些行为,而不必真实等待几分钟几小时。
有些功能和时间死死绑在一起。比如登录后五分钟不动就自动登出,比如一个倒计时显示「还剩 30 秒」,比如页面每隔一秒刷新一次时间。
如果老老实实等真实时间,测一个自动登出就得干坐五分钟。而且真实时间有抖动,今天跑过明天未必过。Clock(时钟)就是专门解决这件事的:它让你在测试里手动控制时间,想让它几点就几点,想快进就快进。
Clock 到底接管了什么
调用 page.clock 的相关方法后,Playwright 会把页面里一批和时间有关的原生函数换成自己实现的控制版本。被接管的包括:
Date(所以new Date()、Date.now()都听 Clock 的)setTimeout/clearTimeoutsetInterval/clearIntervalrequestAnimationFrame/cancelAnimationFramerequestIdleCallback/cancelIdleCallbackperformance(性能计时也归 Clock 管)Event.timeStamp(事件时间戳同样受控)
一句话:凡是页面里「靠时间驱动」的东西,一旦装了 Clock,都由你说了算。
补充一点:Clock 是装在 BrowserContext(浏览器上下文)上的,同一 context 下的所有页面、iframe 时间都归它管。所以你在某个页面上拨快了时间,同 context 里的其他页面也会一起变。
三种武器,先选对
Playwright 给你三档能力,由浅到深:
setFixedTime:只把Date.now()钉成一个固定值,计时器照常走。最常用、最省心。install:完全接管时间,之后你能暂停、快进、手动 tick(步进)。功能最强。setSystemTime:把系统时间拨到某一刻,但不触发任何计时器。官方只推荐给进阶场景。
记住一个总原则:能 setFixedTime 解决就别用 install。前者简单不出错,后者控制力强但坑也多。
Tip大多数「页面显示当前时间」的需求,
setFixedTime就够了。只有当你要测「用户离开 5 分钟被登出」这种需要时间往前走很久的逻辑,才上install。
setFixedTime:只钉时间,计时器不停
setFixedTime 把 Date.now() 和 new Date() 钉死成一个固定时刻。注意它不会停掉 setInterval / setTimeout,计时器依旧每秒触发,只是它们读到的时间永远是你钉住的值。
看个直观例子。页面里有段脚本每秒把当前时间渲染到屏幕:
<div data-testid="current-time"></div>
<script>
const renderTime = () => {
document.getElementById('current-time').textContent =
new Date().toLocaleString();
};
setInterval(renderTime, 1000);
</script>
测试里这样钉时间:
import { test, expect } from '@playwright/test';
test('时间被钉住,但每秒仍在刷新', async ({ page }) => {
// 把 Date.now 钉到 10:00:00
await page.clock.setFixedTime(new Date('2024-02-02T10:00:00'));
await page.goto('http://localhost:3333');
await expect(page.getByTestId('current-time'))
.toHaveText('2/2/2024, 10:00:00 AM');
// 再钉到 10:30:00
await page.clock.setFixedTime(new Date('2024-02-02T10:30:00'));
// 页面里的 setInterval 每秒会再渲染一次,读到新时间
await expect(page.getByTestId('current-time'))
.toHaveText('2/2/2024, 10:30:00 AM');
});
这里容易绕晕:为什么「时间固定」了,文本还能从 10:00 变成 10:30?关键在于 setInterval 没被停,它每隔一秒还是触发 renderTime。renderTime 读的是 new Date()——也就是被你钉住的值。你第二次 setFixedTime 把钉的值改成 10:30,下一次计时器触发时自然就渲染出 10:30 了。
所以准确的描述是:时间值是固定的,但计时器的触发节奏是正常的。
install:完全接管时间
setFixedTime 管不了计时器「走了多久」。如果你想让时间真正往前走,比如快进 5 分钟去触发登出,就得用 install。
install 一调用,时间就归你控制,页面里所有计时器都按你给的节奏走。它有几个配套动作:
| 动作 | 作用 |
|---|---|
pauseAt(time) | 让时间走到 time 那一刻后暂停,计时器停住 |
fastForward(duration) | 像合上笔记本又打开,把时间快进一段,期间该触发的计时器一次性补触发 |
runFor(duration) | 手动逐步推进时间,过程中所有计时器按顺序触发 |
resume() | 恢复正常流动,时间继续自然走 |
pauseAt:走到某点就停
test('时间走到 10 点暂停', async ({ page }) => {
// 先用某个起点初始化时钟,页面照常加载
await page.clock.install({ time: new Date('2024-02-02T08:00:00') });
await page.goto('http://localhost:3333');
// 让时间走到 10:00:00 然后暂停
await page.clock.pauseAt(new Date('2024-02-02T10:00:00'));
await expect(page.getByTestId('current-time'))
.toHaveText('2/2/2024, 10:00:00 AM');
});
暂停后,页面里的 Date.now 卡在 10:00:00,计时器也停了。你这时可以安心断言页面状态。
fastForward:快进一段
接着上面的场景,想模拟「又过了 30 分钟」:
test('从 10 点快进 30 分钟', async ({ page }) => {
await page.clock.install({ time: new Date('2024-02-02T08:00:00') });
await page.goto('http://localhost:3333');
await page.clock.pauseAt(new Date('2024-02-02T10:00:00'));
// 快进 30 分钟,相当于合上笔记本半小时再打开
await page.clock.fastForward('30:00');
await expect(page.getByTestId('current-time'))
.toHaveText('2/2/2024, 10:30:00 AM');
});
fastForward 的语义很妙:它不像你手动等 30 分钟,而是瞬间把时间推到目标点,并让途中到期的所有计时器立刻补触发——和真实浏览器里「合上盖子再打开」的表现一致。
runFor:手动一步一步走
runFor 和 fastForward 类似,但更强调「逐格推进」。比如想精确控制时间前进 2 秒、看两次渲染:
test('手动推进 2 秒', async ({ page }) => {
await page.clock.install({ time: new Date('2024-02-02T08:00:00') });
await page.goto('http://localhost:3333');
await page.clock.pauseAt(new Date('2024-02-02T10:00:00'));
// 手动走 2000 毫秒,期间 setInterval 会触发两次
await page.clock.runFor(2000);
await expect(page.getByTestId('current-time'))
.toHaveText('2/2/2024, 10:00:02 AM');
});
runFor(2000) 让时间前进 2 秒,那个每秒渲染一次的计时器就会触发两次,屏幕时间从 10:00:00 走到 10:00:02。
resume:恢复正常流动
如果暂停后想让时间重新自然走,用 resume():
await page.clock.pauseAt(new Date('2024-02-02T10:00:00'));
// ...断言某些状态...
await page.clock.resume(); // 时间继续按真实节奏流动
setSystemTime:直接拨系统时间(进阶)
setSystemTime 把系统时间拨到任意一刻,但一个计时器都不会触发。
它的定位很窄:测「系统时间突然变了」这种场景。比如夏令时切换、用户手动改了时区,看看页面会不会算错。官方明确说它只推荐进阶用例,新手遇到「钉时间」需求优先用 setFixedTime。
await page.clock.install();
await page.clock.setSystemTime(new Date('2024-02-02T10:00:00'));
Note
setSystemTime本身不强制先install。但只要你这个测试里用到了install,它就必须排在所有时钟调用的最前面——原因见下一节。
install 必须在最前面:铁律
这是最容易踩的坑,务必记住。
Warning一旦你在测试里调用了
install,这个调用必须早于任何其它时钟相关的调用。因为install会覆盖计时器函数的原生定义。如果顺序乱了——比如先setInterval、再install、再clearInterval——就会出现未定义行为,测试可能静默失效。
简单记:install 是开门的第一句话。正确顺序永远是:
await page.clock.install({ time: new Date('2024-02-02T08:00:00') }); // 先开门
await page.goto('http://localhost:3333');
await page.clock.pauseAt(new Date('2024-02-02T10:00:00')); // 再操作
而 setFixedTime 不需要 install 打头,它自成一体,单独用即可。
一个典型知识点:测「不活动自动登出」
这类功能靠 setTimeout 倒计时,真实等五分钟太慢。用 Clock 几行就测完:
test('闲置 5 分钟自动登出', async ({ page }) => {
// 初始时间无所谓,用当前时间即可
await page.clock.install();
await page.goto('http://localhost:3333');
// 模拟用户点了一下(重置倒计时)
await page.getByRole('button', { name: 'Interaction' }).click();
// 快进 5 分钟,像用户一直没动
await page.clock.fastForward('05:00');
await expect(page.getByText('You have been logged out due to inactivity.'))
.toBeVisible();
});
页面逻辑是:最后一次交互后 5 分钟无操作就登出。fastForward('05:00') 让那段 setTimeout 立刻到期触发,登出提示出现,断言通过。整段测试瞬间完成。
时区与语言环境去哪设
Clock 只管「时间怎么走」,不管「时间怎么显示」。页面里 toLocaleString() 显示成什么样子,取决于浏览器的时区和语言,那是上一章讲的环境模拟:
- 时区用
context的timezoneId(如'Asia/Shanghai') - 语言用
context的locale(如'zh-CN')
如果你想让 setFixedTime 钉住的时刻在本地面板里显示成「北京时间」,记得在创建 context 时一并配好 timezoneId,详见第 28 章。
NoteClock 在「库模式(Library)」下同样可用,区别只是
page是你手动browser.newContext()后context.newPage()拿到的,而不是测试运行器自动注入的夹具。
小结
- 测时间相关行为,别真实等,用
page.clock接管时间。 - 只想钉住「当前时刻」:
setFixedTime,计时器照走。 - 想让时间能走、能快进:
install后配pauseAt/fastForward/runFor/resume。 install必须是测试里第一个时钟调用,顺序错会静默出错。setSystemTime只拨时间不触发计时器,用来测夏令时、改时区这类突变。- 时区、语言显示归 context 的环境模拟管,Clock 不负责。
下一章开始进网络这块:先学怎么把请求拦下来。