Playwright Clock API 把时间捏在手里:setFixedTime 与 runFor

2026-08-18

页面上有个”您将在 N 秒后自动登出”的提示,产品要求五分钟无操作就踢下线。这种功能写端到端测试的时候很尴尬:真等五分钟,一条用例就把整个 CI 拖住;不等,又没法验证到底会不会登出。倒计时组件、每秒重绘的时间显示、定时轮询的刷新逻辑,都是同一类麻烦。

Playwright 仓库里 docs/src/clock.md 这一篇专门讲这件事,对应的 API 签名在 docs/src/api/class-clock.md。麻烦的地方在于,它一口气给了四五个方法,名字看着都像”改时间”,语义却差得很远,选错一个用例就会莫名其妙地挂。下面按仓库里写明的内容把这几个入口的分工拆开。

前置条件:从哪个版本、哪个对象拿到它

docs/src/api/class-clock.mdclass: Clock 头部逐字标着 * since: v1.45,文件里 fastForwardinstallrunForpauseAtresumesetFixedTimesetSystemTime 每个方法下面也都标着同一行,也就是这一整套是自 v1.45 起可用的。

入口有两处:docs/src/api/class-page.md 里的 property: Page.clock,和 docs/src/api/class-browsercontext.md 里的 property: BrowserContext.clock,两者类型都是 <[Clock]>。这里有个容易踩的点,class-clock.md 开头写明:clock 是为整个 BrowserContext 安装的,所以同一个 context 下所有页面和 iframe 里的时间由同一个 clock 控制。你在 page.clock 上调,影响面并不止这一个 page。

四种语言绑定都有,但数值参数的单位不一样,这一点必须按自己用的语言去看。class-clock.mdoption: Clock.install.time 被拆成了三份:langs: js, java 那份写 <[long]|[string]|[Date]>langs: python 那份写 <[float]|[string]|[Date]> 并额外注明数值是以秒计的 Unix 时间,langs: csharp 那份则只有 <[string]|[Date]>,没有数值形式。setFixedTimesetSystemTime 的参数说明是同样的拆法,JS/Java 那份写的是毫秒。把 JS 的写法直接抄到 Python 上,数字会差三个数量级。

四个入口各自在做什么

setFixedTime 是最钝也最省事的那个。 class-clock.md 写明它让 Date.nownew Date() 永远返回一个固定的假时间,同时保持所有 timer 继续运行clock.md 的引导语里直接给了建议:推荐的做法是用 setFixedTime 把时间设成一个具体值,如果这满足不了你的场景,再用 install

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');

Python 同步写法在同一个文件里是这样:

page.clock.set_fixed_time(datetime.datetime(2024, 2, 2, 10, 0, 0))
page.goto("http://localhost:3333")
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")

上面两段都是 docs/src/clock.md 里的原文示例,其中的时间值与 3333 端口只是仓库示例里的取值。

install 不是”改时间”,是”换掉时间的实现”。 class-clock.mdinstall 下面列了它会替换的全局函数:DatesetTimeoutclearTimeoutsetIntervalclearIntervalrequestAnimationFramecancelAnimationFramerequestIdleCallbackcancelIdleCallbackperformance。只有先把这套假实现替换上去,才谈得上手动推进时间——pauseAtfastForwardrunForresume 在文档里描述的都是替换到位之后怎么推。class-clock.mdinstall 下面还写明,fake timer 的用途就是在测试里手动控制时间的流动。

fastForward 是”合上笔记本盖子过一会儿再打开”。 class-clock.md 的原话是跳跃式前进,到期的 timer 至多只触发一次。这个”至多一次”在源码里能看到怎么实现的:packages/injected/src/clock.ts_innerFastForwardTo 会先遍历所有 timer,把 callAt 早于目标时刻的统统改写成目标时刻,然后再跑一遍。所以一个每秒一次的 setInterval,快进三十分钟不会补上一千八百次回调,只会在落点那里响一次。

runFor 相反,是逐格走。 class-clock.md 写它推进时钟并触发所有与时间相关的回调。源码里 _runTo 是一个循环,反复调 _callFirstTimer 直到目标时刻之内再没有可取的 timer;而 _takeFirstTimerTimerType.Interval 的处理是把 callAt 往后顺延一个 delay 而不是删除。这就是为什么 clock.md 里那段 runFor(2000) 的示例注释说”屏幕上的时间会被更新 2 次”。

pauseAt 是快进 + 停住。 class-clock.md 写明调用之后 timer 不再触发,除非再调 runForfastForwardpauseAtresume。同一处还给了一条使用建议:最好在导航之前 install,并把初始时间设得比目标测试时间稍早一点,让页面加载期间 timer 正常跑完,页面完全加载后再 pauseAt,以免页面卡住。

setSystemTime 是另一码事。 它设置系统时间但不触发任何 timer,文档给的用途是测页面对时间跳变的反应,比如夏令时切换或换时区。clock.md 里对它的定位是仅推荐用于高级场景。

选择路径其实很短:只想让页面看到某个固定日期,用 setFixedTime;页面的逻辑依赖 Date.now 会往前走、需要跳过一段长等待,用 installfastForwardpauseAt;需要一格一格地把每次回调都跑到,用 runFor;测时间跳变本身,用 setSystemTime

时长字符串的格式,以及会抛什么错

fastForwardrunForparam: ticks 都写着 <[long]|[string]>,说明里给的合法字符串形式是 "08" 表示八秒、"01:00" 表示一分钟、"02:34:10" 表示两小时三十四分十秒。

解析逻辑在 packages/playwright-core/src/server/clock.tsparseTicks 里,校验用的正则是 /^(\d\d:){0,2}\d\d?$/,不匹配时抛的错是 Clock only understands numbers, 'mm:ss' and 'hh:mm:ss';任何一段的数值大于等于 60 会抛 Invalid time。这里可以顺带指出源里两处的落差:文档把 "08" 这种纯秒形式算作合法格式,而错误信息只提了数字、'mm:ss''hh:mm:ss' 三种,措辞比正则实际接受的窄一些——两边都在仓库里白纸黑字写着,遇到报错时知道以正则为准就行。

另外两个报错来自 packages/injected/src/clock.tsrunFor 收到负数会抛 TypeError('Negative ticks are not supported')_innerFastForwardTo 的目标早于当前时刻会抛 Cannot fast-forward to the past。也就是说这套 API 只允许时间往前走。

边界:几处文档明说和几处没说的

调用顺序有硬性要求。 docs/src/clock.md 里有一段警告写得很重:如果你在测试里的任何位置调用了 install,这次调用必须发生在其它所有与 clock 相关的调用之前,顺序错了属于未定义行为。文档举的反例是先 setInterval、再 install、再 clearInterval——因为 install 覆盖了这些函数的原生定义,前后两个句柄不是一回事。

两份”被替换的全局”清单不完全一致。 clock.md 的 note 里除了上面列的那些,还多了 Event.timeStampclass-clock.mdinstall 小节里没有这一项。两处都在仓库里,写用例时若依赖 Event.timeStamp 的取值,值得留意这个差异。

关掉 clock 的公开入口,我们在客户端没有找到。 packages/playwright-core/src/server/clock.tsClock 类上有一个 uninstall(progress),但 packages/playwright-core/src/client/clock.tsClock 类只暴露了 installfastForwardpauseAtresumerunForsetFixedTimesetSystemTime 七个方法,没有对应的 uninstall;class-clock.md 里也没有这个方法的条目。要恢复时间流动,文档提供的是 resume

关于 Web Worker、跨源 iframe 等场景下 clock 的行为,仓库文档里我们没有找到专门说明,这里不做推断。

Windows 侧特别容易被绊的一处:时间对了,字符串还是不对

上面示例里的断言值是 '2/2/2024, 10:00:00 AM',页面代码用的是 new Date().toLocaleString()。Clock 管的是时间点,不管这个时间点被格式化成什么样。

格式化受两个 context 选项影响,docs/src/api/params.md 里写得很直接:timezoneId 改变 context 的时区,默认取系统时区locale 指定用户 locale,会影响 navigator.languageAccept-Language 请求头以及数字与日期的格式化规则,默认取系统默认 locale

把这两条和上面的示例放在一起看,结论是清楚的:在中文 Windows 上跑,系统 locale 与时区通常不是示例假设的那一套,toLocaleString() 的输出形态跟着变,断言就对不上;Linux/macOS 上的 CI 容器同理,取决于镜像里的 TZ 与 locale 设置。想让这类断言在本机和 CI 上表现一致,docs/src/emulation.md 给的做法是在配置里显式固定下来:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Emulates the browser locale.
    locale: 'en-GB',

    // Emulates the browser timezone.
    timezoneId: 'Europe/Paris',
  },
});

这段是 docs/src/emulation.md 里的原文示例,其中的 en-GBEurope/Paris 只是示例取值,换成你项目实际需要的那组。同一篇里也给了单个文件覆盖的写法 test.use({ locale: ..., timezoneId: ... })。这条对 Windows 开发机 + Linux CI 的组合尤其要紧,因为两边的系统默认值大概率不同。

怎么验证自己配对了

clock.md 给的验证方式其实就藏在示例里:设一个时间、断言页面文本,再改一次时间、断言文本跟着变。第二次断言才是关键——它证明的是”页面读到的时间确实被你控制着”,而不是”页面碰巧显示了这个值”。

放到无操作登出这个场景上,仓库示例的骨架是:先 install() 不带参数(install.time 的说明写明默认用当前系统时间),导航,做一次交互,然后快进:

await page.clock.install();
await page.goto('http://localhost:3333');
await page.getByRole('button').click();
await page.clock.fastForward('05:00');
await expect(page.getByText('You have been logged out due to inactivity.')).toBeVisible();

以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。这里选 fastForward 而不是 runFor,正是因为登出逻辑只关心”到点了没有”,不需要把中间每一秒的重绘回调都跑一遍。如果你的用例反过来要验证倒计时数字逐格递减,那就该换成 runFor

最后提醒两点:这套 API 会改写页面里的全局时间函数,属于对被测页面运行环境的侵入式改动,被测代码若自己保存过 setTimeout 的引用,行为需要你自己确认;另外 Playwright 会启动真实浏览器执行页面脚本,安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。


本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测, 因此不涉及运行速度、稳定性与实际表现的任何描述。 该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。 本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。