間模擬定時(shí)、超時(shí)與時(shí)區(qū)切換)
Playwright Clock API 詳解用 page.clock 精確控制瀏覽器時(shí)間模擬定時(shí)、超時(shí)與時(shí)區(qū)切換【免費(fèi)下載鏈接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/pl/playwright本文圍繞 Playwright 官方 API 文檔 class-clock 展開(kāi)完整覆蓋Clock類自 v1.45 起提供的 7 個(gè)方法install、runFor、fastForward、pauseAt、resume、setFixedTime、setSystemTime的參數(shù)語(yǔ)義與多語(yǔ)言用法并結(jié)合 客戶端實(shí)現(xiàn)、服務(wù)端實(shí)現(xiàn) 與 注入腳本 的源碼說(shuō)明假時(shí)鐘是如何注入到頁(yè)面、如何跨 frame 生效、以及runFor與fastForward在定時(shí)器觸發(fā)上的本質(zhì)差異。讀完后你能夠用 Clock 測(cè)試“用戶閑置 5 分鐘自動(dòng)登出”這類原本需要真實(shí)等待的長(zhǎng)時(shí)長(zhǎng)邏輯并在測(cè)試中手動(dòng)撥動(dòng)時(shí)間驗(yàn)證渲染、超時(shí)與計(jì)劃任務(wù)行為。一、Clock 的定位與作用范圍ClockAPIpage.clock用于在測(cè)試中準(zhǔn)確模擬時(shí)間依賴型行為。驗(yàn)證倒計(jì)時(shí)、超時(shí)登出、定時(shí)刷新等功能時(shí)真實(shí)時(shí)間既慢又不可控Clock 允許測(cè)試代碼手動(dòng)推進(jìn)、暫停、快進(jìn)時(shí)間。從 API 文檔開(kāi)頭的關(guān)鍵約束看Note that clock is installed for the entire BrowserContext, so the time in all the pages and iframes is controlled by the same clock.即Clock 安裝在整個(gè)BrowserContext上該上下文內(nèi)所有頁(yè)面、所有 iframe 的時(shí)間都由同一個(gè)假時(shí)鐘控制。這一點(diǎn)在源碼中得到印證服務(wù)端 Clock 類 持有_browserContext字段所有操作方法都通過(guò)_evaluateInFrames在上下文的全部 frame 中執(zhí)行見(jiàn)下文第三部分。官方使用指南位于 docs/src/clock.md建議的選型順序是優(yōu)先用setFixedTime不夠用時(shí)用install配合pauseAt/fastForward/runFor/resumesetSystemTime僅推薦用于高級(jí)場(chǎng)景。二、API 總覽Clock類* since: v1.45提供以下方法方法作用參數(shù)參數(shù)類型JS / Java / Python / C#setFixedTime(time)讓Date.now()與new Date()始終返回固定假時(shí)間定時(shí)器繼續(xù)正常運(yùn)行timelong\|string\|Date/float\|string\|Date數(shù)值為 Unix 秒/string\|Dateinstall(time?)用假實(shí)現(xiàn)接管一組時(shí)間相關(guān)全局函數(shù)之后可用pauseAt/fastForward/runFor/resume控制時(shí)間time可選同上默認(rèn)當(dāng)前系統(tǒng)時(shí)間runFor(ticks)推進(jìn)時(shí)鐘并觸發(fā)所有到期回調(diào)逐 tick 執(zhí)行tickslong\|stringfastForward(ticks)跳著快進(jìn)時(shí)間到期定時(shí)器最多只觸發(fā)一次tickslong\|stringpauseAt(time)跳進(jìn)時(shí)間并暫停此后不調(diào)用runFor/fastForward/pauseAt/resume則定時(shí)器不再觸發(fā)time同time參數(shù)resume()恢復(fù)時(shí)間流動(dòng)定時(shí)器照常觸發(fā)無(wú)—setSystemTime(time)設(shè)置系統(tǒng)時(shí)間但不觸發(fā)任何定時(shí)器適合測(cè)試頁(yè)面反應(yīng)時(shí)間偏移夏令時(shí)/冬令時(shí)切換、時(shí)區(qū)變更time同time參數(shù)其中ticks既可以是毫秒數(shù)也支持人類可讀字符串08表示 8 秒01:00表示 1 分鐘02:34:10表示 2 小時(shí) 34 分 10 秒。time參數(shù)的語(yǔ)言差異引自 class-clock.mdJS/Javalong|string|Date數(shù)值為毫秒Pythonfloat|string|Date數(shù)值是 Unix 時(shí)間秒也可以傳datetime或字符串C#string|Date。三、install接管哪些時(shí)間相關(guān)函數(shù)Clock.install會(huì)安裝以下時(shí)間相關(guān)函數(shù)的假實(shí)現(xiàn)引自 class-clock.mdDatesetTimeout/clearTimeoutsetInterval/clearIntervalrequestAnimationFrame/cancelAnimationFramerequestIdleCallback/cancelIdleCallbackperformance各語(yǔ)言的調(diào)用示例JS 版本帶time初始化參數(shù)await page.clock.install({ time: new Date(2024-12-10T08:00:00) });await page.clock.install(timedatetime.datetime(2024, 12, 10, 8, 0, 0))SimpleDateFormat format new SimpleDateFormat(yyy-MM-ddTHH:mm:ss); page.clock().install(new Clock.InstallOptions().setTime(format.parse(2024-12-10T08:00:00)));await Page.Clock.InstallAsync(new() { TimeDate new DateTime(2024, 12, 10, 8, 0, 0) });time選項(xiàng)用于指定初始化時(shí)間默認(rèn)為當(dāng)前系統(tǒng)時(shí)間。源碼視角install 實(shí)際替換了什么注入腳本 packages/injected/src/clock.ts 的install函數(shù)createApi 全局替換邏輯揭示了完整的接管清單比文檔列表更完整Date替換為ClockDatecreateDateClockDate.now () clock.now()靜態(tài)屬性parse/UTC/prototype從原生Date鏡像而來(lái)并打上isFake true標(biāo)記setTimeout/setInterval/requestAnimationFrame/requestIdleCallback等全部改為向ClockController.addTimer注冊(cè)不再走瀏覽器原生定時(shí)器performance替換為fakePerformanceperformance.now()返回假時(shí)鐘的ticksgetEntries*返回空數(shù)組、mark/measure返回占位條目源碼 L731-L750額外地Event.timeStamp被重寫為讀取假performance.now()源碼 L811-L818Intl.DateTimeFormat被鏡像以便格式化當(dāng)前假時(shí)間AbortSignal.timeout也被改為走假定時(shí)器源碼 L752-L770。這與 使用指南 中 note 的列表一致property: Page.clock覆蓋原生時(shí)間相關(guān)全局類與函數(shù)包括Event.timeStamp。requestAnimationFrame的延遲由getTimeToNextFrame()計(jì)算為16 - now % 16約 16ms 一幀的假幀率requestIdleCallback在其他定時(shí)器存在時(shí)延遲 50ms 觸發(fā)——這些常量決定了假時(shí)鐘下動(dòng)畫(huà)/空閑回調(diào)的節(jié)奏。一個(gè)防御性細(xì)節(jié)install若發(fā)現(xiàn)Date.isFake已為真會(huì)拋出TypeError(Cant install fake timers twice on the same global object.)源碼 L792-L797即同一全局對(duì)象上不允許重復(fù)安裝。四、runFor 與 fastForward兩種“前進(jìn)時(shí)間”的本質(zhì)差異這兩個(gè)方法都能推進(jìn)時(shí)間但對(duì)定時(shí)器的觸發(fā)策略不同引自 class-clock.mdrunFor(ticks)Advance the clock, firing all the time-related callbacks —— 逐 tick 推進(jìn)途中觸發(fā)所有到期回調(diào)適合精細(xì)控制時(shí)間流逝fastForward(ticks)Advance the clock by jumping forward in time. Only fires due timers at most once —— 相當(dāng)于“用戶合上筆記本蓋子一段時(shí)間再打開(kāi)”到期的定時(shí)器最多只觸發(fā)一次不會(huì)重復(fù)補(bǔ)償中間漏掉的周期。JS 用法await page.clock.runFor(1000); await page.clock.runFor(30:00); await page.clock.fastForward(1000); await page.clock.fastForward(30:00);其他語(yǔ)言await page.clock.run_for(1000) await page.clock.fast_forward(30:00)page.clock().runFor(1000); page.clock().fastForward(30:00);await page.Clock.RunForAsync(1000); await page.Clock.FastForwardAsync(30:00);源碼視角差異的確切含義在 注入腳本 中runFor調(diào)用_runToL169-L196循環(huán)取出最早到期的定時(shí)器執(zhí)行期間setInterval類定時(shí)器執(zhí)行后callAt按delay順延_takeFirstTimerL341-L353因此 2 秒內(nèi)一個(gè)每秒一次的 interval 會(huì)被完整觸發(fā) 2 次。而fastForward先執(zhí)行_innerFastForwardToL284-L292把所有callAt早于目標(biāo)時(shí)刻的定時(shí)器的callAt鉗制到目標(biāo)時(shí)刻再執(zhí)行_runTo——到期定時(shí)器在同一時(shí)刻各自只執(zhí)行一次。這正是文檔中“Only fires due timers at most once”的實(shí)現(xiàn)依據(jù)。單元測(cè)試與頁(yè)面級(jí)測(cè)試分別在 tests/library/unit/clock.spec.ts直接對(duì)ClockController做定時(shí)器語(yǔ)義的回歸驗(yàn)證如 id 從1e12起始以避免與原生 timer id 沖突、參數(shù)透?jìng)鳌⑦f歸 tick 等和 tests/library/page-clock.spec.ts真實(shí)瀏覽器中的端到端驗(yàn)證中覆蓋。五、pauseAt 與 resume暫停與恢復(fù)時(shí)間pauseAt(time)跳到指定時(shí)間并暫停時(shí)鐘。調(diào)用之后除非再調(diào)用runFor、fastForward、pauseAt或resume否則任何定時(shí)器都不會(huì)觸發(fā)。它同樣只讓到期定時(shí)器最多觸發(fā)一次語(yǔ)義等價(jià)于“合上筆記本到指定時(shí)刻打開(kāi)并暫?!?。JS 用法await page.clock.pauseAt(new Date(2020-02-02)); await page.clock.pauseAt(2020-02-02);await page.clock.pause_at(datetime.datetime(2020, 2, 2)) await page.clock.pause_at(2020-02-02)SimpleDateFormat format new SimpleDateFormat(yyy-MM-dd); page.clock().pauseAt(format.parse(2020-02-02)); page.clock().pauseAt(2020-02-02);await page.Clock.PauseAtAsync(DateTime.Parse(2020-02-02)); await page.Clock.PauseAtAsync(2020-02-02);文檔特別給出最佳實(shí)踐在導(dǎo)航之前安裝時(shí)鐘并設(shè)置為略早于目標(biāo)測(cè)試時(shí)間讓頁(yè)面加載期間定時(shí)器自然運(yùn)行防止頁(yè)面卡死等頁(yè)面完全加載后再pauseAt// Initialize clock with some time before the test time and let the page load // naturally. Date.now will progress as the timers fire. await page.clock.install({ time: new Date(2024-12-10T08:00:00) }); await page.goto(http://localhost:3333); await page.clock.pauseAt(new Date(2024-12-10T10:00:00));resume()則恢復(fù)時(shí)間流動(dòng)Once this method is called, time resumes flowing, timers are fired as usual. 源碼中resume通過(guò)_innerResume重建_realTime狀態(tài)并用真實(shí)定時(shí)器按 100ms 塊推進(jìn)假時(shí)間_updateRealTimeTimer源碼 L223-L261保證Date.now()在恢復(fù)后仍能隨真實(shí)時(shí)間自然增長(zhǎng)。六、setFixedTime 與 setSystemTime兩種“改時(shí)間”setFixedTime —— 固定時(shí)間定時(shí)器照常讓Date.now和new Date()始終返回固定假時(shí)間同時(shí)保持所有定時(shí)器運(yùn)行。適合只需要一個(gè)預(yù)定義時(shí)間的簡(jiǎn)單場(chǎng)景更復(fù)雜的場(chǎng)景改用install。await page.clock.setFixedTime(Date.now()); await page.clock.setFixedTime(new Date(2020-02-02)); await page.clock.setFixedTime(2020-02-02);await page.clock.set_fixed_time(datetime.datetime.now()) await page.clock.set_fixed_time(datetime.datetime(2020, 2, 2)) await page.clock.set_fixed_time(2020-02-02)page.clock().setFixedTime(new Date()); page.clock().setFixedTime(new SimpleDateFormat(yyy-MM-dd).parse(2020-02-02)); page.clock().setFixedTime(2020-02-02);await page.Clock.SetFixedTimeAsync(DateTime.Now); await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2)); await page.Clock.SetFixedTimeAsync(2020-02-02);源碼中setFixedTime會(huì)置isFixedTime true_innerSetFixedTime源碼 L148-L151此時(shí)_advanceNow不再推進(jìn)time墻面時(shí)間而只推進(jìn)ticks單調(diào)計(jì)數(shù)因此定時(shí)器照常觸發(fā)但Date.now()紋絲不動(dòng)——這正是 使用指南 “Test with predefined time” 示例每秒用setInterval渲染new Date()的頁(yè)面兩次setFixedTime后斷言顯示文本能成立的原因。setSystemTime —— 只撥指針不觸發(fā)定時(shí)器設(shè)置系統(tǒng)時(shí)間但不觸發(fā)任何定時(shí)器。官方定位是測(cè)試頁(yè)面對(duì)時(shí)間偏移的反應(yīng)例如從夏令時(shí)切到冬令時(shí)、或改變時(shí)區(qū)await page.clock.setSystemTime(Date.now()); await page.clock.setSystemTime(new Date(2020-02-02)); await page.clock.setSystemTime(2020-02-02);await page.clock.set_system_time(datetime.datetime.now()) await page.clock.set_system_time(datetime.datetime(2020, 2, 2)) await page.clock.set_system_time(2020-02-02)page.clock().setSystemTime(new Date()); page.clock().setSystemTime(new SimpleDateFormat(yyy-MM-dd).parse(2020-02-02)); page.clock().setSystemTime(2020-02-02);await page.Clock.SetSystemTimeAsync(DateTime.Now); await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2)); await page.Clock.SetSystemTimeAsync(2020-02-02);time參數(shù)含義為毫秒級(jí)時(shí)間JS/Java、Unix 秒/datetime/字符串Python、DateTime/字符串C#與setFixedTime相同。七、客戶端到瀏覽器內(nèi)部的完整調(diào)用鏈理解 Clock 如何工作值得走一遍三層實(shí)現(xiàn)1. 客戶端測(cè)試進(jìn)程packages/playwright-core/src/client/clock.ts 中Clock類構(gòu)造時(shí)綁定BrowserContext每個(gè)方法只是轉(zhuǎn)發(fā)對(duì)應(yīng)的 channel 消息clockInstall、clockRunFor、clockFastForward、clockPauseAt、clockResume、clockSetFixedTime、clockSetSystemTime且統(tǒng)一使用kNoTimeout無(wú)超時(shí)。parseTime把number/string/Date歸一為協(xié)議字段timeNumber/timeStringparseTicks把毫秒數(shù)與可讀字符串歸一為ticksNumber/ticksString。2. 服務(wù)端驅(qū)動(dòng)瀏覽器的一側(cè)packages/playwright-core/src/server/clock.ts 是核心機(jī)制所在_installIfNeeded若尚未安裝把 packages/injected/src/clock.ts 的源碼經(jīng)packages/playwright-core/src/generated/clockSource生成作為初始化腳本注冊(cè)到BrowserContext并立即在現(xiàn)有 frame 中執(zhí)行掛載出globalThis.__pwClockL95-L107每個(gè)操作install/runFor/pauseAt/…都會(huì)先注冊(cè)一條 init script在之后新建的 frame/頁(yè)面中調(diào)用__pwClock.controller.log(type, Date.now(), param)記錄該操作同時(shí)用safeNonStallingEvaluateInAllFrames在當(dāng)前所有 frame中立即執(zhí)行L37-L49。這就解釋了“上下文級(jí)作用范圍”新打開(kāi)的頁(yè)面自動(dòng)重放此前的時(shí)間操作各 frame 狀態(tài)保持一致服務(wù)端parseTicksL119-L146是08、01:00、02:34:10字符串格式的解析器正則/^(\d\d:){0,2}\d\d?$/限制最多hh:mm:ss三段任一段 ≥ 60 拋Invalid time最終換算為毫秒。3. 瀏覽器內(nèi)部注入腳本packages/injected/src/clock.ts 的ClockController維護(hù)兩套時(shí)間——ticks單調(diào)推進(jìn)驅(qū)動(dòng)定時(shí)器調(diào)度與performance.now()和time墻面時(shí)間驅(qū)動(dòng)Date.now()。runFor/fastForward期間會(huì)先臨時(shí)_innerPause真實(shí)時(shí)間同步、執(zhí)行完再_innerResume_runWithDisabledRealTimeSyncL263-L275避免手動(dòng)推進(jìn)與真實(shí)時(shí)間漂移互相干擾。install時(shí)若為全新安裝ticks會(huì)被重置為 0防止安裝前真實(shí)時(shí)間累積的漂移泄漏進(jìn)performance.now()L139-L146 的注釋說(shuō)明了該動(dòng)機(jī)。定時(shí)器排序規(guī)則compareTimersL581-L607為先到期先執(zhí)行 → Immediate 優(yōu)先 → 先創(chuàng)建優(yōu)先 → id 小者優(yōu)先。八、實(shí)戰(zhàn)場(chǎng)景閑置超時(shí)登出的秒級(jí)驗(yàn)證使用指南 給出的典型場(chǎng)景是“不活動(dòng)監(jiān)控”頁(yè)面在 5 分鐘無(wú)操作后登出用戶真實(shí)測(cè)試要等 5 分鐘。用 Clock 可秒級(jí)完成// Initial time does not matter for the test, so we can pick current time. await page.clock.install(); await page.goto(http://localhost:3333); // Interact with the page await page.getByRole(button).click(); // Fast forward time 5 minutes as if the user did not do anything. // Fast forward is like closing the laptop lid and opening it after 5 minutes. // All the timers due will fire once immediately, as in the real browser. await page.clock.fastForward(05:00); // Check that the user was logged out automatically. await expect(page.getByText(You have been logged out due to inactivity.)).toBeVisible();這里用fastForward而非runFor正符合其語(yǔ)義模擬“5 分鐘內(nèi)用戶什么都沒(méi)做”期間到期的定時(shí)器一次性觸發(fā)與真實(shí)瀏覽器合蓋再開(kāi)蓋的行為一致。指南中還演示了另一組合install到較早時(shí)間 → 頁(yè)面自然加載 →pauseAt到目標(biāo)時(shí)刻斷言 →fastForward(30:00)再斷言完整代碼可參考 docs/src/clock.md。九、注意事項(xiàng)與限制調(diào)用順序官方指南明確警告一旦調(diào)用install它必須先于其他時(shí)鐘相關(guān)調(diào)用發(fā)生順序錯(cuò)亂如先setInterval再install再clearInterval會(huì)導(dǎo)致未定義行為因?yàn)閕nstall會(huì)覆蓋原生的時(shí)鐘函數(shù)定義見(jiàn) docs/src/clock.md 的 warning上下文級(jí)作用范圍Clock 作用于整個(gè)BrowserContext同上下文的多個(gè)頁(yè)面與 iframe 共享同一時(shí)鐘class-clock.md 開(kāi)頭聲明人類可讀時(shí)間格式有限ticks字符串僅支持ss/mm:ss/hh:mm:ss形態(tài)超過(guò) 3 段或單段 ≥ 60 會(huì)拋出Clock only understands numbers, mm:ss and hh:mm:ss/Invalid time錯(cuò)誤server/clock.tsPython 數(shù)值單位是秒time/setFixedTime等參數(shù)在 Python 中傳數(shù)值時(shí)是 Unix 秒float與其他語(yǔ)言的毫秒不同跨語(yǔ)言移植測(cè)試時(shí)需注意版本前提整個(gè)Clock類自v1.45引入本文所有行為描述以當(dāng)前倉(cāng)庫(kù)packages/playwright-core、packages/injected源碼為準(zhǔn)。十、延伸閱讀API 參考本文主體docs/src/api/class-clock.md使用指南與完整場(chǎng)景示例docs/src/clock.md客戶端/服務(wù)端/注入腳本三層實(shí)現(xiàn)packages/playwright-core/src/client/clock.ts、packages/playwright-core/src/server/clock.ts、packages/injected/src/clock.ts測(cè)試覆蓋tests/library/unit/clock.spec.ts、tests/library/page-clock.spec.ts【免費(fèi)下載鏈接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/pl/playwright創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考