From a617dd0df8cad7e1ec4391f7539ed6e63afd0f88 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 31 May 2024 05:39:58 -0700 Subject: [PATCH 001/144] feat(webkit): roll to r2015 (#31103) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 5215861436..263862e1eb 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2014", + "revision": "2015", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 76e977a934ccf96c6e7236f9e33e2c2dd8afff42 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 31 May 2024 08:09:24 -0700 Subject: [PATCH 002/144] chore: add clock.next() (#31097) --- docs/src/api/class-clock.md | 10 +- packages/playwright-core/src/client/clock.ts | 5 + .../playwright-core/src/protocol/validator.ts | 4 + packages/playwright-core/src/server/clock.ts | 8 +- .../dispatchers/browserContextDispatcher.ts | 4 + .../src/third_party/fake-timers-src.js | 1 - packages/playwright-core/types/types.d.ts | 5 + packages/protocol/src/channels.ts | 6 + packages/protocol/src/protocol.yml | 4 + tests/page/page-clock.spec.ts | 122 +++++++++++++++++- 10 files changed, 160 insertions(+), 9 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index f119073e07..a78f7b30ba 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -4,6 +4,7 @@ Playwright uses [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) for clock emulation. Clock is installed for the entire [BrowserContext], so the time in all the pages and iframes is controlled by the same clock. + ## async method: Clock.install * since: v1.45 @@ -42,6 +43,14 @@ Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the Relevant only when using with [`option: shouldAdvanceTime`]. Increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change in the real system time (default: 20). + +## async method: Clock.next +* since: v1.45 +- returns: <[int]> Fake milliseconds since the unix epoch. + +Advances the clock to the the moment of the first scheduled timer, firing it. + + ## async method: Clock.jump * since: v1.45 @@ -54,7 +63,6 @@ This can be used to simulate the JS engine (such as a browser) being put to slee Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. - ## async method: Clock.runAll * since: v1.45 - returns: <[int]> Fake milliseconds since the unix epoch. diff --git a/packages/playwright-core/src/client/clock.ts b/packages/playwright-core/src/client/clock.ts index 8fd9e359d8..3287f41dbb 100644 --- a/packages/playwright-core/src/client/clock.ts +++ b/packages/playwright-core/src/client/clock.ts @@ -37,6 +37,11 @@ export class Clock implements api.Clock { }); } + async next(): Promise { + const result = await this._browserContext._channel.clockNext(); + return result.fakeTime; + } + async runAll(): Promise { const result = await this._browserContext._channel.clockRunAll(); return result.fakeTime; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 30c7856ca5..6e2993901f 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -976,6 +976,10 @@ scheme.BrowserContextClockJumpParams = tObject({ timeString: tOptional(tString), }); scheme.BrowserContextClockJumpResult = tOptional(tObject({})); +scheme.BrowserContextClockNextParams = tOptional(tObject({})); +scheme.BrowserContextClockNextResult = tObject({ + fakeTime: tNumber, +}); scheme.BrowserContextClockRunAllParams = tOptional(tObject({})); scheme.BrowserContextClockRunAllResult = tObject({ fakeTime: tNumber, diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index e0ce1b64ff..fee151b407 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -40,7 +40,13 @@ export class Clock { async jump(time: number | string) { this._assertInstalled(); - await this._addAndEvaluate(`globalThis.__pwFakeTimers.jump(${JSON.stringify(time)}); 0`); + await this._addAndEvaluate(`globalThis.__pwFakeTimers.jump(${JSON.stringify(time)})`); + } + + async next(): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.next()`); + return await this._evaluateInFrames(`globalThis.__pwFakeTimers.nextAsync()`); } async runAll(): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 741e49a456..419b99f205 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -320,6 +320,10 @@ export class BrowserContextDispatcher extends Dispatcher { + return { fakeTime: await this._context.clock.next() }; + } + async clockRunAll(params: channels.BrowserContextClockRunAllParams, metadata?: CallMetadata | undefined): Promise { return { fakeTime: await this._context.clock.runAll() }; } diff --git a/packages/playwright-core/src/third_party/fake-timers-src.js b/packages/playwright-core/src/third_party/fake-timers-src.js index 9602123052..d4bfdd7305 100644 --- a/packages/playwright-core/src/third_party/fake-timers-src.js +++ b/packages/playwright-core/src/third_party/fake-timers-src.js @@ -1654,7 +1654,6 @@ function withGlobal(_global) { * @returns {Clock} */ function install(config) { - console.log('INSTALL', config); if ( arguments.length > 1 || config instanceof Date || diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 6757e3b79c..82538f2b50 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17289,6 +17289,11 @@ export interface Clock { */ jump(time: number|string): Promise; + /** + * Advances the clock to the the moment of the first scheduled timer, firing it. + */ + next(): Promise; + /** * Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be * run as well. This makes it easier to run asynchronous tests to completion without worrying about the number of diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index df8658ee0d..8308477701 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1462,6 +1462,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise; clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise; clockJump(params: BrowserContextClockJumpParams, metadata?: CallMetadata): Promise; + clockNext(params?: BrowserContextClockNextParams, metadata?: CallMetadata): Promise; clockRunAll(params?: BrowserContextClockRunAllParams, metadata?: CallMetadata): Promise; clockRunToLast(params?: BrowserContextClockRunToLastParams, metadata?: CallMetadata): Promise; clockTick(params: BrowserContextClockTickParams, metadata?: CallMetadata): Promise; @@ -1777,6 +1778,11 @@ export type BrowserContextClockJumpOptions = { timeString?: string, }; export type BrowserContextClockJumpResult = void; +export type BrowserContextClockNextParams = {}; +export type BrowserContextClockNextOptions = {}; +export type BrowserContextClockNextResult = { + fakeTime: number, +}; export type BrowserContextClockRunAllParams = {}; export type BrowserContextClockRunAllOptions = {}; export type BrowserContextClockRunAllResult = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 96a511d306..4151cf7104 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1219,6 +1219,10 @@ BrowserContext: timeNumber: number? timeString: string? + clockNext: + returns: + fakeTime: number + clockRunAll: returns: fakeTime: number diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index c3e2aaeb54..185443d4c7 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -40,7 +40,7 @@ it.describe('tick', () => { }); await page.clock.tick(0); - expect(calls).toEqual([{ params: [] }]); + expect(calls).toHaveLength(1); }); it('does not trigger without sufficient delay', async ({ page, calls }) => { @@ -58,7 +58,7 @@ it.describe('tick', () => { setTimeout(window.stub, 100); }); await page.clock.tick(100); - expect(calls).toEqual([{ params: [] }]); + expect(calls).toHaveLength(1); }); it('triggers simultaneous timers', async ({ page, calls }) => { @@ -68,7 +68,7 @@ it.describe('tick', () => { setTimeout(window.stub, 100); }); await page.clock.tick(100); - expect(calls).toEqual([{ params: [] }, { params: [] }]); + expect(calls).toHaveLength(2); }); it('triggers multiple simultaneous timers', async ({ page, calls }) => { @@ -91,7 +91,7 @@ it.describe('tick', () => { await page.clock.tick(50); expect(calls).toEqual([]); await page.clock.tick(100); - expect(calls).toEqual([{ params: [] }]); + expect(calls).toHaveLength(1); }); it('triggers event when some throw', async ({ page, calls }) => { @@ -102,7 +102,7 @@ it.describe('tick', () => { }); await expect(page.clock.tick(120)).rejects.toThrow(); - expect(calls).toEqual([{ params: [] }]); + expect(calls).toHaveLength(1); }); it('creates updated Date while ticking', async ({ page, calls }) => { @@ -210,7 +210,7 @@ it.describe('jump', () => { }); }); -it.describe('runAllAsyn', () => { +it.describe('runAll', () => { it('if there are no timers just return', async ({ page }) => { await page.clock.install(); await page.clock.runAll(); @@ -612,3 +612,113 @@ it.describe('shouldAdvanceTime', () => { expect(timeDifference).toBe(0); }); }); + +it.describe('popup', () => { + it('should tick after popup', async ({ page }) => { + const now = new Date('2015-09-25'); + await page.clock.install({ now }); + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.evaluate(() => window.open('about:blank')), + ]); + const popupTime = await popup.evaluate(() => Date.now()); + expect(popupTime).toBe(now.getTime()); + await page.clock.tick(1000); + const popupTimeAfter = await popup.evaluate(() => Date.now()); + expect(popupTimeAfter).toBe(now.getTime() + 1000); + }); + + it('should tick before popup', async ({ page, browserName }) => { + it.skip(browserName === 'chromium'); + const now = new Date('2015-09-25'); + await page.clock.install({ now }); + const newNow = await page.clock.tick(1000); + expect(newNow).toBe(now.getTime() + 1000); + + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.evaluate(() => window.open('about:blank')), + ]); + const popupTime = await popup.evaluate(() => Date.now()); + expect(popupTime).toBe(now.getTime() + 1000); + }); +}); + +it.describe('next', () => { + it('triggers the next timer', async ({ page, calls }) => { + await page.clock.install(); + await page.evaluate(async () => { + setTimeout(window.stub, 100); + }); + expect(await page.clock.next()).toBe(100); + expect(calls).toHaveLength(1); + }); + + it('does not trigger simultaneous timers', async ({ page, calls }) => { + await page.clock.install(); + await page.evaluate(() => { + setTimeout(() => { + window.stub(); + }, 100); + setTimeout(() => { + window.stub(); + }, 100); + }); + + await page.clock.next(); + expect(calls).toHaveLength(1); + }); + + it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => { + await page.clock.install(); + await page.evaluate(async () => { + setTimeout(() => { + window.stub(); + }, 100); + setTimeout(() => { + window.stub(); + }, 100); + setTimeout(() => { + window.stub(); + }, 99); + setTimeout(() => { + window.stub(); + }, 100); + }); + + await page.clock.next(); + expect(calls).toHaveLength(1); + await page.clock.next(); + expect(calls).toHaveLength(2); + await page.clock.next(); + expect(calls).toHaveLength(3); + await page.clock.next(); + expect(calls).toHaveLength(4); + }); + + it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => { + await page.clock.install(); + await page.evaluate(async () => { + window.stub(1); + setTimeout(() => { + setTimeout(() => window.stub(2), 0); + }, 0); + }); + + await page.clock.next(); + expect(calls).toEqual([{ params: [1] }]); + await page.clock.next(); + expect(calls).toEqual([{ params: [1] }, { params: [2] }]); + }); + + it('throws exception thrown by timer', async ({ page, calls }) => { + await page.clock.install(); + await page.evaluate(async () => { + setTimeout(() => { + throw new Error(); + }, 100); + }); + + await expect(page.clock.next()).rejects.toThrow(); + }); +}); From afa0bf224748d77f5dc03b8d84c9bb7ed109a2fe Mon Sep 17 00:00:00 2001 From: Joe-Hendley <95080839+Joe-Hendley@users.noreply.github.com> Date: Fri, 31 May 2024 18:45:56 +0100 Subject: [PATCH 003/144] feat: increase length of printed html tags (#31105) resolves https://github.com/microsoft/playwright/issues/30977 by increasing the printed length of HTML tags from 50 -> 500 as suggested by @dgozman --- packages/playwright-core/src/server/injected/injectedScript.ts | 2 +- tests/library/hit-target.spec.ts | 2 +- tests/page/elementhandle-convenience.spec.ts | 2 +- tests/page/page-wait-for-selector-1.spec.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index c988143a8f..dba8d188e4 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -983,7 +983,7 @@ export class InjectedScript { attrs.push(` ${name}="${value}"`); } attrs.sort((a, b) => a.length - b.length); - const attrText = trimStringWithEllipsis(attrs.join(''), 50); + const attrText = trimStringWithEllipsis(attrs.join(''), 500); if (autoClosingTags.has(element.nodeName)) return oneLine(`<${element.nodeName.toLowerCase()}${attrText}/>`); diff --git a/tests/library/hit-target.spec.ts b/tests/library/hit-target.spec.ts index 0b3d0f6597..6bf4085b6b 100644 --- a/tests/library/hit-target.spec.ts +++ b/tests/library/hit-target.spec.ts @@ -253,7 +253,7 @@ it('should not click iframe overlaying the target', async ({ page, server }) => `); const error = await page.click('text=click-me', { timeout: 1000 }).catch(e => e); expect(await page.evaluate('window._clicked')).toBe(undefined); - expect(error.message).toContain(` from
…
subtree intercepts pointer events`); }); it('should not click an element overlaying iframe with the target', async ({ page, server }) => { diff --git a/tests/page/elementhandle-convenience.spec.ts b/tests/page/elementhandle-convenience.spec.ts index 5020f34ea8..8e2f4895bf 100644 --- a/tests/page/elementhandle-convenience.spec.ts +++ b/tests/page/elementhandle-convenience.spec.ts @@ -34,7 +34,7 @@ it('should have a nice preview for non-ascii attributes/children', async ({ page await page.goto(server.EMPTY_PAGE); await page.setContent(`
${'πŸ˜›'.repeat(100)}`); const handle = await page.$('div'); - await expect.poll(() => String(handle)).toBe(`JSHandle@
πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›β€¦
`); + await expect.poll(() => String(handle)).toBe(`JSHandle@
πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›πŸ˜›β€¦
`); }); it('getAttribute should work', async ({ page, server }) => { diff --git a/tests/page/page-wait-for-selector-1.spec.ts b/tests/page/page-wait-for-selector-1.spec.ts index d20165af66..3ab3e92b73 100644 --- a/tests/page/page-wait-for-selector-1.spec.ts +++ b/tests/page/page-wait-for-selector-1.spec.ts @@ -135,7 +135,7 @@ it('should report logs while waiting for visible', async ({ page, server }) => { const error = await watchdog.catch(e => e); expect(error.message).toContain(`frame.waitForSelector: Timeout 5000ms exceeded.`); expect(error.message).toContain(`waiting for locator(\'div\') to be visible`); - expect(error.message).toContain(`locator resolved to hidden
abcdefghijklmnopqrstuvwyxzabcdefghijklmnopqrstuvw…
`); expect(error.message).toContain(`locator resolved to hidden
`); }); From 8bfd0eb6e4188985c6a5f0ad57306eda10a37266 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 31 May 2024 14:44:26 -0700 Subject: [PATCH 004/144] chore: introduce clock test mode (#31110) --- .github/workflows/tests_clock.yml | 62 +++++++++++++++++++ docs/src/api/class-clock.md | 2 +- .../playwright-core/src/client/browser.ts | 2 + packages/playwright-core/src/client/frame.ts | 2 +- .../playwright-core/src/protocol/validator.ts | 1 - .../src/server/dispatchers/frameDispatcher.ts | 2 +- packages/playwright-core/src/server/dom.ts | 4 +- packages/playwright-core/src/server/frames.ts | 8 +-- .../src/server/injected/fakeTimers.ts | 19 +++++- .../src/server/injected/highlight.ts | 2 +- .../src/server/injected/injectedScript.ts | 29 ++++++++- .../src/server/injected/recorder/recorder.ts | 8 +-- .../src/server/injected/utilityScript.ts | 52 +++++++++++++++- .../playwright-core/src/server/javascript.ts | 7 ++- packages/playwright-core/types/types.d.ts | 3 +- packages/protocol/src/channels.ts | 2 - packages/protocol/src/protocol.yml | 1 - tests/assets/input/handle-locator.html | 2 +- tests/config/baseTest.ts | 13 ++++ .../browsercontext-add-cookies.spec.ts | 1 + tests/library/browsercontext-events.spec.ts | 2 +- tests/library/browsertype-launch.spec.ts | 2 +- tests/library/chromium/css-coverage.spec.ts | 2 +- tests/library/chromium/js-coverage.spec.ts | 1 + tests/library/headful.spec.ts | 1 + tests/library/popup.spec.ts | 8 +-- tests/library/trace-viewer.spec.ts | 4 +- tests/library/tracing.spec.ts | 6 +- tests/library/video.spec.ts | 4 +- tests/page/elementhandle-screenshot.spec.ts | 11 ++-- .../elementhandle-scroll-into-view.spec.ts | 2 +- tests/page/elementhandle-select-text.spec.ts | 2 +- ...ementhandle-wait-for-element-state.spec.ts | 10 ++- tests/page/page-click-timeout-4.spec.ts | 4 +- tests/page/page-click.spec.ts | 12 ++-- tests/page/page-clock.spec.ts | 2 + tests/page/page-close.spec.ts | 3 +- tests/page/page-dialog.spec.ts | 2 +- tests/page/page-dispatchevent.spec.ts | 2 +- tests/page/page-drag.spec.ts | 2 +- tests/page/page-evaluate.spec.ts | 20 ++---- tests/page/page-event-console.spec.ts | 4 +- tests/page/page-event-pageerror.spec.ts | 16 +++-- tests/page/page-expose-function.spec.ts | 2 +- tests/page/page-fill.spec.ts | 8 +-- tests/page/page-goto.spec.ts | 5 +- tests/page/page-history.spec.ts | 1 + tests/page/page-mouse.spec.ts | 4 +- tests/page/page-screenshot.spec.ts | 14 +---- tests/page/page-select-option.spec.ts | 8 +-- tests/page/page-set-input-files.spec.ts | 2 +- tests/page/page-wait-for-function.spec.ts | 4 +- tests/page/page-wait-for-navigation.spec.ts | 2 +- tests/page/page-wait-for-request.spec.ts | 2 +- tests/page/page-wait-for-response.spec.ts | 2 +- tests/page/page-wait-for-selector-1.spec.ts | 7 +-- tests/page/pageTest.ts | 10 ++- tests/page/retarget.spec.ts | 7 +-- tests/page/wheel.spec.ts | 5 +- .../ui-mode-test-output.spec.ts | 4 +- 60 files changed, 291 insertions(+), 140 deletions(-) create mode 100644 .github/workflows/tests_clock.yml diff --git a/.github/workflows/tests_clock.yml b/.github/workflows/tests_clock.yml new file mode 100644 index 0000000000..cc9a519ee3 --- /dev/null +++ b/.github/workflows/tests_clock.yml @@ -0,0 +1,62 @@ +name: "tests Clock" + +on: + push: + branches: + - main + - release-* + pull_request: + paths-ignore: + - 'browser_patches/**' + - 'docs/**' + types: [ labeled ] + branches: + - main + - release-* + +env: + # Force terminal colors. @see https://www.npmjs.com/package/colors + FORCE_COLOR: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + +jobs: + frozen_time_linux: + name: Frozen time library + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/run-test + with: + node-version: 20 + browsers-to-install: chromium + command: npm run test -- --project=chromium-* + bot-name: "frozen-time-library-chromium-linux" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} + env: + PW_FREEZE_TIME: 1 + + frozen_time_test_runner: + name: Frozen time test runner + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + runs-on: ubuntu-22.04 + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/run-test + with: + node-version: 20 + command: npm run ttest + bot-name: "frozen-time-runner-chromium-linux" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} + env: + PW_FREEZE_TIME: 1 diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index a78f7b30ba..90c75de16a 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -21,7 +21,7 @@ Install fake timers with the specified unix epoch (default: 0). - `toFake` <[Array]<[FakeMethod]<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">>> An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: ['setTimeout'] })` will fake only `setTimeout()`. -By default, `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` and `Date` are faked. +By default, all the methods are faked. ### option: Clock.install.loopLimit * since: v1.45 diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index be47ddeb51..802939295c 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -85,6 +85,8 @@ export class Browser extends ChannelOwner implements ap const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const context = BrowserContext.from(response.context); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); + if (!forReuse && !!process.env.PW_FREEZE_TIME) + await this._wrapApiCall(async () => { await context.clock.install(); }, true); return context; } diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index 582490d0af..a7ceeb9456 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -189,7 +189,7 @@ export class Frame extends ChannelOwner implements api.Fr async _evaluateExposeUtilityScript(pageFunction: structs.PageFunction, arg?: Arg): Promise { assertMaxArguments(arguments.length, 2); - const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', exposeUtilityScript: true, arg: serializeArgument(arg) }); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 6e2993901f..ec1d4bffe2 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1429,7 +1429,6 @@ scheme.FrameDispatchEventResult = tOptional(tObject({})); scheme.FrameEvaluateExpressionParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), - exposeUtilityScript: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.FrameEvaluateExpressionResult = tObject({ diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index b335f27ea0..6dcb9a7220 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -83,7 +83,7 @@ export class FrameDispatcher extends Dispatcher { - return { value: serializeResult(await this._frame.evaluateExpression(params.expression, { isFunction: params.isFunction, exposeUtilityScript: params.exposeUtilityScript }, parseArgument(params.arg))) }; + return { value: serializeResult(await this._frame.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) }; } async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise { diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index eb2f2068f1..01e70f1637 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -71,11 +71,11 @@ export class FrameExecutionContext extends js.ExecutionContext { return js.evaluate(this, false /* returnByValue */, pageFunction, arg); } - async evaluateExpression(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise { + async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg?: any): Promise { return js.evaluateExpression(this, expression, { ...options, returnByValue: true }, arg); } - async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise> { + async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg?: any): Promise> { return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index e2b1eec2a5..c2a11d27be 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -745,13 +745,13 @@ export class Frame extends SdkObject { return this._context('utility'); } - async evaluateExpression(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean, world?: types.World } = {}, arg?: any): Promise { + async evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise { const context = await this._context(options.world ?? 'main'); const value = await context.evaluateExpression(expression, options, arg); return value; } - async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean, world?: types.World } = {}, arg?: any): Promise> { + async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise> { const context = await this._context(options.world ?? 'main'); const value = await context.evaluateExpressionHandle(expression, options, arg); return value; @@ -1513,9 +1513,9 @@ export class Frame extends SdkObject { return; } if (typeof polling !== 'number') - requestAnimationFrame(next); + injected.builtinRequestAnimationFrame(next); else - setTimeout(next, polling); + injected.builtinSetTimeout(next, polling); } catch (e) { reject(e); } diff --git a/packages/playwright-core/src/server/injected/fakeTimers.ts b/packages/playwright-core/src/server/injected/fakeTimers.ts index 3e8b1ab05b..e3d8e5a54d 100644 --- a/packages/playwright-core/src/server/injected/fakeTimers.ts +++ b/packages/playwright-core/src/server/injected/fakeTimers.ts @@ -19,5 +19,22 @@ import SinonFakeTimers from '../../third_party/fake-timers-src'; import type * as channels from '@protocol/channels'; export function install(params: channels.BrowserContextClockInstallOptions) { - return SinonFakeTimers.install(params); + // eslint-disable-next-line no-restricted-globals + const window = globalThis; + const builtin = { + setTimeout: window.setTimeout.bind(window), + clearTimeout: window.clearTimeout.bind(window), + setInterval: window.setInterval.bind(window), + clearInterval: window.clearInterval.bind(window), + requestAnimationFrame: window.requestAnimationFrame.bind(window), + cancelAnimationFrame: window.cancelAnimationFrame.bind(window), + requestIdleCallback: window.requestIdleCallback?.bind(window), + cancelIdleCallback: window.cancelIdleCallback?.bind(window), + performance: window.performance, + Intl: window.Intl, + Date: window.Date, + }; + const result = SinonFakeTimers.install(params); + result.builtin = builtin; + return result; } diff --git a/packages/playwright-core/src/server/injected/highlight.ts b/packages/playwright-core/src/server/injected/highlight.ts index dfc298cb24..c85216e106 100644 --- a/packages/playwright-core/src/server/injected/highlight.ts +++ b/packages/playwright-core/src/server/injected/highlight.ts @@ -101,7 +101,7 @@ export class Highlight { if (this._rafRequest) cancelAnimationFrame(this._rafRequest); this.updateHighlight(this._injectedScript.querySelectorAll(selector, this._injectedScript.document.documentElement), { tooltipText: asLocator(this._language, stringifySelector(selector)) }); - this._rafRequest = requestAnimationFrame(() => this.runHighlightOnRaf(selector)); + this._rafRequest = this._injectedScript.builtinRequestAnimationFrame(() => this.runHighlightOnRaf(selector)); } uninstall() { diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index dba8d188e4..347d5cb40c 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -124,6 +124,18 @@ export class InjectedScript { (this.window as any).__injectedScript = this; } + builtinSetTimeout(callback: Function, timeout: number) { + if (this.window.__pwFakeTimers?.builtin) + return this.window.__pwFakeTimers.builtin.setTimeout(callback, timeout); + return setTimeout(callback, timeout); + } + + builtinRequestAnimationFrame(callback: FrameRequestCallback) { + if (this.window.__pwFakeTimers?.builtin) + return this.window.__pwFakeTimers.builtin.requestAnimationFrame(callback); + return requestAnimationFrame(callback); + } + eval(expression: string): any { return this.window.eval(expression); } @@ -427,7 +439,7 @@ export class InjectedScript { observer.observe(element); // Firefox doesn't call IntersectionObserver callback unless // there are rafs. - requestAnimationFrame(() => {}); + this.builtinRequestAnimationFrame(() => {}); }); } @@ -536,12 +548,12 @@ export class InjectedScript { if (success !== continuePolling) fulfill(success); else - requestAnimationFrame(raf); + this.builtinRequestAnimationFrame(raf); } catch (e) { reject(e); } }; - requestAnimationFrame(raf); + this.builtinRequestAnimationFrame(raf); return result; } @@ -1510,3 +1522,14 @@ function deepEquals(a: any, b: any): boolean { return false; } + +declare global { + interface Window { + __pwFakeTimers?: { + builtin: { + setTimeout: Window['setTimeout'], + requestAnimationFrame: Window['requestAnimationFrame'], + } + } + } +} diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 8c02ddb507..3977d69aef 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -881,7 +881,7 @@ class Overlay { flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') { const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle; element.classList.add('succeeded'); - setTimeout(() => element.classList.remove('succeeded'), 2000); + this._recorder.injectedScript.builtinSetTimeout(() => element.classList.remove('succeeded'), 2000); } private _hideOverlay() { @@ -1312,7 +1312,7 @@ interface Embedder { export class PollingRecorder implements RecorderDelegate { private _recorder: Recorder; private _embedder: Embedder; - private _pollRecorderModeTimer: NodeJS.Timeout | undefined; + private _pollRecorderModeTimer: number | undefined; constructor(injectedScript: InjectedScript) { this._recorder = new Recorder(injectedScript); @@ -1333,7 +1333,7 @@ export class PollingRecorder implements RecorderDelegate { clearTimeout(this._pollRecorderModeTimer); const state = await this._embedder.__pw_recorderState().catch(() => {}); if (!state) { - this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod); + this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); return; } const win = this._recorder.document.defaultView!; @@ -1343,7 +1343,7 @@ export class PollingRecorder implements RecorderDelegate { state.actionPoint = undefined; } this._recorder.setUIState(state, this); - this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod); + this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); } async performAction(action: actions.Action) { diff --git a/packages/playwright-core/src/server/injected/utilityScript.ts b/packages/playwright-core/src/server/injected/utilityScript.ts index 92571ad8a2..e8dbbc6ba1 100644 --- a/packages/playwright-core/src/server/injected/utilityScript.ts +++ b/packages/playwright-core/src/server/injected/utilityScript.ts @@ -17,17 +17,20 @@ import { serializeAsCallArgument, parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers'; export class UtilityScript { + constructor(isUnderTest: boolean) { + if (isUnderTest) + this._setBuiltins(); + } + serializeAsCallArgument = serializeAsCallArgument; parseEvaluationResultValue = parseEvaluationResultValue; - evaluate(isFunction: boolean | undefined, returnByValue: boolean, exposeUtilityScript: boolean | undefined, expression: string, argCount: number, ...argsAndHandles: any[]) { + evaluate(isFunction: boolean | undefined, returnByValue: boolean, expression: string, argCount: number, ...argsAndHandles: any[]) { const args = argsAndHandles.slice(0, argCount); const handles = argsAndHandles.slice(argCount); const parameters = []; for (let i = 0; i < args.length; i++) parameters[i] = this.parseEvaluationResultValue(args[i], handles); - if (exposeUtilityScript) - parameters.unshift(this); // eslint-disable-next-line no-restricted-globals let result = globalThis.eval(expression); @@ -71,4 +74,47 @@ export class UtilityScript { } return safeJson(value); } + + private _setBuiltins() { + // eslint-disable-next-line no-restricted-globals + const window = (globalThis as any); + window.builtinSetTimeout = (callback: Function, timeout: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.setTimeout(callback, timeout); + return setTimeout(callback, timeout); + }; + + window.builtinClearTimeout = (id: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.clearTimeout(id); + return clearTimeout(id); + }; + + window.builtinSetInterval = (callback: Function, timeout: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.setInterval(callback, timeout); + return setInterval(callback, timeout); + }; + + window.builtinClearInterval = (id: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.clearInterval(id); + return clearInterval(id); + }; + + window.builtinRequestAnimationFrame = (callback: FrameRequestCallback) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.requestAnimationFrame(callback); + return requestAnimationFrame(callback); + }; + + window.builtinCancelAnimationFrame = (id: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.cancelAnimationFrame(id); + return cancelAnimationFrame(id); + }; + + window.builtinDate = window.__pwFakeTimers?.builtin.Date || Date; + window.builtinPerformance = window.__pwFakeTimers?.builtin.performance || performance; + } } diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index a1c52cc42b..663df78b5b 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -20,6 +20,7 @@ import { serializeAsCallArgument } from './isomorphic/utilityScriptSerializers'; import type { UtilityScript } from './injected/utilityScript'; import { SdkObject } from './instrumentation'; import { LongStandingScope } from '../utils/manualPromise'; +import { isUnderTest } from '../utils'; export type ObjectId = string; export type RemoteObject = { @@ -118,7 +119,7 @@ export class ExecutionContext extends SdkObject { (() => { const module = {}; ${utilityScriptSource.source} - return new (module.exports.UtilityScript())(); + return new (module.exports.UtilityScript())(${isUnderTest()}); })();`; this._utilityScriptPromise = this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', 'UtilityScript', objectId))); } @@ -257,7 +258,7 @@ export async function evaluate(context: ExecutionContext, returnByValue: boolean return evaluateExpression(context, String(pageFunction), { returnByValue, isFunction: typeof pageFunction === 'function' }, ...args); } -export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean, exposeUtilityScript?: boolean }, ...args: any[]): Promise { +export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean }, ...args: any[]): Promise { const utilityScript = await context.utilityScript(); expression = normalizeEvaluationExpression(expression, options.isFunction); const handles: (Promise)[] = []; @@ -290,7 +291,7 @@ export async function evaluateExpression(context: ExecutionContext, expression: } // See UtilityScript for arguments. - const utilityScriptValues = [options.isFunction, options.returnByValue, options.exposeUtilityScript, expression, args.length, ...args]; + const utilityScriptValues = [options.isFunction, options.returnByValue, expression, args.length, ...args]; const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; try { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 82538f2b50..2ed2bf7c97 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17274,8 +17274,7 @@ export interface Clock { /** * An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: - * ['setTimeout'] })` will fake only `setTimeout()`. By default, `setTimeout`, `clearTimeout`, `setInterval`, - * `clearInterval` and `Date` are faked. + * ['setTimeout'] })` will fake only `setTimeout()`. By default, all the methods are faked. */ toFake?: Array<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">; }): Promise; diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 8308477701..d5dd5c3e33 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -2612,12 +2612,10 @@ export type FrameDispatchEventResult = void; export type FrameEvaluateExpressionParams = { expression: string, isFunction?: boolean, - exposeUtilityScript?: boolean, arg: SerializedArgument, }; export type FrameEvaluateExpressionOptions = { isFunction?: boolean, - exposeUtilityScript?: boolean, }; export type FrameEvaluateExpressionResult = { value: SerializedValue, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 4151cf7104..8056debdcd 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1922,7 +1922,6 @@ Frame: parameters: expression: string isFunction: boolean? - exposeUtilityScript: boolean? arg: SerializedArgument returns: value: SerializedValue diff --git a/tests/assets/input/handle-locator.html b/tests/assets/input/handle-locator.html index f8f2111c91..a42849e951 100644 --- a/tests/assets/input/handle-locator.html +++ b/tests/assets/input/handle-locator.html @@ -57,7 +57,7 @@ }; if (interstitial.classList.contains('timeout')) - setTimeout(closeInterstitial, 3000); + builtinSetTimeout(closeInterstitial, 3000); else closeInterstitial(); }); diff --git a/tests/config/baseTest.ts b/tests/config/baseTest.ts index a71878c1c1..b585f28e56 100644 --- a/tests/config/baseTest.ts +++ b/tests/config/baseTest.ts @@ -41,3 +41,16 @@ export function step( } return replacementMethod; } + +declare global { + interface Window { + builtinSetTimeout: WindowOrWorkerGlobalScope['setTimeout'], + builtinClearTimeout: WindowOrWorkerGlobalScope['setTimeout'], + builtinSetInterval: WindowOrWorkerGlobalScope['setInterval'], + builtinClearInterval: WindowOrWorkerGlobalScope['clearInterval'], + builtinRequestAnimationFrame: AnimationFrameProvider['requestAnimationFrame'], + builtinCancelAnimationFrame: AnimationFrameProvider['cancelAnimationFrame'], + builtinPerformance: WindowOrWorkerGlobalScope['performance'], + builtinDate: typeof Date, + } +} diff --git a/tests/library/browsercontext-add-cookies.spec.ts b/tests/library/browsercontext-add-cookies.spec.ts index f35ea8b327..311084a837 100644 --- a/tests/library/browsercontext-add-cookies.spec.ts +++ b/tests/library/browsercontext-add-cookies.spec.ts @@ -391,6 +391,7 @@ it('should(not) block third party cookies', async ({ context, page, server, brow it('should not block third party SameSite=None cookies', async ({ contextFactory, httpsServer, browserName }) => { it.skip(browserName === 'webkit', 'No third party cookies in WebKit'); + it.skip(!!process.env.PW_FREEZE_TIME); const context = await contextFactory({ ignoreHTTPSErrors: true, }); diff --git a/tests/library/browsercontext-events.spec.ts b/tests/library/browsercontext-events.spec.ts index 4ad1ccc1f0..f197436bdc 100644 --- a/tests/library/browsercontext-events.spec.ts +++ b/tests/library/browsercontext-events.spec.ts @@ -46,7 +46,7 @@ test('console event should work in popup 2', async ({ page, browserName }) => { const [, message, popup] = await Promise.all([ page.evaluate(async () => { const win = window.open('javascript:console.log("hello")')!; - await new Promise(f => setTimeout(f, 0)); + await new Promise(f => window.builtinSetTimeout(f, 0)); win.close(); }), page.context().waitForEvent('console', msg => msg.type() === 'log'), diff --git a/tests/library/browsertype-launch.spec.ts b/tests/library/browsertype-launch.spec.ts index bdd5128dd1..011bc8a50d 100644 --- a/tests/library/browsertype-launch.spec.ts +++ b/tests/library/browsertype-launch.spec.ts @@ -24,7 +24,7 @@ it('should reject all promises when browser is closed', async ({ browserType }) const page = await (await browser.newContext()).newPage(); let error: Error | undefined; const neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e); - await page.evaluate(() => new Promise(f => setTimeout(f, 0))); + await page.evaluate(() => new Promise(f => window.builtinSetTimeout(f, 0))); await browser.close(); await neverResolves; // WebKit under task-set -c 1 is giving browser, rest are giving target. diff --git a/tests/library/chromium/css-coverage.spec.ts b/tests/library/chromium/css-coverage.spec.ts index 8dcb42d0b5..229f8baaa1 100644 --- a/tests/library/chromium/css-coverage.spec.ts +++ b/tests/library/chromium/css-coverage.spec.ts @@ -135,7 +135,7 @@ it('should work with a recently loaded stylesheet', async function({ page, serve link.href = url; document.head.appendChild(link); await new Promise(x => link.onload = x); - await new Promise(f => requestAnimationFrame(f)); + await new Promise(f => window.builtinRequestAnimationFrame(f)); }, server.PREFIX + '/csscoverage/stylesheet1.css'); const coverage = await page.coverage.stopCSSCoverage(); expect(coverage.length).toBe(1); diff --git a/tests/library/chromium/js-coverage.spec.ts b/tests/library/chromium/js-coverage.spec.ts index 09fcb69f11..f36e6cbcbb 100644 --- a/tests/library/chromium/js-coverage.spec.ts +++ b/tests/library/chromium/js-coverage.spec.ts @@ -43,6 +43,7 @@ it('should ignore eval() scripts by default', async function({ page, server }) { }); it('shouldn\'t ignore eval() scripts if reportAnonymousScripts is true', async function({ page, server }) { + it.skip(!!process.env.PW_FREEZE_TIME); await page.coverage.startJSCoverage({ reportAnonymousScripts: true }); await page.goto(server.PREFIX + '/jscoverage/eval.html'); const coverage = await page.coverage.stopJSCoverage(); diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index 22cb8033b7..f6e4a28488 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -156,6 +156,7 @@ it('should(not) block third party cookies', async ({ page, server, allowsThirdPa it('should not block third party SameSite=None cookies', async ({ httpsServer, browserName, browser }) => { it.skip(browserName === 'webkit', 'No third party cookies in WebKit'); + it.skip(!!process.env.PW_FREEZE_TIME); const page = await browser.newPage({ ignoreHTTPSErrors: true, }); diff --git a/tests/library/popup.spec.ts b/tests/library/popup.spec.ts index 52e13c5175..56865bdb53 100644 --- a/tests/library/popup.spec.ts +++ b/tests/library/popup.spec.ts @@ -136,9 +136,9 @@ it('should use viewport size from window features', async function({ browser, se page.evaluate(async () => { const win = window.open(window.location.href, 'Title', 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=300,top=0,left=0'); await new Promise(resolve => { - const interval = setInterval(() => { + const interval = window.builtinSetInterval(() => { if (win.innerWidth === 600 && win.innerHeight === 300) { - clearInterval(interval); + window.builtinClearInterval(interval); resolve(); } }, 10); @@ -281,8 +281,8 @@ async function waitForRafs(page: Page, count: number): Promise { if (!count) resolve(); else - requestAnimationFrame(onRaf); + window.builtinRequestAnimationFrame(onRaf); }; - requestAnimationFrame(onRaf); + window.builtinRequestAnimationFrame(onRaf); }), count); } \ No newline at end of file diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 54988eb73c..e0c5055692 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -44,9 +44,9 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s console.error('Error'); return new Promise(f => { // Generate exception. - setTimeout(() => { + window.builtinSetTimeout(() => { // And then resolve. - setTimeout(() => f('return ' + a), 0); + window.builtinSetTimeout(() => f('return ' + a), 0); throw new Error('Unhandled exception'); }, 0); }); diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index e0477bd993..c82db377aa 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -426,7 +426,7 @@ for (const params of [ // Make sure we have a chance to paint. for (let i = 0; i < 10; ++i) { await page.setContent(''); - await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.evaluate(() => new Promise(window.builtinRequestAnimationFrame)); } await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); @@ -709,7 +709,7 @@ test('should not flush console events', async ({ context, page, mode }, testInfo }); await page.evaluate(() => { - setTimeout(() => { + window.builtinSetTimeout(() => { for (let i = 0; i < 100; ++i) console.log('hello ' + i); }, 10); @@ -749,7 +749,7 @@ test('should flush console events on tracing stop', async ({ context, page }, te }); }); await page.evaluate(() => { - setTimeout(() => { + window.builtinSetTimeout(() => { for (let i = 0; i < 100; ++i) console.log('hello ' + i); }); diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index 7229fb10ac..dd73efa876 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -829,8 +829,8 @@ async function waitForRafs(page: Page, count: number): Promise { if (!count) resolve(); else - requestAnimationFrame(onRaf); + window.builtinRequestAnimationFrame(onRaf); }; - requestAnimationFrame(onRaf); + window.builtinRequestAnimationFrame(onRaf); }), count); } diff --git a/tests/page/elementhandle-screenshot.spec.ts b/tests/page/elementhandle-screenshot.spec.ts index 3f172c9253..042fb7c565 100644 --- a/tests/page/elementhandle-screenshot.spec.ts +++ b/tests/page/elementhandle-screenshot.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; import { verifyViewport } from '../config/utils'; import path from 'path'; import fs from 'fs'; @@ -207,8 +207,7 @@ it.describe('element screenshot', () => { done = true; return buffer; }); - for (let i = 0; i < 10; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(f))); + await rafraf(page, 10); expect(done).toBe(false); await elementHandle.evaluate(e => e.style.visibility = 'visible'); const screenshot = await promise; @@ -233,10 +232,8 @@ it.describe('element screenshot', () => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const elementHandle = await page.$('.box:nth-of-type(3)'); - await elementHandle.evaluate(e => { - e.classList.add('animation'); - return new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f))); - }); + await elementHandle.evaluate(e => e.classList.add('animation')); + await rafraf(page); const screenshot = await elementHandle.screenshot(); expect(screenshot).toMatchSnapshot('screenshot-element-bounding-box.png'); }); diff --git a/tests/page/elementhandle-scroll-into-view.spec.ts b/tests/page/elementhandle-scroll-into-view.spec.ts index 5fe53d0722..e2a401ccea 100644 --- a/tests/page/elementhandle-scroll-into-view.spec.ts +++ b/tests/page/elementhandle-scroll-into-view.spec.ts @@ -48,7 +48,7 @@ async function testWaiting(page, after) { const div = await page.$('div'); let done = false; const promise = div.scrollIntoViewIfNeeded().then(() => done = true); - await page.evaluate(() => new Promise(f => setTimeout(f, 1000))); + await page.waitForTimeout(1000); expect(done).toBe(false); await div.evaluate(after); await promise; diff --git a/tests/page/elementhandle-select-text.spec.ts b/tests/page/elementhandle-select-text.spec.ts index bc3630249b..b916feb3fc 100644 --- a/tests/page/elementhandle-select-text.spec.ts +++ b/tests/page/elementhandle-select-text.spec.ts @@ -65,7 +65,7 @@ it('should wait for visible', async ({ page, server }) => { await textarea.evaluate(e => e.style.display = 'none'); let done = false; const promise = textarea.selectText({ timeout: 3000 }).then(() => done = true); - await page.evaluate(() => new Promise(f => setTimeout(f, 1000))); + await page.waitForTimeout(1000); expect(done).toBe(false); await textarea.evaluate(e => e.style.display = 'block'); await promise; diff --git a/tests/page/elementhandle-wait-for-element-state.spec.ts b/tests/page/elementhandle-wait-for-element-state.spec.ts index e943d229d1..bbccf620c7 100644 --- a/tests/page/elementhandle-wait-for-element-state.spec.ts +++ b/tests/page/elementhandle-wait-for-element-state.spec.ts @@ -15,12 +15,10 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import type { Page } from '@playwright/test'; +import { test as it, expect, rafraf } from './pageTest'; -async function giveItAChanceToResolve(page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToResolve = (page: Page) => rafraf(page, 5); it('should wait for visible', async ({ page }) => { await page.setContent(`
content
`); @@ -124,7 +122,7 @@ it('should wait for stable position', async ({ page, server, browserName, platfo button.style.marginLeft = '20000px'; }); // rafraf for Firefox to kick in the animation. - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + await rafraf(page); let done = false; const promise = button.waitForElementState('stable').then(() => done = true); await giveItAChanceToResolve(page); diff --git a/tests/page/page-click-timeout-4.spec.ts b/tests/page/page-click-timeout-4.spec.ts index 0ee7942eef..d00cc62c37 100644 --- a/tests/page/page-click-timeout-4.spec.ts +++ b/tests/page/page-click-timeout-4.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; it('should timeout waiting for stable position', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -25,7 +25,7 @@ it('should timeout waiting for stable position', async ({ page, server }) => { button.style.marginLeft = '200px'; }); // rafraf for Firefox to kick in the animation. - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + await rafraf(page); const error = await button.click({ timeout: 3000 }).catch(e => e); expect(error.message).toContain('elementHandle.click: Timeout 3000ms exceeded.'); expect(error.message).toContain('waiting for element to be visible, enabled and stable'); diff --git a/tests/page/page-click.spec.ts b/tests/page/page-click.spec.ts index a774294c04..aa02d210eb 100644 --- a/tests/page/page-click.spec.ts +++ b/tests/page/page-click.spec.ts @@ -15,13 +15,11 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; import { attachFrame, detachFrame } from '../config/utils'; +import type { Page } from '@playwright/test'; -async function giveItAChanceToClick(page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToClick = (page: Page) => rafraf(page, 5); it('should click the button @smoke', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -456,7 +454,7 @@ it('should wait for stable position', async ({ page, server }) => { document.body.style.margin = '0'; }); // rafraf for Firefox to kick in the animation. - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + await rafraf(page); await page.click('button'); expect(await page.evaluate(() => window['result'])).toBe('Clicked'); expect(await page.evaluate('pageX')).toBe(300); @@ -1072,7 +1070,7 @@ it('ensure events are dispatched in the individual tasks', async ({ page, browse function onClick(name) { console.log(`click ${name}`); - setTimeout(function() { + window.builtinSetTimeout(function() { console.log(`timeout ${name}`); }, 0); diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index 185443d4c7..7e001e6641 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -16,6 +16,8 @@ import { test, expect } from './pageTest'; +test.skip(!!process.env.PW_FREEZE_TIME); + declare global { interface Window { stub: (param?: any) => void diff --git a/tests/page/page-close.spec.ts b/tests/page/page-close.spec.ts index 5c7d24110a..c1123b8190 100644 --- a/tests/page/page-close.spec.ts +++ b/tests/page/page-close.spec.ts @@ -20,7 +20,8 @@ import { test as it, expect } from './pageTest'; it.skip(({ isWebView2 }) => isWebView2, 'Page.close() is not supported in WebView2'); it('should close page with active dialog', async ({ page }) => { - await page.setContent(``); + await page.evaluate('"trigger builtinSetTimeout"'); + await page.setContent(``); void page.click('button').catch(() => {}); await page.waitForEvent('dialog'); await page.close(); diff --git a/tests/page/page-dialog.spec.ts b/tests/page/page-dialog.spec.ts index fdaed252b8..6149989081 100644 --- a/tests/page/page-dialog.spec.ts +++ b/tests/page/page-dialog.spec.ts @@ -67,7 +67,7 @@ it('should dismiss the confirm prompt', async ({ page }) => { it('should be able to close context with open alert', async ({ page }) => { const alertPromise = page.waitForEvent('dialog'); await page.evaluate(() => { - setTimeout(() => alert('hello'), 0); + window.builtinSetTimeout(() => alert('hello'), 0); }); await alertPromise; }); diff --git a/tests/page/page-dispatchevent.spec.ts b/tests/page/page-dispatchevent.spec.ts index 3486eb3b1e..d4d780c190 100644 --- a/tests/page/page-dispatchevent.spec.ts +++ b/tests/page/page-dispatchevent.spec.ts @@ -92,7 +92,7 @@ it('should dispatch click when node is added in shadow dom', async ({ page, serv div.attachShadow({ mode: 'open' }); document.body.appendChild(div); }); - await page.evaluate(() => new Promise(f => setTimeout(f, 100))); + await page.waitForTimeout(100); await page.evaluate(() => { const span = document.createElement('span'); span.textContent = 'Hello from shadow'; diff --git a/tests/page/page-drag.spec.ts b/tests/page/page-drag.spec.ts index 58f82ba2d3..f7b60baf8c 100644 --- a/tests/page/page-drag.spec.ts +++ b/tests/page/page-drag.spec.ts @@ -357,7 +357,7 @@ it('should report event.buttons', async ({ page, browserName }) => { function onEvent(event) { logs.push({ type: event.type, buttons: event.buttons }); } - await new Promise(requestAnimationFrame); + await new Promise(window.builtinRequestAnimationFrame); return logs; }); await page.mouse.move(20, 20); diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index 5e101f1b7a..8414d23b2c 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -349,10 +349,10 @@ it('should properly serialize null fields', async ({ page }) => { it('should properly serialize PerformanceMeasure object', async ({ page }) => { expect(await page.evaluate(() => { - window.performance.mark('start'); - window.performance.mark('end'); - window.performance.measure('my-measure', 'start', 'end'); - return performance.getEntriesByType('measure'); + window.builtinPerformance.mark('start'); + window.builtinPerformance.mark('end'); + window.builtinPerformance.measure('my-measure', 'start', 'end'); + return window.builtinPerformance.getEntriesByType('measure'); })).toEqual([{ duration: expect.any(Number), entryType: 'measure', @@ -362,6 +362,8 @@ it('should properly serialize PerformanceMeasure object', async ({ page }) => { }); it('should properly serialize window.performance object', async ({ page }) => { + it.skip(!!process.env.PW_FREEZE_TIME); + expect(await page.evaluate(() => performance)).toEqual({ 'navigation': { 'redirectCount': 0, @@ -760,16 +762,6 @@ it('should work with overridden URL/Date/RegExp', async ({ page, server }) => { } }); -it('should expose utilityScript', async ({ page }) => { - const result = await (page.mainFrame() as any)._evaluateExposeUtilityScript((utilityScript, { a }) => { - return { utils: 'parseEvaluationResultValue' in utilityScript, a }; - }, { a: 42 }); - expect(result).toEqual({ - a: 42, - utils: true, - }); -}); - it('should work with Array.from/map', async ({ page }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28520' }); expect(await page.evaluate(() => { diff --git a/tests/page/page-event-console.spec.ts b/tests/page/page-event-console.spec.ts index 3b5f652fc9..ee47e81e02 100644 --- a/tests/page/page-event-console.spec.ts +++ b/tests/page/page-event-console.spec.ts @@ -97,9 +97,9 @@ it('should format the message correctly with time/timeLog/timeEnd', async ({ pag page.on('console', msg => messages.push(msg)); await page.evaluate(async () => { console.time('foo time'); - await new Promise(x => setTimeout(x, 100)); + await new Promise(x => window.builtinSetTimeout(x, 100)); console.timeLog('foo time'); - await new Promise(x => setTimeout(x, 100)); + await new Promise(x => window.builtinSetTimeout(x, 100)); console.timeEnd('foo time'); }); expect(messages.length).toBe(2); diff --git a/tests/page/page-event-pageerror.spec.ts b/tests/page/page-event-pageerror.spec.ts index 2193bec598..1e91ce7b7f 100644 --- a/tests/page/page-event-pageerror.spec.ts +++ b/tests/page/page-event-pageerror.spec.ts @@ -70,7 +70,7 @@ it('should contain the Error.name property', async ({ page }) => { const [error] = await Promise.all([ page.waitForEvent('pageerror'), page.evaluate(() => { - setTimeout(() => { + window.builtinSetTimeout(() => { const error = new Error('my-message'); error.name = 'my-name'; throw error; @@ -85,7 +85,7 @@ it('should support an empty Error.name property', async ({ page }) => { const [error] = await Promise.all([ page.waitForEvent('pageerror'), page.evaluate(() => { - setTimeout(() => { + window.builtinSetTimeout(() => { const error = new Error('my-message'); error.name = ''; throw error; @@ -106,7 +106,9 @@ it('should handle odd values', async ({ page }) => { for (const [value, message] of cases) { const [error] = await Promise.all([ page.waitForEvent('pageerror'), - page.evaluate(value => setTimeout(() => { throw value; }, 0), value), + page.evaluate(value => { + window.builtinSetTimeout(() => { throw value; }, 0); + }, value), ]); expect(error.message).toBe(message); } @@ -115,7 +117,9 @@ it('should handle odd values', async ({ page }) => { it('should handle object', async ({ page, browserName }) => { const [error] = await Promise.all([ page.waitForEvent('pageerror'), - page.evaluate(() => setTimeout(() => { throw {}; }, 0)), + page.evaluate(() => { + window.builtinSetTimeout(() => { throw {}; }, 0); + }), ]); expect(error.message).toBe(browserName === 'chromium' ? 'Object' : '[object Object]'); }); @@ -123,7 +127,9 @@ it('should handle object', async ({ page, browserName }) => { it('should handle window', async ({ page, browserName }) => { const [error] = await Promise.all([ page.waitForEvent('pageerror'), - page.evaluate(() => setTimeout(() => { throw window; }, 0)), + page.evaluate(() => { + window.builtinSetTimeout(() => { throw window; }, 0); + }), ]); expect(error.message).toBe(browserName === 'chromium' ? 'Window' : '[object Window]'); }); diff --git a/tests/page/page-expose-function.spec.ts b/tests/page/page-expose-function.spec.ts index c084e5ea21..ece4a602df 100644 --- a/tests/page/page-expose-function.spec.ts +++ b/tests/page/page-expose-function.spec.ts @@ -230,7 +230,7 @@ it('should not result in unhandled rejection', async ({ page, isAndroid, isWebVi await page.close(); }); await page.evaluate(() => { - setTimeout(() => (window as any).foo(), 0); + window.builtinSetTimeout(() => (window as any).foo(), 0); return undefined; }); await closedPromise; diff --git a/tests/page/page-fill.spec.ts b/tests/page/page-fill.spec.ts index 0704de327a..88cd33d407 100644 --- a/tests/page/page-fill.spec.ts +++ b/tests/page/page-fill.spec.ts @@ -15,12 +15,10 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import type { Page } from '@playwright/test'; +import { test as it, expect, rafraf } from './pageTest'; -async function giveItAChanceToFill(page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToFill = (page: Page) => rafraf(page, 5); it('should fill textarea @smoke', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/textarea.html'); diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 3deff339a3..6703d84498 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -481,6 +481,7 @@ it('js redirect overrides url bar navigation ', async ({ page, server, browserNa it('should succeed on url bar navigation when there is pending navigation', async ({ page, server, browserName }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/21574' }); + it.skip(!!process.env.PW_FREEZE_TIME); server.setRoute('/a', (req, res) => { res.writeHead(200, { 'content-type': 'text/html' }); res.end(` @@ -509,7 +510,7 @@ it('should succeed on url bar navigation when there is pending navigation', asyn events.push('finished c'); }); await page.goto(server.PREFIX + '/a'); - await new Promise(f => setTimeout(f, 1000)); + await page.waitForTimeout(1000); const error = await page.goto(server.PREFIX + '/b').then(r => null, e => e); const expectEvents = ['started c', 'started b', 'finished c', 'finished b']; await expect(() => expect(events).toEqual(expectEvents)).toPass({ timeout: 5000 }); @@ -753,6 +754,7 @@ it('should properly wait for load', async ({ page, server, browserName }) => { it('should not resolve goto upon window.stop()', async ({ browserName, page, server }) => { it.fixme(browserName === 'firefox', 'load/domcontentloaded events are flaky'); + it.skip(!!process.env.PW_FREEZE_TIME); let response; server.setRoute('/module.js', (req, res) => { @@ -795,6 +797,7 @@ it('should return when navigation is committed if commit is specified', async ({ }); it('should wait for load when iframe attaches and detaches', async ({ page, server }) => { + it.skip(!!process.env.PW_FREEZE_TIME); server.setRoute('/empty.html', (req, res) => { res.writeHead(200, { 'content-type': 'text/html' }); res.end(` diff --git a/tests/page/page-history.spec.ts b/tests/page/page-history.spec.ts index 7e243c9b82..2bc01512dd 100644 --- a/tests/page/page-history.spec.ts +++ b/tests/page/page-history.spec.ts @@ -245,6 +245,7 @@ it('page.goForward during renderer-initiated navigation', async ({ page, server it('regression test for issue 20791', async ({ page, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20791' }); + it.skip(!!process.env.PW_FREEZE_TIME); server.setRoute('/iframe.html', (req, res) => { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); // iframe access parent frame to log a value from it. diff --git a/tests/page/page-mouse.spec.ts b/tests/page/page-mouse.spec.ts index cf93bd8bf1..5ae7de6508 100644 --- a/tests/page/page-mouse.spec.ts +++ b/tests/page/page-mouse.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; function dimensions() { const rect = document.querySelector('textarea').getBoundingClientRect(); @@ -150,7 +150,7 @@ it('should select the text with mouse', async ({ page, server }) => { const text = 'This is the text that we are going to try to select. Let\'s see how it goes.'; await page.keyboard.type(text); // Firefox needs an extra frame here after typing or it will fail to set the scrollTop - await page.evaluate(() => new Promise(requestAnimationFrame)); + await rafraf(page); await page.evaluate(() => document.querySelector('textarea').scrollTop = 0); const { x, y } = await page.evaluate(dimensions); await page.mouse.move(x + 2, y + 2); diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index 529bb99a74..7f374ac59b 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -16,7 +16,7 @@ */ import os from 'os'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; import { verifyViewport, attachFrame } from '../config/utils'; import type { Route } from 'playwright-core'; import path from 'path'; @@ -589,14 +589,6 @@ it.describe('page screenshot', () => { }); }); -async function rafraf(page) { - // Do a double raf since single raf does not - // actually guarantee a new animation frame. - await page.evaluate(() => new Promise(x => { - requestAnimationFrame(() => requestAnimationFrame(x)); - })); -} - declare global { interface Window { animation?: Animation; @@ -732,9 +724,9 @@ it.describe('page screenshot animations', () => { const div = page.locator('div'); await div.evaluate(el => { el.addEventListener('transitionend', () => { - const time = Date.now(); + const time = window.builtinDate.now(); // Block main thread for 200ms, emulating heavy layout. - while (Date.now() - time < 200) {} + while (window.builtinDate.now() - time < 200) {} const h1 = document.createElement('h1'); h1.textContent = 'woof-woof'; document.body.append(h1); diff --git a/tests/page/page-select-option.spec.ts b/tests/page/page-select-option.spec.ts index 522c6270ff..0faeee5f51 100644 --- a/tests/page/page-select-option.spec.ts +++ b/tests/page/page-select-option.spec.ts @@ -15,12 +15,10 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import type { Page } from '@playwright/test'; +import { test as it, expect, rafraf } from './pageTest'; -async function giveItAChanceToResolve(page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToResolve = (page: Page) => rafraf(page, 5); it('should select single option @smoke', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/select.html'); diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index af6197c1c7..5777b1087a 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -399,7 +399,7 @@ it('should prioritize exact timeout over default timeout', async ({ page, playwr it('should work with no timeout', async ({ page, server }) => { const [chooser] = await Promise.all([ page.waitForEvent('filechooser', { timeout: 0 }), - page.evaluate(() => setTimeout(() => { + page.evaluate(() => window.builtinSetTimeout(() => { const el = document.createElement('input'); el.type = 'file'; el.click(); diff --git a/tests/page/page-wait-for-function.spec.ts b/tests/page/page-wait-for-function.spec.ts index 7f93f7b122..88b6d7cb14 100644 --- a/tests/page/page-wait-for-function.spec.ts +++ b/tests/page/page-wait-for-function.spec.ts @@ -44,10 +44,10 @@ it('should poll on interval', async ({ page, server }) => { const polling = 100; const timeDelta = await page.waitForFunction(() => { if (!window['__startTime']) { - window['__startTime'] = Date.now(); + window['__startTime'] = window.builtinDate.now(); return false; } - return Date.now() - window['__startTime']; + return window.builtinDate.now() - window['__startTime']; }, {}, { polling }); expect(await timeDelta.jsonValue()).not.toBeLessThan(polling); }); diff --git a/tests/page/page-wait-for-navigation.spec.ts b/tests/page/page-wait-for-navigation.spec.ts index 4d2eb8177f..d34251e977 100644 --- a/tests/page/page-wait-for-navigation.spec.ts +++ b/tests/page/page-wait-for-navigation.spec.ts @@ -255,7 +255,7 @@ it('should fail when frame detaches', async ({ page, server }) => { frame.waitForNavigation().catch(e => e), page.$eval('iframe', frame => { frame.contentWindow.location.href = '/one-style.html'; }), // Make sure policy checks pass and navigation actually begins before removing the frame to avoid other errors - server.waitForRequest('/one-style.css').then(() => page.$eval('iframe', frame => setTimeout(() => frame.remove(), 0))) + server.waitForRequest('/one-style.css').then(() => page.$eval('iframe', frame => window.builtinSetTimeout(() => frame.remove(), 0))) ]); expect(error.message).toContain('waiting for navigation until "load"'); expect(error.message).toContain('frame was detached'); diff --git a/tests/page/page-wait-for-request.spec.ts b/tests/page/page-wait-for-request.spec.ts index 3ba6e9304f..e7ae14ba66 100644 --- a/tests/page/page-wait-for-request.spec.ts +++ b/tests/page/page-wait-for-request.spec.ts @@ -70,7 +70,7 @@ it('should work with no timeout', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); const [request] = await Promise.all([ page.waitForRequest(server.PREFIX + '/digits/2.png', { timeout: 0 }), - page.evaluate(() => setTimeout(() => { + page.evaluate(() => window.builtinSetTimeout(() => { void fetch('/digits/1.png'); void fetch('/digits/2.png'); void fetch('/digits/3.png'); diff --git a/tests/page/page-wait-for-response.spec.ts b/tests/page/page-wait-for-response.spec.ts index b5735e1a8c..ccfc18c9eb 100644 --- a/tests/page/page-wait-for-response.spec.ts +++ b/tests/page/page-wait-for-response.spec.ts @@ -108,7 +108,7 @@ it('should work with no timeout', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); const [response] = await Promise.all([ page.waitForResponse(server.PREFIX + '/digits/2.png', { timeout: 0 }), - page.evaluate(() => setTimeout(() => { + page.evaluate(() => window.builtinSetTimeout(() => { void fetch('/digits/1.png'); void fetch('/digits/2.png'); void fetch('/digits/3.png'); diff --git a/tests/page/page-wait-for-selector-1.spec.ts b/tests/page/page-wait-for-selector-1.spec.ts index 3ab3e92b73..6ee01e29e8 100644 --- a/tests/page/page-wait-for-selector-1.spec.ts +++ b/tests/page/page-wait-for-selector-1.spec.ts @@ -16,12 +16,11 @@ */ import type { Frame } from '@playwright/test'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; import { attachFrame, detachFrame } from '../config/utils'; async function giveItTimeToLog(frame: Frame) { - await frame.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); - await frame.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + await rafraf(frame, 2); } const addElement = (tag: string) => document.body.appendChild(document.createElement(tag)); @@ -189,7 +188,7 @@ it('should resolve promise when node is added in shadow dom', async ({ page, ser div.attachShadow({ mode: 'open' }); document.body.appendChild(div); }); - await page.evaluate(() => new Promise(f => setTimeout(f, 100))); + await page.waitForTimeout(100); await page.evaluate(() => { const span = document.createElement('span'); span.textContent = 'Hello from shadow'; diff --git a/tests/page/pageTest.ts b/tests/page/pageTest.ts index 54f3ec9233..533d901037 100644 --- a/tests/page/pageTest.ts +++ b/tests/page/pageTest.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { TestType } from '@playwright/test'; +import type { Frame, Page, TestType } from '@playwright/test'; import type { PlatformWorkerFixtures } from '../config/platformFixtures'; import type { TestModeTestFixtures, TestModeWorkerFixtures, TestModeWorkerOptions } from '../config/testModeFixtures'; import { androidTest } from '../android/androidTest'; @@ -35,3 +35,11 @@ if (process.env.PWPAGE_IMPL === 'webview2') impl = webView2Test; export const test = impl; + +export async function rafraf(target: Page | Frame, count = 1) { + for (let i = 0; i < count; i++) { + await target.evaluate(async () => { + await new Promise(f => window.builtinRequestAnimationFrame(() => window.builtinRequestAnimationFrame(f))); + }); + } +} diff --git a/tests/page/retarget.spec.ts b/tests/page/retarget.spec.ts index 4465643d77..65daf1e205 100644 --- a/tests/page/retarget.spec.ts +++ b/tests/page/retarget.spec.ts @@ -16,12 +16,9 @@ */ import type { Page } from '@playwright/test'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; -async function giveItAChanceToResolve(page: Page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToResolve = (page: Page) => rafraf(page, 5); it('element state checks should work as expected for label with zero-sized input', async ({ page, server }) => { await page.setContent(` diff --git a/tests/page/wheel.spec.ts b/tests/page/wheel.spec.ts index fb655c7e43..c8c634e16d 100644 --- a/tests/page/wheel.spec.ts +++ b/tests/page/wheel.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import type { Page } from 'playwright-core'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; it.skip(({ isAndroid }) => { return isAndroid; @@ -209,8 +209,7 @@ it('should work when the event is canceled', async ({ page }) => { document.querySelector('div').addEventListener('wheel', e => e.preventDefault()); }); // Give wheel listener a chance to propagate through all the layers in Firefox. - for (let i = 0; i < 10; i++) - await page.evaluate(() => new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))); + await rafraf(page, 10); await page.mouse.wheel(0, 100); await expectEvent(page, { deltaX: 0, diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts index d82e3b8f40..9e44804dcf 100644 --- a/tests/playwright-test/ui-mode-test-output.spec.ts +++ b/tests/playwright-test/ui-mode-test-output.spec.ts @@ -154,7 +154,7 @@ test('should format console messages in page', async ({ runUITest }, testInfo) = await expect(link).toHaveCSS('text-decoration', 'none solid rgb(0, 0, 255)'); }); -test('should stream console messages live', async ({ runUITest }, testInfo) => { +test('should stream console messages live', async ({ runUITest }) => { const { page } = await runUITest({ 'a.spec.ts': ` import { test, expect } from '@playwright/test'; @@ -162,7 +162,7 @@ test('should stream console messages live', async ({ runUITest }, testInfo) => { await page.setContent(''); const button = page.getByRole('button', { name: 'Click me' }); await button.evaluate(node => node.addEventListener('click', () => { - setTimeout(() => { console.log('I was clicked'); }, 1000); + builtinSetTimeout(() => { console.log('I was clicked'); }, 1000); })); console.log('I was logged'); await button.click(); From 4655a30bdd7977a1911a6482d6ad4c529f9a7949 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Sat, 1 Jun 2024 04:48:17 -0700 Subject: [PATCH 005/144] feat(webkit): roll to r2016 (#31114) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 263862e1eb..dbe8771f92 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2015", + "revision": "2016", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From fc6c67f5f9a1fb64fa8497c4116df5fdc4539001 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 1 Jun 2024 07:30:58 -0700 Subject: [PATCH 006/144] docs: start adding clock docs (#31111) --- docs/src/api/class-clock.md | 121 ++++++++++++++ docs/src/clock.md | 187 ++++++++++++++++++++++ packages/playwright-core/types/types.d.ts | 42 ++++- 3 files changed, 347 insertions(+), 3 deletions(-) create mode 100644 docs/src/clock.md diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 90c75de16a..d703c07b84 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -10,6 +10,49 @@ in all the pages and iframes is controlled by the same clock. Creates a clock and installs it globally. +**Usage** + +```js +await page.clock.install(); +await page.clock.install({ now }); +await page.clock.install({ now, toFake: ['Date'] }); +``` + +```python async +await page.clock.install() +await page.clock.install(now=now) +await page.clock.install(now=now, toFake=['Date']) +``` + +```python sync +page.clock.install() +page.clock.install(now=now) +page.clock.install(now=now, toFake=['Date']) +``` + +```java +page.clock().install(); +page.clock().install( + new Clock.InstallOptions() + .setNow(now)); +page.clock().install( + new Clock.InstallOptions() + .setNow(now) + .setToFake(new String[]{"Date"})); +``` + +```csharp +await page.Clock.InstallAsync(); +await page.Clock.InstallAsync( + new ClockInstallOptions { Now = now }); +await page.Clock.InstallAsync( + new ClockInstallOptions + { + Now = now, + ToFake = new[] { "Date" } + }); +``` + ### option: Clock.install.now * since: v1.45 - `now` <[int]|[Date]> @@ -50,6 +93,27 @@ in the real system time (default: 20). Advances the clock to the the moment of the first scheduled timer, firing it. +**Usage** + +```js +await page.clock.next(); +``` + +```python async +await page.clock.next() +``` + +```python sync +page.clock.next() +``` + +```java +page.clock().next(); +``` + +```csharp +await page.Clock.NextAsync(); +``` ## async method: Clock.jump * since: v1.45 @@ -57,6 +121,33 @@ Advances the clock to the the moment of the first scheduled timer, firing it. Advance the clock by jumping forward in time, firing callbacks at most once. Returns fake milliseconds since the unix epoch. This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers. +**Usage** + +```js +await page.clock.jump(1000); +await page.clock.jump('30:00'); +``` + +```python async +await page.clock.jump(1000); +await page.clock.jump('30:00') +``` + +```python sync +page.clock.jump(1000); +page.clock.jump('30:00') +``` + +```java +page.clock().jump(1000); +page.clock().jump("30:00"); +``` + +```csharp +await page.Clock.JumpAsync(1000); +await page.Clock.JumpAsync("30:00"); +``` + ### param: Clock.jump.time * since: v1.45 - `time` <[int]|[string]> @@ -68,6 +159,9 @@ Time may be the number of milliseconds to advance the clock by or a human-readab - returns: <[int]> Fake milliseconds since the unix epoch. Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. + +**Details** + This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or the delays in those timers. It runs a maximum of [`option: loopLimit`] times after which it assumes there is an infinite loop of timers and throws an error. @@ -87,6 +181,33 @@ This is useful when you want to run a test to completion, but the test recursive Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. +**Usage** + +```js +await page.clock.tick(1000); +await page.clock.tick('30:00'); +``` + +```python async +await page.clock.tick(1000); +await page.clock.tick('30:00') +``` + +```python sync +page.clock.tick(1000); +page.clock.tick('30:00') +``` + +```java +page.clock().tick(1000); +page.clock().tick("30:00"); +``` + +```csharp +await page.Clock.TickAsync(1000); +await page.Clock.TickAsync("30:00"); +``` + ### param: Clock.tick.time * since: v1.45 - `time` <[int]|[string]> diff --git a/docs/src/clock.md b/docs/src/clock.md new file mode 100644 index 0000000000..05a15e2496 --- /dev/null +++ b/docs/src/clock.md @@ -0,0 +1,187 @@ +--- +id: clock +title: "Clock" +--- + +## Introduction + +[`property: Page.clock`] overrides native global functions related to time allowing them to be manually controlled: + - `setTimeout` + - `clearTimeout` + - `setInterval` + - `clearInterval` + - `Date` + - `requestAnimationFrame` + - `cancelAnimationFrame` + - `requestIdleCallback` + +By default, the clock starts at the unix epoch (timestamp of 0), but you can override it using the `now` option. + +```js +await page.clock.install(); +await page.clock.install({ now: new Date('2020-02-02') }); +``` + +## Only fake Date.now + +```html card + + +``` + +```js +// Initialize clock with a specific time. +await page.clock.install({ + now: new Date('2024-01-01T10:00:00Z'), + toFake: ['Date'], +}); +await page.goto('http://localhost:3333'); +await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:00'); +``` + +```python async +# Initialize clock with a specific time. +await page.clock.install( + now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), + toFake=['Date'], +) +await page.goto('http://localhost:3333') +locator = page.get_by_test_id('my-time') +await expect(locator).to_have_value('2024-01-01T10:00') +``` + +```python sync +# Initialize clock with a specific time. +page.clock.install( + now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), + to_fake=['Date'], +) +page.goto('http://localhost:3333') +locator = page.get_by_test_id('my-time') +expect(locator).to_have_value('2024-01-01T10:00') +``` + +```java +// Initialize clock with a specific time. +page.clock().install( + new Clock.InstallOptions() + .setNow(Instant.parse("2024-01-01T10:00:00Z")) + .setToFake(new String[]{"Date"}) +); +page.navigate("http://localhost:3333"); +Locator locator = page.getByTestId("my-time"); +assertThat(locator).hasValue("2024-01-01T10:00"); +``` + +```csharp +// Initialize clock with a specific time. +await page.Clock.InstallAsync( + new ClockInstallOptions + { + Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), + ToFake = new[] { "Date" } + }); +await page.GotoAsync("http://localhost:3333"); +var locator = page.GetByTestId("my-time"); +await Expect(locator).ToHaveValueAsync("2024-01-01T10:00"); +``` + +## Assert page at different points in time + +```html card + + +``` + +```js +// Initialize clock with a specific time. +await page.clock.install({ now: new Date('2024-01-01T10:00:00Z') }); +await page.goto('http://localhost:3333'); +await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:00'); + +// Fast forward time 30 minutes without firing intermediate timers, as if the user +// closed and opened the lid of the laptop. +await page.clock.jump('30:00'); +await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:30'); +``` + +```python async +# Initialize clock with a specific time. +await page.clock.install( + now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), + toFake=['Date'], +) +await page.goto('http://localhost:3333') +locator = page.get_by_test_id('my-time') +await expect(locator).to_have_value('2024-01-01T10:00') + +# Fast forward time 30 minutes without firing intermediate timers, as if the user +# closed and opened the lid of the laptop. +await page.clock.jump('30:00') +await expect(locator).to_have_value('2024-01-01T10:30') +``` + +```python sync +# Initialize clock with a specific time. +page.clock.install( + now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), + to_fake=['Date'], +) +page.goto('http://localhost:3333') +locator = page.get_by_test_id('my-time') +expect(locator).to_have_value('2024-01-01T10:00') + +# Fast forward time 30 minutes without firing intermediate timers, as if the user +# closed and opened the lid of the laptop. +page.clock.jump('30:00') +expect(locator).to_have_value('2024-01-01T10:30') +``` + +```java +// Initialize clock with a specific time. +page.clock().install( + new Clock.InstallOptions() + .setNow(Instant.parse("2024-01-01T10:00:00Z")) + .setToFake(new String[]{"Date"}) +); +page.navigate("http://localhost:3333"); +Locator locator = page.getByTestId("my-time"); +assertThat(locator).hasValue("2024-01-01T10:00"); + +// Fast forward time 30 minutes without firing intermediate timers, as if the user +// closed and opened the lid of the laptop. +page.clock().jump("30:00"); +assertThat(locator).hasValue("2024-01-01T10:30"); +``` + +```csharp +// Initialize clock with a specific time. +await page.Clock.InstallAsync( + new ClockInstallOptions + { + Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), + ToFake = new[] { "Date" } + }); +await page.GotoAsync("http://localhost:3333"); +var locator = page.GetByTestId("my-time"); +await Expect(locator).ToHaveValueAsync("2024-01-01T10:00"); + +// Fast forward time 30 minutes without firing intermediate timers, as if the user +// closed and opened the lid of the laptop. +await page.Clock.JumpAsync("30:00"); +await Expect(locator).ToHaveValueAsync("2024-01-01T10:30"); +``` diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 2ed2bf7c97..a77afd9b5c 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17246,6 +17246,15 @@ export interface BrowserServer { export interface Clock { /** * Creates a clock and installs it globally. + * + * **Usage** + * + * ```js + * await page.clock.install(); + * await page.clock.install({ now }); + * await page.clock.install({ now, toFake: ['Date'] }); + * ``` + * * @param options */ install(options?: { @@ -17283,6 +17292,14 @@ export interface Clock { * Advance the clock by jumping forward in time, firing callbacks at most once. Returns fake milliseconds since the * unix epoch. This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, * skipping intermediary timers. + * + * **Usage** + * + * ```js + * await page.clock.jump(1000); + * await page.clock.jump('30:00'); + * ``` + * * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. */ @@ -17290,14 +17307,25 @@ export interface Clock { /** * Advances the clock to the the moment of the first scheduled timer, firing it. + * + * **Usage** + * + * ```js + * await page.clock.next(); + * ``` + * */ next(): Promise; /** * Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be - * run as well. This makes it easier to run asynchronous tests to completion without worrying about the number of - * timers they use, or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there - * is an infinite loop of timers and throws an error. + * run as well. + * + * **Details** + * + * This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, + * or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there is an infinite + * loop of timers and throws an error. */ runAll(): Promise; @@ -17311,6 +17339,14 @@ export interface Clock { /** * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. + * + * **Usage** + * + * ```js + * await page.clock.tick(1000); + * await page.clock.tick('30:00'); + * ``` + * * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. */ From 935aa0493c837b49ad8afa153fa457aba4b4b8d6 Mon Sep 17 00:00:00 2001 From: Pavel Date: Sat, 1 Jun 2024 07:43:11 -0700 Subject: [PATCH 007/144] docs: do not use html cards with scripts --- docs/src/clock.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/clock.md b/docs/src/clock.md index 05a15e2496..1b063f11b7 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -24,7 +24,7 @@ await page.clock.install({ now: new Date('2020-02-02') }); ## Only fake Date.now -```html card +```html +``` + +```js +// Initialize clock with a specific time, take full control over time. +await page.clock.install({ now: new Date('2024-01-01T10:00:00Z') }); +await page.goto('http://localhost:3333'); + +// Tick through time manually, firing all timers in the process. +// In this case, time will be updated in the screen 2 times. +await page.clock.tick(2000); +``` + +```python async +# Initialize clock with a specific time, take full control over time. +await page.clock.install( + now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +) +await page.goto('http://localhost:3333') +locator = page.get_by_test_id('my-time') + +# Tick through time manually, firing all timers in the process. +# In this case, time will be updated in the screen 2 times. +await page.clock.tick(2000) +``` + +```python sync +# Initialize clock with a specific time, take full control over time. +page.clock.install( + now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +) +page.goto('http://localhost:3333') +locator = page.get_by_test_id('my-time') + +# Tick through time manually, firing all timers in the process. +# In this case, time will be updated in the screen 2 times. +page.clock.tick(2000) +``` + +```java +// Initialize clock with a specific time, take full control over time. +page.clock().install( + new Clock.InstallOptions() + .setNow(Instant.parse("2024-01-01T10:00:00Z")) +); +page.navigate("http://localhost:3333"); +Locator locator = page.getByTestId("my-time"); + +// Tick through time manually, firing all timers in the process. +// In this case, time will be updated in the screen 2 times. +page.clock().tick(2000); +``` + +```csharp +// Initialize clock with a specific time, take full control over time. +await page.Clock.InstallAsync( + new ClockInstallOptions + { + Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), + }); +await page.GotoAsync("http://localhost:3333"); +var locator = page.GetByTestId("my-time"); + +// Tick through time manually, firing all timers in the process. +// In this case, time will be updated in the screen 2 times. +await page.Clock.TickAsync(2000); +``` diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index a77afd9b5c..dd53b1e890 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17239,9 +17239,11 @@ export interface BrowserServer { } /** - * Playwright uses [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) for clock emulation. Clock is - * installed for the entire {@link BrowserContext}, so the time in all the pages and iframes is controlled by the same - * clock. + * Accurately simulating time-dependent behavior is essential for verifying the correctness of applications. Learn + * more about [clock emulation](https://playwright.dev/docs/clock). + * + * Note that clock is installed for the entire {@link BrowserContext}, so the time in all the pages and iframes is + * controlled by the same clock. */ export interface Clock { /** From 54e7e254cd86feca7fef3acc5c866ada17fa808c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 1 Jun 2024 08:33:46 -0700 Subject: [PATCH 009/144] chore: fix driver modes to test with under_test set (#31117) --- tests/config/testMode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/config/testMode.ts b/tests/config/testMode.ts index 02623b6dce..06329496f9 100644 --- a/tests/config/testMode.ts +++ b/tests/config/testMode.ts @@ -30,6 +30,7 @@ export class DriverTestMode implements TestMode { async setup() { this._impl = await start({ NODE_OPTIONS: undefined, // Hide driver process while debugging. + PWTEST_UNDER_TEST: 1, }); return this._impl.playwright; } From 0203fed0c29fcb1dba3467ac9a724e8bcc4b8079 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Mon, 3 Jun 2024 00:52:26 -0700 Subject: [PATCH 010/144] feat(webkit): roll to r2017 (#31127) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index dbe8771f92..913af15077 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2016", + "revision": "2017", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 2c6fd722dda807975d3ad4887f540521867227f3 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 3 Jun 2024 12:47:16 +0200 Subject: [PATCH 011/144] feat: support Ubuntu 24.04 (#30826) --- .github/workflows/tests_secondary.yml | 4 +- docs/src/docker.md | 2 + docs/src/intro-csharp.md | 2 +- docs/src/intro-java.md | 2 +- docs/src/intro-js.md | 2 +- docs/src/intro-python.md | 2 +- .../src/server/registry/index.ts | 14 ++ .../src/server/registry/nativeDeps.ts | 225 ++++++++++++++++++ .../playwright-core/src/utils/hostPlatform.ts | 5 +- utils/docker/Dockerfile.noble | 49 ++++ utils/docker/publish_docker.sh | 25 +- utils/linux-browser-dependencies/.gitignore | 2 +- 12 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 utils/docker/Dockerfile.noble diff --git a/.github/workflows/tests_secondary.yml b/.github/workflows/tests_secondary.yml index 9fd407d4dc..adb1f2b8ed 100644 --- a/.github/workflows/tests_secondary.yml +++ b/.github/workflows/tests_secondary.yml @@ -31,7 +31,7 @@ jobs: fail-fast: false matrix: browser: [chromium, firefox, webkit] - os: [ubuntu-20.04] + os: [ubuntu-20.04, ubuntu-24.04] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -116,7 +116,7 @@ jobs: fail-fast: false matrix: browser: [chromium, firefox, webkit] - os: [ubuntu-20.04, ubuntu-22.04, macos-14, windows-latest] + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, macos-14, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/docs/src/docker.md b/docs/src/docker.md index f0ec7d8621..11553a578a 100644 --- a/docs/src/docker.md +++ b/docs/src/docker.md @@ -111,6 +111,7 @@ We currently publish images with the following tags: - `:next` - tip-of-tree image version based on Ubuntu 22.04 LTS (Jammy Jellyfish). - `:next-jammy` - tip-of-tree image version based on Ubuntu 22.04 LTS (Jammy Jellyfish). - `:v%%VERSION%%` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish). +- `:v%%VERSION%%-noble` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat). - `:v%%VERSION%%-jammy` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish). - `:v%%VERSION%%-focal` - Playwright v%%VERSION%% release docker image based on Ubuntu 20.04 LTS (Focal Fossa). @@ -121,6 +122,7 @@ It is recommended to always pin your Docker image to a specific version if possi ### Base images We currently publish images based on the following [Ubuntu](https://hub.docker.com/_/ubuntu) versions: +- **Ubuntu 24.04 LTS** (Noble Numbat), image tags include `noble` - **Ubuntu 22.04 LTS** (Jammy Jellyfish), image tags include `jammy` - **Ubuntu 20.04 LTS** (Focal Fossa), image tags include `focal` diff --git a/docs/src/intro-csharp.md b/docs/src/intro-csharp.md index 99f4767a2a..91f8a967b0 100644 --- a/docs/src/intro-csharp.md +++ b/docs/src/intro-csharp.md @@ -183,7 +183,7 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a - Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - MacOS 12 Monterey, MacOS 13 Ventura, or MacOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04. +- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-java.md b/docs/src/intro-java.md index 999bbcc0f4..a99a1de521 100644 --- a/docs/src/intro-java.md +++ b/docs/src/intro-java.md @@ -131,7 +131,7 @@ By default browsers launched with Playwright run headless, meaning no browser UI - Java 8 or higher. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - MacOS 12 Monterey, MacOS 13 Ventura, or MacOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04. +- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-js.md b/docs/src/intro-js.md index c742ce6fa8..68ac7f1a7c 100644 --- a/docs/src/intro-js.md +++ b/docs/src/intro-js.md @@ -289,7 +289,7 @@ pnpm exec playwright --version - Node.js 18+ - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - MacOS 12 Monterey, MacOS 13 Ventura, or MacOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, with x86-64 or arm64 architecture. +- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-python.md b/docs/src/intro-python.md index 09a493ae83..70e56037d9 100644 --- a/docs/src/intro-python.md +++ b/docs/src/intro-python.md @@ -102,7 +102,7 @@ pip install pytest-playwright playwright -U - Python 3.8 or higher. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - MacOS 12 Monterey, MacOS 13 Ventura, or MacOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04. +- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index b23865663c..c7ce3a2e7e 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -80,9 +80,11 @@ const DOWNLOAD_PATHS: Record = { 'ubuntu18.04-x64': undefined, 'ubuntu20.04-x64': 'builds/chromium/%s/chromium-linux.zip', 'ubuntu22.04-x64': 'builds/chromium/%s/chromium-linux.zip', + 'ubuntu24.04-x64': 'builds/chromium/%s/chromium-linux.zip', 'ubuntu18.04-arm64': undefined, 'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', 'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', + 'ubuntu24.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', 'debian11-x64': 'builds/chromium/%s/chromium-linux.zip', 'debian11-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', 'debian12-x64': 'builds/chromium/%s/chromium-linux.zip', @@ -105,9 +107,11 @@ const DOWNLOAD_PATHS: Record = { 'ubuntu18.04-x64': undefined, 'ubuntu20.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip', 'ubuntu22.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip', + 'ubuntu24.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip', 'ubuntu18.04-arm64': undefined, 'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', 'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', + 'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', 'debian11-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip', 'debian11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', 'debian12-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip', @@ -130,9 +134,11 @@ const DOWNLOAD_PATHS: Record = { 'ubuntu18.04-x64': undefined, 'ubuntu20.04-x64': 'builds/firefox/%s/firefox-ubuntu-20.04.zip', 'ubuntu22.04-x64': 'builds/firefox/%s/firefox-ubuntu-22.04.zip', + 'ubuntu24.04-x64': 'builds/firefox/%s/firefox-ubuntu-24.04.zip', 'ubuntu18.04-arm64': undefined, 'ubuntu20.04-arm64': 'builds/firefox/%s/firefox-ubuntu-20.04-arm64.zip', 'ubuntu22.04-arm64': 'builds/firefox/%s/firefox-ubuntu-22.04-arm64.zip', + 'ubuntu24.04-arm64': 'builds/firefox/%s/firefox-ubuntu-24.04-arm64.zip', 'debian11-x64': 'builds/firefox/%s/firefox-debian-11.zip', 'debian11-arm64': 'builds/firefox/%s/firefox-debian-11-arm64.zip', 'debian12-x64': 'builds/firefox/%s/firefox-debian-12.zip', @@ -155,9 +161,11 @@ const DOWNLOAD_PATHS: Record = { 'ubuntu18.04-x64': undefined, 'ubuntu20.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-20.04.zip', 'ubuntu22.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04.zip', + 'ubuntu24.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04.zip', 'ubuntu18.04-arm64': undefined, 'ubuntu20.04-arm64': undefined, 'ubuntu22.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04-arm64.zip', + 'ubuntu24.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04-arm64.zip', 'debian11-x64': 'builds/firefox-beta/%s/firefox-beta-debian-11.zip', 'debian11-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-11-arm64.zip', 'debian12-x64': 'builds/firefox-beta/%s/firefox-beta-debian-12.zip', @@ -180,9 +188,11 @@ const DOWNLOAD_PATHS: Record = { 'ubuntu18.04-x64': undefined, 'ubuntu20.04-x64': 'builds/webkit/%s/webkit-ubuntu-20.04.zip', 'ubuntu22.04-x64': 'builds/webkit/%s/webkit-ubuntu-22.04.zip', + 'ubuntu24.04-x64': 'builds/webkit/%s/webkit-ubuntu-24.04.zip', 'ubuntu18.04-arm64': undefined, 'ubuntu20.04-arm64': 'builds/webkit/%s/webkit-ubuntu-20.04-arm64.zip', 'ubuntu22.04-arm64': 'builds/webkit/%s/webkit-ubuntu-22.04-arm64.zip', + 'ubuntu24.04-arm64': 'builds/webkit/%s/webkit-ubuntu-24.04-arm64.zip', 'debian11-x64': 'builds/webkit/%s/webkit-debian-11.zip', 'debian11-arm64': 'builds/webkit/%s/webkit-debian-11-arm64.zip', 'debian12-x64': 'builds/webkit/%s/webkit-debian-12.zip', @@ -205,9 +215,11 @@ const DOWNLOAD_PATHS: Record = { 'ubuntu18.04-x64': undefined, 'ubuntu20.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', 'ubuntu22.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', + 'ubuntu24.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', 'ubuntu18.04-arm64': undefined, 'ubuntu20.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', 'ubuntu22.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', + 'ubuntu24.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', 'debian11-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', 'debian11-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', 'debian12-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', @@ -230,9 +242,11 @@ const DOWNLOAD_PATHS: Record = { 'ubuntu18.04-x64': undefined, 'ubuntu20.04-x64': 'builds/android/%s/android.zip', 'ubuntu22.04-x64': 'builds/android/%s/android.zip', + 'ubuntu24.04-x64': 'builds/android/%s/android.zip', 'ubuntu18.04-arm64': undefined, 'ubuntu20.04-arm64': 'builds/android/%s/android.zip', 'ubuntu22.04-arm64': 'builds/android/%s/android.zip', + 'ubuntu24.04-arm64': 'builds/android/%s/android.zip', 'debian11-x64': 'builds/android/%s/android.zip', 'debian11-arm64': 'builds/android/%s/android.zip', 'debian12-x64': 'builds/android/%s/android.zip', diff --git a/packages/playwright-core/src/server/registry/nativeDeps.ts b/packages/playwright-core/src/server/registry/nativeDeps.ts index 0f5f1c499e..81813880c2 100644 --- a/packages/playwright-core/src/server/registry/nativeDeps.ts +++ b/packages/playwright-core/src/server/registry/nativeDeps.ts @@ -450,6 +450,217 @@ export const deps: any = { 'libevent-2.1.so.7': 'libevent-2.1-7', }, }, + + 'ubuntu24.04-x64': { + tools: [ + 'xvfb', + 'fonts-noto-color-emoji', + 'fonts-unifont', + 'libfontconfig1', + 'libfreetype6', + 'xfonts-cyrillic', + 'xfonts-scalable', + 'fonts-liberation', + 'fonts-ipafont-gothic', + 'fonts-wqy-zenhei', + 'fonts-tlwg-loma-otf', + 'fonts-freefont-ttf', + ], + chromium: [ + 'libasound2t64', + 'libatk-bridge2.0-0t64', + 'libatk1.0-0t64', + 'libatspi2.0-0t64', + 'libcairo2', + 'libcups2t64', + 'libdbus-1-3', + 'libdrm2', + 'libgbm1', + 'libglib2.0-0t64', + 'libnspr4', + 'libnss3', + 'libpango-1.0-0', + 'libx11-6', + 'libxcb1', + 'libxcomposite1', + 'libxdamage1', + 'libxext6', + 'libxfixes3', + 'libxkbcommon0', + 'libxrandr2' + ], + firefox: [ + 'libasound2t64', + 'libatk1.0-0t64', + 'libcairo-gobject2', + 'libcairo2', + 'libdbus-1-3', + 'libfontconfig1', + 'libfreetype6', + 'libgdk-pixbuf-2.0-0', + 'libglib2.0-0t64', + 'libgtk-3-0t64', + 'libpango-1.0-0', + 'libpangocairo-1.0-0', + 'libx11-6', + 'libx11-xcb1', + 'libxcb-shm0', + 'libxcb1', + 'libxcomposite1', + 'libxcursor1', + 'libxdamage1', + 'libxext6', + 'libxfixes3', + 'libxi6', + 'libxrandr2', + 'libxrender1' + ], + webkit: [ + 'gstreamer1.0-libav', + 'gstreamer1.0-plugins-bad', + 'gstreamer1.0-plugins-base', + 'gstreamer1.0-plugins-good', + 'libatomic1', + 'libatk-bridge2.0-0t64', + 'libatk1.0-0t64', + 'libcairo-gobject2', + 'libcairo2', + 'libdbus-1-3', + 'libdrm2', + 'libenchant-2-2', + 'libepoxy0', + 'libevent-2.1-7t64', + 'libflite1', + 'libfontconfig1', + 'libfreetype6', + 'libgbm1', + 'libgdk-pixbuf-2.0-0', + 'libgles2', + 'libglib2.0-0t64', + 'libgstreamer-gl1.0-0', + 'libgstreamer-plugins-bad1.0-0', + 'libgstreamer-plugins-base1.0-0', + 'libgstreamer1.0-0', + 'libgtk-3-0t64', + 'libharfbuzz-icu0', + 'libharfbuzz0b', + 'libhyphen0', + 'libicu74', + 'libjpeg-turbo8', + 'liblcms2-2', + 'libmanette-0.2-0', + 'libopus0', + 'libpango-1.0-0', + 'libpangocairo-1.0-0', + 'libpng16-16t64', + 'libsecret-1-0', + 'libvpx9', + 'libwayland-client0', + 'libwayland-egl1', + 'libwayland-server0', + 'libwebp7', + 'libwebpdemux2', + 'libwoff1', + 'libx11-6', + 'libxkbcommon0', + 'libxml2', + 'libxslt1.1', + 'libx264-164' + ], + lib2package: { + 'libasound.so.2': 'libasound2t64', + 'libatk-1.0.so.0': 'libatk1.0-0t64', + 'libatk-bridge-2.0.so.0': 'libatk-bridge2.0-0t64', + 'libatomic.so.1': 'libatomic1', + 'libatspi.so.0': 'libatspi2.0-0t64', + 'libcairo-gobject.so.2': 'libcairo-gobject2', + 'libcairo.so.2': 'libcairo2', + 'libcups.so.2': 'libcups2t64', + 'libdbus-1.so.3': 'libdbus-1-3', + 'libdrm.so.2': 'libdrm2', + 'libenchant-2.so.2': 'libenchant-2-2', + 'libepoxy.so.0': 'libepoxy0', + 'libevent-2.1.so.7': 'libevent-2.1-7t64', + 'libflite_cmu_grapheme_lang.so.1': 'libflite1', + 'libflite_cmu_grapheme_lex.so.1': 'libflite1', + 'libflite_cmu_indic_lang.so.1': 'libflite1', + 'libflite_cmu_indic_lex.so.1': 'libflite1', + 'libflite_cmu_time_awb.so.1': 'libflite1', + 'libflite_cmu_us_awb.so.1': 'libflite1', + 'libflite_cmu_us_kal.so.1': 'libflite1', + 'libflite_cmu_us_kal16.so.1': 'libflite1', + 'libflite_cmu_us_rms.so.1': 'libflite1', + 'libflite_cmu_us_slt.so.1': 'libflite1', + 'libflite_cmulex.so.1': 'libflite1', + 'libflite_usenglish.so.1': 'libflite1', + 'libflite.so.1': 'libflite1', + 'libfontconfig.so.1': 'libfontconfig1', + 'libfreetype.so.6': 'libfreetype6', + 'libgbm.so.1': 'libgbm1', + 'libgdk_pixbuf-2.0.so.0': 'libgdk-pixbuf-2.0-0', + 'libgdk-3.so.0': 'libgtk-3-0t64', + 'libgio-2.0.so.0': 'libglib2.0-0t64', + 'libGLESv2.so.2': 'libgles2', + 'libglib-2.0.so.0': 'libglib2.0-0t64', + 'libgmodule-2.0.so.0': 'libglib2.0-0t64', + 'libgobject-2.0.so.0': 'libglib2.0-0t64', + 'libgstallocators-1.0.so.0': 'libgstreamer-plugins-base1.0-0', + 'libgstapp-1.0.so.0': 'libgstreamer-plugins-base1.0-0', + 'libgstaudio-1.0.so.0': 'libgstreamer-plugins-base1.0-0', + 'libgstbase-1.0.so.0': 'libgstreamer1.0-0', + 'libgstcodecparsers-1.0.so.0': 'libgstreamer-plugins-bad1.0-0', + 'libgstfft-1.0.so.0': 'libgstreamer-plugins-base1.0-0', + 'libgstgl-1.0.so.0': 'libgstreamer-gl1.0-0', + 'libgstpbutils-1.0.so.0': 'libgstreamer-plugins-base1.0-0', + 'libgstreamer-1.0.so.0': 'libgstreamer1.0-0', + 'libgsttag-1.0.so.0': 'libgstreamer-plugins-base1.0-0', + 'libgstvideo-1.0.so.0': 'libgstreamer-plugins-base1.0-0', + 'libgtk-3.so.0': 'libgtk-3-0t64', + 'libharfbuzz-icu.so.0': 'libharfbuzz-icu0', + 'libharfbuzz.so.0': 'libharfbuzz0b', + 'libhyphen.so.0': 'libhyphen0', + 'libicudata.so.74': 'libicu74', + 'libicui18n.so.74': 'libicu74', + 'libicuuc.so.74': 'libicu74', + 'libjpeg.so.8': 'libjpeg-turbo8', + 'liblcms2.so.2': 'liblcms2-2', + 'libmanette-0.2.so.0': 'libmanette-0.2-0', + 'libnspr4.so': 'libnspr4', + 'libnss3.so': 'libnss3', + 'libnssutil3.so': 'libnss3', + 'libopus.so.0': 'libopus0', + 'libpango-1.0.so.0': 'libpango-1.0-0', + 'libpangocairo-1.0.so.0': 'libpangocairo-1.0-0', + 'libpng16.so.16': 'libpng16-16t64', + 'libsecret-1.so.0': 'libsecret-1-0', + 'libsmime3.so': 'libnss3', + 'libsoup-3.0.so.0': 'libsoup-3.0-0', + 'libvpx.so.9': 'libvpx9', + 'libwayland-client.so.0': 'libwayland-client0', + 'libwayland-egl.so.1': 'libwayland-egl1', + 'libwayland-server.so.0': 'libwayland-server0', + 'libwebp.so.7': 'libwebp7', + 'libwebpdemux.so.2': 'libwebpdemux2', + 'libwoff2dec.so.1.0.2': 'libwoff1', + 'libX11-xcb.so.1': 'libx11-xcb1', + 'libX11.so.6': 'libx11-6', + 'libxcb-shm.so.0': 'libxcb-shm0', + 'libxcb.so.1': 'libxcb1', + 'libXcomposite.so.1': 'libxcomposite1', + 'libXcursor.so.1': 'libxcursor1', + 'libXdamage.so.1': 'libxdamage1', + 'libXext.so.6': 'libxext6', + 'libXfixes.so.3': 'libxfixes3', + 'libXi.so.6': 'libxi6', + 'libxkbcommon.so.0': 'libxkbcommon0', + 'libxml2.so.2': 'libxml2', + 'libXrandr.so.2': 'libxrandr2', + 'libXrender.so.1': 'libxrender1', + 'libxslt.so.1': 'libxslt1.1', + 'libx264.so': 'libx264-164', + }, + }, + 'debian11-x64': { tools: [ 'xvfb', @@ -842,6 +1053,20 @@ deps['ubuntu22.04-arm64'] = { }, }; +deps['ubuntu24.04-arm64'] = { + tools: [...deps['ubuntu24.04-x64'].tools], + chromium: [...deps['ubuntu24.04-x64'].chromium], + firefox: [ + ...deps['ubuntu24.04-x64'].firefox, + ], + webkit: [ + ...deps['ubuntu24.04-x64'].webkit, + ], + lib2package: { + ...deps['ubuntu24.04-x64'].lib2package, + }, +}; + deps['debian11-arm64'] = { tools: [...deps['debian11-x64'].tools], chromium: [...deps['debian11-x64'].chromium], diff --git a/packages/playwright-core/src/utils/hostPlatform.ts b/packages/playwright-core/src/utils/hostPlatform.ts index 72c5822e73..9e7be7a5a8 100644 --- a/packages/playwright-core/src/utils/hostPlatform.ts +++ b/packages/playwright-core/src/utils/hostPlatform.ts @@ -28,6 +28,7 @@ export type HostPlatform = 'win64' | 'ubuntu18.04-x64' | 'ubuntu18.04-arm64' | 'ubuntu20.04-x64' | 'ubuntu20.04-arm64' | 'ubuntu22.04-x64' | 'ubuntu22.04-arm64' | + 'ubuntu24.04-x64' | 'ubuntu24.04-arm64' | 'debian11-x64' | 'debian11-arm64' | 'debian12-x64' | 'debian12-arm64' | ''; @@ -71,7 +72,9 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte return { hostPlatform: ('ubuntu18.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; if (parseInt(distroInfo.version, 10) <= 21) return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; + if (parseInt(distroInfo.version, 10) <= 22) + return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; + return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; } // Linux Mint is ubuntu-based but does not have the same versions if (distroInfo?.id === 'linuxmint') { diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble new file mode 100644 index 0000000000..7236acbbfc --- /dev/null +++ b/utils/docker/Dockerfile.noble @@ -0,0 +1,49 @@ +FROM ubuntu:noble + +ARG DEBIAN_FRONTEND=noninteractive +ARG TZ=America/Los_Angeles +ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-jammy" + +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + +# === INSTALL Node.js === + +RUN apt-get update && \ + # Install Node.js + apt-get install -y curl wget gpg ca-certificates && \ + mkdir -p /etc/apt/keyrings && \ + curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \ + apt-get update && \ + apt-get install -y nodejs && \ + # Feature-parity with node.js base images. + apt-get install -y --no-install-recommends git openssh-client && \ + npm install -g yarn && \ + # clean apt cache + rm -rf /var/lib/apt/lists/* && \ + # Create the pwuser + adduser pwuser + +# === BAKE BROWSERS INTO IMAGE === + +ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright + +# 1. Add tip-of-tree Playwright package to install its browsers. +# The package should be built beforehand from tip-of-tree Playwright. +COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz + +# 2. Bake in browsers & deps. +# Browsers will be downloaded in `/ms-playwright`. +# Note: make sure to set 777 to the registry so that any user can access +# registry. +RUN mkdir /ms-playwright && \ + mkdir /ms-playwright-agent && \ + cd /ms-playwright-agent && npm init -y && \ + npm i /tmp/playwright-core.tar.gz && \ + npm exec --no -- playwright-core mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ + npm exec --no -- playwright-core install --with-deps && rm -rf /var/lib/apt/lists/* && \ + rm /tmp/playwright-core.tar.gz && \ + rm -rf /ms-playwright-agent && \ + rm -rf ~/.npm/ && \ + chmod -R 777 /ms-playwright diff --git a/utils/docker/publish_docker.sh b/utils/docker/publish_docker.sh index a1fb63b81e..a1f69e4554 100755 --- a/utils/docker/publish_docker.sh +++ b/utils/docker/publish_docker.sh @@ -26,6 +26,7 @@ else exit 1 fi +# Ubuntu 20.04 FOCAL_TAGS=( "next" "next-focal" @@ -36,6 +37,7 @@ if [[ "$RELEASE_CHANNEL" == "stable" ]]; then FOCAL_TAGS+=("focal") fi +# Ubuntu 22.04 JAMMY_TAGS=( "next-jammy" "v${PW_VERSION}-jammy" @@ -47,6 +49,15 @@ if [[ "$RELEASE_CHANNEL" == "stable" ]]; then JAMMY_TAGS+=("jammy") fi +NOBLE_TAGS=( + "next-noble" + "v${PW_VERSION}-noble" +) + +if [[ "$RELEASE_CHANNEL" == "stable" ]]; then + NOBLE_TAGS+=("noble") +fi + tag_and_push() { local source="$1" local target="$2" @@ -83,8 +94,10 @@ publish_docker_images_with_arch_suffix() { TAGS=("${FOCAL_TAGS[@]}") elif [[ "$FLAVOR" == "jammy" ]]; then TAGS=("${JAMMY_TAGS[@]}") + elif [[ "$FLAVOR" == "noble" ]]; then + TAGS=("${NOBLE_TAGS[@]}") else - echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal' or 'jammy'" + echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'" exit 1 fi local ARCH="$2" @@ -109,8 +122,10 @@ publish_docker_manifest () { TAGS=("${FOCAL_TAGS[@]}") elif [[ "$FLAVOR" == "jammy" ]]; then TAGS=("${JAMMY_TAGS[@]}") + elif [[ "$FLAVOR" == "noble" ]]; then + TAGS=("${NOBLE_TAGS[@]}") else - echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal' or 'jammy'" + echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'" exit 1 fi @@ -129,11 +144,17 @@ publish_docker_manifest () { done } +# Ubuntu 20.04 publish_docker_images_with_arch_suffix focal amd64 publish_docker_images_with_arch_suffix focal arm64 publish_docker_manifest focal amd64 arm64 +# Ubuntu 22.04 publish_docker_images_with_arch_suffix jammy amd64 publish_docker_images_with_arch_suffix jammy arm64 publish_docker_manifest jammy amd64 arm64 +# Ubuntu 24.04 +publish_docker_images_with_arch_suffix noble amd64 +publish_docker_images_with_arch_suffix noble arm64 +publish_docker_manifest noble amd64 arm64 diff --git a/utils/linux-browser-dependencies/.gitignore b/utils/linux-browser-dependencies/.gitignore index 54f19fb231..2079723526 100644 --- a/utils/linux-browser-dependencies/.gitignore +++ b/utils/linux-browser-dependencies/.gitignore @@ -1,2 +1,2 @@ RUN_RESULT -playwright.tar.gz +playwright-core.tar.gz From baecdfd9389298a64afe15ff9a6c6ad09f0c09ea Mon Sep 17 00:00:00 2001 From: ggorlen Date: Mon, 3 Jun 2024 03:50:37 -0700 Subject: [PATCH 012/144] docs(auth): fix auth bash typo (#31124) --- docs/src/auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/auth.md b/docs/src/auth.md index 6c26e6b1a3..c008771a5c 100644 --- a/docs/src/auth.md +++ b/docs/src/auth.md @@ -15,7 +15,7 @@ We recommend to create `playwright/.auth` directory and add it to your `.gitigno ```bash tab=bash-bash mkdir -p playwright/.auth -echo "\nplaywright/.auth" >> .gitignore +echo $'\nplaywright/.auth' >> .gitignore ``` ```batch tab=bash-batch From d50a2bb0c6c41a30362b041cb7333b2bd51888a4 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 3 Jun 2024 16:28:07 +0200 Subject: [PATCH 013/144] test: fix service tests (#31130) --- tests/library/playwright.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/library/playwright.config.ts b/tests/library/playwright.config.ts index b5e3ef44f1..9ab64c22be 100644 --- a/tests/library/playwright.config.ts +++ b/tests/library/playwright.config.ts @@ -56,7 +56,7 @@ const os: 'linux' | 'windows' = (process.env.PLAYWRIGHT_SERVICE_OS as 'linux' | const runId = process.env.PLAYWRIGHT_SERVICE_RUN_ID || new Date().toISOString(); // name the test run let connectOptions: any; -let webServer: any; +let webServer: Config['webServer']; if (mode === 'service') { connectOptions = { wsEndpoint: 'ws://localhost:3333/' }; @@ -64,6 +64,7 @@ if (mode === 'service') { command: 'npx playwright run-server --port=3333', url: 'http://localhost:3333', reuseExistingServer: !process.env.CI, + env: { PWTEST_UNDER_TEST: '1' } }; } if (mode === 'service2') { From 89756847534f34731b902369596b274a7139d338 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 3 Jun 2024 16:39:18 +0200 Subject: [PATCH 014/144] devops: group run-test commands into groups (#31116) --- .github/actions/run-test/action.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/actions/run-test/action.yml b/.github/actions/run-test/action.yml index 83d0ef901b..568e3900bb 100644 --- a/.github/actions/run-test/action.yml +++ b/.github/actions/run-test/action.yml @@ -36,14 +36,23 @@ runs: with: node-version: ${{ inputs.node-version }} - uses: ./.github/actions/enable-microphone-access - - run: npm ci + - run: | + echo "::group::npm ci" + npm ci + echo "::endgroup::" shell: bash env: DEBUG: pw:install PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - - run: npm run build + - run: | + echo "::group::npm run build" + npm run build + echo "::endgroup::" shell: bash - - run: npx playwright install --with-deps ${{ inputs.browsers-to-install }} + - run: | + echo "::group::npx playwright install --with-deps" + npx playwright install --with-deps ${{ inputs.browsers-to-install }} + echo "::endgroup::" shell: bash - name: Run tests if: inputs.shell == 'bash' @@ -69,7 +78,10 @@ runs: client-id: ${{ inputs.flakiness-client-id }} tenant-id: ${{ inputs.flakiness-tenant-id }} subscription-id: ${{ inputs.flakiness-subscription-id }} - - run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json + - run: | + echo "::group::./utils/upload_flakiness_dashboard.sh" + ./utils/upload_flakiness_dashboard.sh ./test-results/report.json + echo "::endgroup::" if: ${{ !cancelled() }} shell: bash - name: Upload blob report From 8d0def190ddcc88842811f8623cf7c8fb5e83ccc Mon Sep 17 00:00:00 2001 From: Pavel Date: Mon, 3 Jun 2024 09:00:38 -0700 Subject: [PATCH 015/144] docs: iterate over the clock (2) --- docs/src/api/class-clock.md | 71 ++++++++++++----------- packages/playwright-core/types/types.d.ts | 15 ++--- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 271023e2cb..c140c471f4 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -87,39 +87,10 @@ Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the Relevant only when using with [`option: shouldAdvanceTime`]. Increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change in the real system time (default: 20). - -## async method: Clock.next -* since: v1.45 -- returns: <[int]> Fake milliseconds since the unix epoch. - -Advances the clock to the the moment of the first scheduled timer, firing it. - -**Usage** - -```js -await page.clock.next(); -``` - -```python async -await page.clock.next() -``` - -```python sync -page.clock.next() -``` - -```java -page.clock().next(); -``` - -```csharp -await page.Clock.NextAsync(); -``` - ## async method: Clock.jump * since: v1.45 -Advance the clock by jumping forward in time, firing callbacks at most once. Returns fake milliseconds since the unix epoch. +Advance the clock by jumping forward in time, firing callbacks at most once. This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers. **Usage** @@ -155,11 +126,40 @@ await page.Clock.JumpAsync("30:00"); Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. +## async method: Clock.next +* since: v1.45 +- returns: <[int]> + +Advances the clock to the the moment of the first scheduled timer, firing it. +Returns fake milliseconds since the unix epoch. + +**Usage** + +```js +await page.clock.next(); +``` + +```python async +await page.clock.next() +``` + +```python sync +page.clock.next() +``` + +```java +page.clock().next(); +``` + +```csharp +await page.Clock.NextAsync(); +``` + ## async method: Clock.runAll * since: v1.45 -- returns: <[int]> Fake milliseconds since the unix epoch. +- returns: <[int]> -Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. +Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. Returns fake milliseconds since the unix epoch. **Details** @@ -169,18 +169,19 @@ It runs a maximum of [`option: loopLimit`] times after which it assumes there is ## async method: Clock.runToLast * since: v1.45 -- returns: <[int]> Fake milliseconds since the unix epoch. +- returns: <[int]> This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as necessary. If new timers are added while it is executing they will be run only if they would occur before this time. This is useful when you want to run a test to completion, but the test recursively sets timers that would cause runAll to trigger an infinite loop warning. +Returns fake milliseconds since the unix epoch. ## async method: Clock.tick * since: v1.45 -- returns: <[int]> Fake milliseconds since the unix epoch. +- returns: <[int]> -Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. +Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Returns fake milliseconds since the unix epoch. **Usage** diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index dd53b1e890..11305a90d5 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17291,9 +17291,8 @@ export interface Clock { }): Promise; /** - * Advance the clock by jumping forward in time, firing callbacks at most once. Returns fake milliseconds since the - * unix epoch. This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, - * skipping intermediary timers. + * Advance the clock by jumping forward in time, firing callbacks at most once. This can be used to simulate the JS + * engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers. * * **Usage** * @@ -17308,7 +17307,8 @@ export interface Clock { jump(time: number|string): Promise; /** - * Advances the clock to the the moment of the first scheduled timer, firing it. + * Advances the clock to the the moment of the first scheduled timer, firing it. Returns fake milliseconds since the + * unix epoch. * * **Usage** * @@ -17321,7 +17321,7 @@ export interface Clock { /** * Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be - * run as well. + * run as well. Returns fake milliseconds since the unix epoch. * * **Details** * @@ -17335,12 +17335,13 @@ export interface Clock { * This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as * necessary. If new timers are added while it is executing they will be run only if they would occur before this * time. This is useful when you want to run a test to completion, but the test recursively sets timers that would - * cause runAll to trigger an infinite loop warning. + * cause runAll to trigger an infinite loop warning. Returns fake milliseconds since the unix epoch. */ runToLast(): Promise; /** - * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. + * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Returns fake + * milliseconds since the unix epoch. * * **Usage** * From c912621d1026d1b473af4f9242e8078f2ee303d4 Mon Sep 17 00:00:00 2001 From: Sander <17591696+sand4rt@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:58:56 +0200 Subject: [PATCH 016/144] docs(ct): api reference (#31109) closes: https://github.com/microsoft/playwright/issues/30581 --- docs/src/test-components-js.md | 303 ++++++++++++++++++++++++++++++++- 1 file changed, 295 insertions(+), 8 deletions(-) diff --git a/docs/src/test-components-js.md b/docs/src/test-components-js.md index c618489833..099dc2b362 100644 --- a/docs/src/test-components-js.md +++ b/docs/src/test-components-js.md @@ -281,7 +281,179 @@ test('changes the image', async ({ mount }) => { As a result, for every component you'll have a story file that exports all the stories that are actually tested. These stories live in the browser and "convert" complex object into the simple objects that can be accessed in the test. -## Hooks +## Under the hood + +Here is how component testing works: + +- Once the tests are executed, Playwright creates a list of components that the tests need. +- It then compiles a bundle that includes these components and serves it using a local static web server. +- Upon the `mount` call within the test, Playwright navigates to the facade page `/playwright/index.html` of this bundle and tells it to render the component. +- Events are marshalled back to the Node.js environment to allow verification. + +Playwright is using [Vite](https://vitejs.dev/) to create the components bundle and serve it. + +## API reference + +### props + +Provide props to a component when mounted. + + + + + +```js +test('props', async ({ mount }) => { + const component = await mount(); +}); +``` + + + + +```js +test('props', async ({ mount }) => { + const component = await mount(); +}); +``` + + + + +```js +test('props', async ({ mount }) => { + const component = await mount(Component, { props: { msg: 'greetings' } }); +}); +``` + + + + +```js +test('props', async ({ mount }) => { + const component = await mount(Component, { props: { msg: 'greetings' } }); +}); +``` + + + + + +### callbacks / events + +Provide callbacks/events to a component when mounted. + + + + + +```js +test('callback', async ({ mount }) => { + const component = await mount( {}} />); +}); +``` + + + + +```js +test('callback', async ({ mount }) => { + const component = await mount( {}} />); +}); +``` + + + + +```js +test('event', async ({ mount }) => { + const component = await mount(Component, { on: { callback() {} } }); +}); +``` + + + + +```js +test('event', async ({ mount }) => { + const component = await mount(Component, { on: { callback() {} } }); +}); +``` + + + + + +### children / slots + +Provide children/slots to a component when mounted. + + + + + +```js +test('children', async ({ mount }) => { + const component = await mount(Child); +}); +``` + + + + +```js +test('children', async ({ mount }) => { + const component = await mount(Child); +}); +``` + + + + +```js +test('slot', async ({ mount }) => { + const component = await mount(Component, { slots: { default: 'Slot' } }); +}); +``` + + + + +```js +test('slot', async ({ mount }) => { + const component = await mount(Component, { slots: { default: 'Slot' } }); +}); +``` + + + + + +### hooks You can use `beforeMount` and `afterMount` hooks to configure your app. This lets you setup things like your app router, fake server etc. giving you the flexibility you need. You can also pass custom configuration from the `mount` call from a test, which is accessible from the `hooksConfig` fixture. This includes any config that needs to be run before or after mounting the component. An example of configuring a router is provided below: @@ -423,16 +595,131 @@ You can use `beforeMount` and `afterMount` hooks to configure your app. This let -## Under the hood +### unmount -Here is how component testing works: +Unmount the mounted component from the DOM. This is useful for testing the component's behavior upon unmounting. Use cases include testing an "Are you sure you want to leave?" modal or ensuring proper cleanup of event handlers to prevent memory leaks. -- Once the tests are executed, Playwright creates a list of components that the tests need. -- It then compiles a bundle that includes these components and serves it using a local static web server. -- Upon the `mount` call within the test, Playwright navigates to the facade page `/playwright/index.html` of this bundle and tells it to render the component. -- Events are marshalled back to the Node.js environment to allow verification. + -Playwright is using [Vite](https://vitejs.dev/) to create the components bundle and serve it. + + +```js +test('unmount', async ({ mount }) => { + const component = await mount(); + await component.unmount(); +}); +``` + + + + +```js +test('unmount', async ({ mount }) => { + const component = await mount(); + await component.unmount(); +}); +``` + + + + +```js +test('unmount', async ({ mount }) => { + const component = await mount(Component); + await component.unmount(); +}); +``` + + + + +```js +test('unmount', async ({ mount }) => { + const component = await mount(Component); + await component.unmount(); +}); +``` + + + + + +### update + +Update props, slots/children, and/or events/callbacks of a mounted component. These component inputs can change at any time and are typically provided by the parent component, but sometimes it is necessary to ensure that your components behave appropriately to new inputs. + + + + + +```js +test('update', async ({ mount }) => { + const component = await mount(); + await component.update( + {}}>Child + ); +}); +``` + + + + +```js +test('update', async ({ mount }) => { + const component = await mount(); + await component.update( + {}}>Child + ); +}); +``` + + + + +```js +test('update', async ({ mount }) => { + const component = await mount(); + await component.update({ + props: { msg: 'greetings' }, + on: { callback: () => {} }, + slots: { default: 'Child' } + }); +}); +``` + + + + +```js +test('update', async ({ mount }) => { + const component = await mount(); + await component.update({ + props: { msg: 'greetings' }, + on: { callback: () => {} }, + slots: { default: 'Child' } + }); +}); +``` + + + + ## Frequently asked questions From bdc7fc82880f137c52226a9bc0629e314a7b1fcb Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 3 Jun 2024 16:26:29 -0700 Subject: [PATCH 017/144] docs: explain install-deps with proxy (#31136) --- docs/src/browsers.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/src/browsers.md b/docs/src/browsers.md index ca65a19cad..c29acb9f7e 100644 --- a/docs/src/browsers.md +++ b/docs/src/browsers.md @@ -457,7 +457,7 @@ Google Chrome and Microsoft Edge respect enterprise policies, which include limi ### Firefox -Playwright's Firefox version matches the recent [Firefox Stable](https://www.mozilla.org/en-US/firefox/new/) build. Playwright doesn't work with the branded version of Firefox since it relies on patches. +Playwright's Firefox version matches the recent [Firefox Stable](https://www.mozilla.org/en-US/firefox/new/) build. Playwright doesn't work with the branded version of Firefox since it relies on patches. ### WebKit @@ -604,6 +604,24 @@ $Env:PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT="120000" pwsh bin/Debug/netX/playwright.ps1 install ``` +If you are [installing dependencies](#install-system-dependencies) and need to use a proxy on Linux, make sure to run the command as a root user. Otherwise, Playwright will attempt to become a root and will not pass environment variables like `HTTPS_PROXY` to the linux package manager. + +```bash js +sudo HTTPS_PROXY=https://192.0.2.1 npx playwright install-deps +``` + +```bash java +sudo HTTPS_PROXY=https://192.0.2.1 mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install-deps" +``` + +```bash python +sudo HTTPS_PROXY=https://192.0.2.1 playwright install-deps +``` + +```bash csharp +sudo HTTPS_PROXY=https://192.0.2.1 pwsh bin/Debug/netX/playwright.ps1 install-deps +``` + ## Download from artifact repository By default, Playwright downloads browsers from Microsoft's CDN. From 9e6f0514881c497331d3db62bcba4321f91387eb Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 4 Jun 2024 08:59:50 +0200 Subject: [PATCH 018/144] devops: merge other workflows (#31121) --- .github/actions/run-test/action.yml | 3 +- .github/workflows/create_test_report.yml | 2 +- .github/workflows/tests_clock.yml | 62 --------- .github/workflows/tests_electron.yml | 42 ------- .github/workflows/tests_others.yml | 153 +++++++++++++++++++++++ .github/workflows/tests_secondary.yml | 4 +- .github/workflows/tests_stress.yml | 56 --------- .github/workflows/tests_webview2.yml | 60 --------- tests/electron/playwright.config.ts | 1 + tests/webview2/playwright.config.ts | 1 + 10 files changed, 161 insertions(+), 223 deletions(-) delete mode 100644 .github/workflows/tests_clock.yml delete mode 100644 .github/workflows/tests_electron.yml create mode 100644 .github/workflows/tests_others.yml delete mode 100644 .github/workflows/tests_stress.yml delete mode 100644 .github/workflows/tests_webview2.yml diff --git a/.github/actions/run-test/action.yml b/.github/actions/run-test/action.yml index 568e3900bb..dcf8168367 100644 --- a/.github/actions/run-test/action.yml +++ b/.github/actions/run-test/action.yml @@ -85,7 +85,8 @@ runs: if: ${{ !cancelled() }} shell: bash - name: Upload blob report - if: ${{ !cancelled() }} + # We only merge reports for PRs as per .github/workflows/create_test_report.yml. + if: ${{ !cancelled() && github.event.workflow_run.event == 'pull_request' }} uses: ./.github/actions/upload-blob-report with: report_dir: blob-report diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index 8ae54609e4..9d382f1ee6 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -1,7 +1,7 @@ name: Publish Test Results on: workflow_run: - workflows: ["tests 1", "tests 2"] + workflows: ["tests 1", "tests 2", "tests others"] types: - completed jobs: diff --git a/.github/workflows/tests_clock.yml b/.github/workflows/tests_clock.yml deleted file mode 100644 index cc9a519ee3..0000000000 --- a/.github/workflows/tests_clock.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: "tests Clock" - -on: - push: - branches: - - main - - release-* - pull_request: - paths-ignore: - - 'browser_patches/**' - - 'docs/**' - types: [ labeled ] - branches: - - main - - release-* - -env: - # Force terminal colors. @see https://www.npmjs.com/package/colors - FORCE_COLOR: 1 - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - -jobs: - frozen_time_linux: - name: Frozen time library - environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} - permissions: - id-token: write # This is required for OIDC login (azure/login) to succeed - contents: read # This is required for actions/checkout to succeed - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/run-test - with: - node-version: 20 - browsers-to-install: chromium - command: npm run test -- --project=chromium-* - bot-name: "frozen-time-library-chromium-linux" - flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} - flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} - flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} - env: - PW_FREEZE_TIME: 1 - - frozen_time_test_runner: - name: Frozen time test runner - environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} - runs-on: ubuntu-22.04 - permissions: - id-token: write # This is required for OIDC login (azure/login) to succeed - contents: read # This is required for actions/checkout to succeed - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/run-test - with: - node-version: 20 - command: npm run ttest - bot-name: "frozen-time-runner-chromium-linux" - flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} - flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} - flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} - env: - PW_FREEZE_TIME: 1 diff --git a/.github/workflows/tests_electron.yml b/.github/workflows/tests_electron.yml deleted file mode 100644 index 604c2e0e2c..0000000000 --- a/.github/workflows/tests_electron.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: "electron" - -on: - push: - branches: - - main - - release-* - pull_request: - paths-ignore: - - 'browser_patches/**' - - 'docs/**' - types: [ labeled ] - branches: - - main - - release-* - -env: - # Force terminal colors. @see https://www.npmjs.com/package/colors - FORCE_COLOR: 1 - -jobs: - test_electron: - name: ${{ matrix.os }} - environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - permissions: - id-token: write # This is required for OIDC login (azure/login) to succeed - contents: read # This is required for actions/checkout to succeed - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/run-test - with: - browsers-to-install: chromium - command: npm run etest - bot-name: "electron-${{ matrix.os }}" - flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} - flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} - flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} diff --git a/.github/workflows/tests_others.yml b/.github/workflows/tests_others.yml new file mode 100644 index 0000000000..04115ce693 --- /dev/null +++ b/.github/workflows/tests_others.yml @@ -0,0 +1,153 @@ +name: tests others + +on: + push: + branches: + - main + - release-* + pull_request: + paths-ignore: + - 'browser_patches/**' + - 'docs/**' + types: [ labeled ] + branches: + - main + - release-* + +env: + FORCE_COLOR: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + +jobs: + test_stress: + name: Stress - ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Stick with macos-latest-large for now which is Intel-based until + # https://github.com/microsoft/playwright/issues/30705 is fixed. + os: [ubuntu-latest, macos-latest-large, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npm run build + - run: npx playwright install --with-deps + - run: npm run stest contexts -- --project=chromium + if: ${{ !cancelled() }} + - run: npm run stest browsers -- --project=chromium + if: ${{ !cancelled() }} + - run: npm run stest frames -- --project=chromium + if: ${{ !cancelled() }} + - run: npm run stest contexts -- --project=webkit + if: ${{ !cancelled() }} + - run: npm run stest browsers -- --project=webkit + if: ${{ !cancelled() }} + - run: npm run stest frames -- --project=webkit + if: ${{ !cancelled() }} + - run: npm run stest contexts -- --project=firefox + if: ${{ !cancelled() }} + - run: npm run stest browsers -- --project=firefox + if: ${{ !cancelled() }} + - run: npm run stest frames -- --project=firefox + if: ${{ !cancelled() }} + - run: npm run stest heap -- --project=chromium + if: ${{ !cancelled() }} + + test_webview2: + name: WebView2 + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + runs-on: windows-2022 + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + - run: dotnet build + working-directory: tests/webview2/webview2-app/ + - name: Update to Evergreen WebView2 Runtime + shell: pwsh + run: | + # See here: https://developer.microsoft.com/en-us/microsoft-edge/webview2/ + Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe' + Start-Process -FilePath setup.exe -Verb RunAs -Wait + - uses: ./.github/actions/run-test + with: + node-version: 20 + browsers-to-install: chromium + command: npm run webview2test + bot-name: "webview2-chromium-windows" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} + + test_clock_frozen_time_linux: + name: Frozen time library + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/run-test + with: + node-version: 20 + browsers-to-install: chromium + command: npm run test -- --project=chromium-* + bot-name: "frozen-time-library-chromium-linux" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} + env: + PW_FREEZE_TIME: 1 + + test_clock_frozen_time_test_runner: + name: Frozen time test runner + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + runs-on: ubuntu-22.04 + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/run-test + with: + node-version: 20 + command: npm run ttest + bot-name: "frozen-time-runner-chromium-linux" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} + env: + PW_FREEZE_TIME: 1 + + test_electron: + name: Electron - ${{ matrix.os }} + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/run-test + with: + browsers-to-install: chromium + command: npm run etest + bot-name: "electron-${{ matrix.os }}" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: diff --git a/.github/workflows/tests_secondary.yml b/.github/workflows/tests_secondary.yml index adb1f2b8ed..fd5458fb89 100644 --- a/.github/workflows/tests_secondary.yml +++ b/.github/workflows/tests_secondary.yml @@ -307,7 +307,9 @@ jobs: firefox_beta_mac: name: "Firefox Beta (Mac)" environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} - runs-on: macos-latest + # Stick with macos-latest-large for now which is Intel-based until + # https://github.com/microsoft/playwright/issues/30705 is fixed. + runs-on: macos-latest-large steps: - uses: actions/checkout@v4 - uses: ./.github/actions/run-test diff --git a/.github/workflows/tests_stress.yml b/.github/workflows/tests_stress.yml deleted file mode 100644 index 6001f04567..0000000000 --- a/.github/workflows/tests_stress.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: "stress" - -on: - push: - branches: - - main - - release-* - pull_request: - paths-ignore: - - 'browser_patches/**' - - 'docs/**' - types: [ labeled ] - branches: - - main - - release-* - -env: - FORCE_COLOR: 1 - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - -jobs: - test_components: - name: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: npm ci - - run: npm run build - - run: npx playwright install --with-deps - - run: npm run stest contexts -- --project=chromium - if: ${{ !cancelled() }} - - run: npm run stest browsers -- --project=chromium - if: ${{ !cancelled() }} - - run: npm run stest frames -- --project=chromium - if: ${{ !cancelled() }} - - run: npm run stest contexts -- --project=webkit - if: ${{ !cancelled() }} - - run: npm run stest browsers -- --project=webkit - if: ${{ !cancelled() }} - - run: npm run stest frames -- --project=webkit - if: ${{ !cancelled() }} - - run: npm run stest contexts -- --project=firefox - if: ${{ !cancelled() }} - - run: npm run stest browsers -- --project=firefox - if: ${{ !cancelled() }} - - run: npm run stest frames -- --project=firefox - if: ${{ !cancelled() }} - - run: npm run stest heap -- --project=chromium - if: ${{ !cancelled() }} diff --git a/.github/workflows/tests_webview2.yml b/.github/workflows/tests_webview2.yml deleted file mode 100644 index e894c2dc78..0000000000 --- a/.github/workflows/tests_webview2.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: "WebView2 Tests" - -on: - push: - branches: - - main - - release-* - pull_request: - paths-ignore: - - 'browser_patches/**' - - 'docs/**' - types: [ labeled ] - branches: - - main - - release-* - -env: - # Force terminal colors. @see https://www.npmjs.com/package/colors - FORCE_COLOR: 1 - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - -jobs: - test_webview2: - name: WebView2 - environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} - runs-on: windows-2022 - permissions: - id-token: write # This is required for OIDC login (azure/login) to succeed - contents: read # This is required for actions/checkout to succeed - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18 - - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '8.0.x' - - run: npm ci - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - - run: npm run build - - run: dotnet build - working-directory: tests/webview2/webview2-app/ - - name: Update to Evergreen WebView2 Runtime - shell: pwsh - run: | - # See here: https://developer.microsoft.com/en-us/microsoft-edge/webview2/ - Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe' - Start-Process -FilePath setup.exe -Verb RunAs -Wait - - run: npm run webview2test - - name: Azure Login - uses: azure/login@v2 - if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }} - with: - client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} - - run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json - if: ${{ !cancelled() }} - shell: bash diff --git a/tests/electron/playwright.config.ts b/tests/electron/playwright.config.ts index 562e2ae6a1..b630b38e56 100644 --- a/tests/electron/playwright.config.ts +++ b/tests/electron/playwright.config.ts @@ -35,6 +35,7 @@ const config: Config = { reporter: process.env.CI ? [ ['dot'], ['json', { outputFile: path.join(outputDir, 'report.json') }], + ['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }], ] : 'line', projects: [], globalSetup: './globalSetup.ts' diff --git a/tests/webview2/playwright.config.ts b/tests/webview2/playwright.config.ts index 1bdd704a35..0710378a50 100644 --- a/tests/webview2/playwright.config.ts +++ b/tests/webview2/playwright.config.ts @@ -35,6 +35,7 @@ const config: Config = { reporter: process.env.CI ? [ ['dot'], ['json', { outputFile: path.join(outputDir, 'report.json') }], + ['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }], ] : 'line', projects: [], globalSetup: './globalSetup.ts', From 727b2189e4ed8d3a51cfa2b271adbd1642c9dba9 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 4 Jun 2024 04:31:36 -0400 Subject: [PATCH 019/144] docs: fix parallelism and sharding references (#31134) --- docs/src/test-configuration-js.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/test-configuration-js.md b/docs/src/test-configuration-js.md index 99fd492816..30bdbee8ca 100644 --- a/docs/src/test-configuration-js.md +++ b/docs/src/test-configuration-js.md @@ -59,14 +59,14 @@ export default defineConfig({ | Option | Description | | :- | :- | | [`property: TestConfig.forbidOnly`] | Whether to exit with an error if any tests are marked as `test.only`. Useful on CI.| -| [`property: TestConfig.fullyParallel`] | have all tests in all files to run in parallel. See [/Parallelism and sharding](./test-parallel) for more details. | +| [`property: TestConfig.fullyParallel`] | have all tests in all files to run in parallel. See [Parallelism](./test-parallel) and [Sharding](./test-sharding) for more details. | | [`property: TestConfig.projects`] | Run tests in multiple configurations or on multiple browsers | | [`property: TestConfig.reporter`] | Reporter to use. See [Test Reporters](/test-reporters.md) to learn more about which reporters are available. | | [`property: TestConfig.retries`] | The maximum number of retry attempts per test. See [Test Retries](/test-retries.md) to learn more about retries.| | [`property: TestConfig.testDir`] | Directory with the test files. | | [`property: TestConfig.use`] | Options with `use{}` | | [`property: TestConfig.webServer`] | To launch a server during the tests, use the `webServer` option | -| [`property: TestConfig.workers`] | The maximum number of concurrent worker processes to use for parallelizing tests. Can also be set as percentage of logical CPU cores, e.g. `'50%'.`. See [/Parallelism and sharding](./test-parallel) for more details. | +| [`property: TestConfig.workers`] | The maximum number of concurrent worker processes to use for parallelizing tests. Can also be set as percentage of logical CPU cores, e.g. `'50%'.`. See [Parallelism](./test-parallel) and [Sharding](./test-sharding) for more details. | ## Filtering Tests From c516ba0ec85b1789895afb0a9ac8a336ce782171 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 4 Jun 2024 06:51:35 -0700 Subject: [PATCH 020/144] api(clock): rework api based on the review (#31137) --- docs/src/api/class-clock.md | 254 +++++-------- docs/src/clock.md | 218 +++++------ .../playwright-core/src/client/browser.ts | 2 +- packages/playwright-core/src/client/clock.ts | 37 +- .../playwright-core/src/protocol/validator.ts | 35 +- packages/playwright-core/src/server/clock.ts | 134 +++++-- .../dispatchers/browserContextDispatcher.ts | 28 +- .../src/server/injected/fakeTimers.ts | 5 +- .../src/third_party/fake-timers-src.js | 1 + packages/playwright-core/types/types.d.ts | 149 ++++---- packages/protocol/src/channels.ts | 87 ++--- packages/protocol/src/protocol.yml | 27 +- tests/page/page-clock.spec.ts | 352 +++++++++--------- 13 files changed, 667 insertions(+), 662 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index c140c471f4..95fa0b69d3 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -6,211 +6,161 @@ Accurately simulating time-dependent behavior is essential for verifying the cor Note that clock is installed for the entire [BrowserContext], so the time in all the pages and iframes is controlled by the same clock. -## async method: Clock.install +## async method: Clock.installFakeTimers * since: v1.45 -Creates a clock and installs it globally. +Install fake implementations for the following time-related functions: -**Usage** +* `setTimeout` +* `clearTimeout` +* `setInterval` +* `clearInterval` +* `requestAnimationFrame` +* `cancelAnimationFrame` +* `requestIdleCallback` +* `cancelIdleCallback` +* `performance` -```js -await page.clock.install(); -await page.clock.install({ now }); -await page.clock.install({ now, toFake: ['Date'] }); -``` +Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.skipTime`] for more information. -```python async -await page.clock.install() -await page.clock.install(now=now) -await page.clock.install(now=now, toFake=['Date']) -``` - -```python sync -page.clock.install() -page.clock.install(now=now) -page.clock.install(now=now, toFake=['Date']) -``` - -```java -page.clock().install(); -page.clock().install( - new Clock.InstallOptions() - .setNow(now)); -page.clock().install( - new Clock.InstallOptions() - .setNow(now) - .setToFake(new String[]{"Date"})); -``` - -```csharp -await page.Clock.InstallAsync(); -await page.Clock.InstallAsync( - new ClockInstallOptions { Now = now }); -await page.Clock.InstallAsync( - new ClockInstallOptions - { - Now = now, - ToFake = new[] { "Date" } - }); -``` - -### option: Clock.install.now +### param: Clock.installFakeTimers.time * since: v1.45 -- `now` <[int]|[Date]> +- `time` <[int]|[Date]> -Install fake timers with the specified unix epoch (default: 0). +Install fake timers with the specified base time. -### option: Clock.install.toFake -* since: v1.45 -- `toFake` <[Array]<[FakeMethod]<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">>> - -An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: ['setTimeout'] })` will fake only `setTimeout()`. -By default, all the methods are faked. - -### option: Clock.install.loopLimit +### option: Clock.installFakeTimers.loopLimit * since: v1.45 - `loopLimit` <[int]> -The maximum number of timers that will be run when calling [`method: Clock.runAll`]. Defaults to `1000`. +The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`. -### option: Clock.install.shouldAdvanceTime -* since: v1.45 -- `shouldAdvanceTime` <[boolean]> - -Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the real system time shift (e.g., the mocked time will be incremented by -20ms for every 20ms change in the real system time). Defaults to `false`. - -### option: Clock.install.advanceTimeDelta -* since: v1.45 -- `advanceTimeDelta` <[int]> - -Relevant only when using with [`option: shouldAdvanceTime`]. Increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change -in the real system time (default: 20). - -## async method: Clock.jump -* since: v1.45 - -Advance the clock by jumping forward in time, firing callbacks at most once. -This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers. - -**Usage** - -```js -await page.clock.jump(1000); -await page.clock.jump('30:00'); -``` - -```python async -await page.clock.jump(1000); -await page.clock.jump('30:00') -``` - -```python sync -page.clock.jump(1000); -page.clock.jump('30:00') -``` - -```java -page.clock().jump(1000); -page.clock().jump("30:00"); -``` - -```csharp -await page.Clock.JumpAsync(1000); -await page.Clock.JumpAsync("30:00"); -``` - -### param: Clock.jump.time -* since: v1.45 -- `time` <[int]|[string]> - -Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. - -## async method: Clock.next +## async method: Clock.runAllTimers * since: v1.45 - returns: <[int]> -Advances the clock to the the moment of the first scheduled timer, firing it. +Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. +Fake timers must be installed. Returns fake milliseconds since the unix epoch. -**Usage** - -```js -await page.clock.next(); -``` - -```python async -await page.clock.next() -``` - -```python sync -page.clock.next() -``` - -```java -page.clock().next(); -``` - -```csharp -await page.Clock.NextAsync(); -``` - -## async method: Clock.runAll -* since: v1.45 -- returns: <[int]> - -Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. Returns fake milliseconds since the unix epoch. - **Details** This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or the delays in those timers. It runs a maximum of [`option: loopLimit`] times after which it assumes there is an infinite loop of timers and throws an error. -## async method: Clock.runToLast +## async method: Clock.runFor +* since: v1.45 +- returns: <[int]> + +Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. +Fake timers must be installed. +Returns fake milliseconds since the unix epoch. + +**Usage** + +```js +await page.clock.runFor(1000); +await page.clock.runFor('30:00'); +``` + +```python async +await page.clock.run_for(1000); +await page.clock.run_for('30:00') +``` + +```python sync +page.clock.run_for(1000); +page.clock.run_for('30:00') +``` + +```java +page.clock().runFor(1000); +page.clock().runFor("30:00"); +``` + +```csharp +await page.Clock.RunForAsync(1000); +await page.Clock.RunForAsync("30:00"); +``` + +### param: Clock.runFor.time +* since: v1.45 +- `time` <[int]|[string]> + +Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + + +## async method: Clock.runToLastTimer * since: v1.45 - returns: <[int]> This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as necessary. If new timers are added while it is executing they will be run only if they would occur before this time. This is useful when you want to run a test to completion, but the test recursively sets timers that would cause runAll to trigger an infinite loop warning. +Fake timers must be installed. Returns fake milliseconds since the unix epoch. -## async method: Clock.tick +## async method: Clock.runToNextTimer * since: v1.45 - returns: <[int]> -Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Returns fake milliseconds since the unix epoch. +Advances the clock to the the moment of the first scheduled timer, firing it. +Fake timers must be installed. +Returns fake milliseconds since the unix epoch. + + +## async method: Clock.setTime +* since: v1.45 + +Set the clock to the specified time. + +When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as a browser) +being put to sleep and resumed later, skipping intermediary timers. + +### param: Clock.setTime.time +* since: v1.45 +- `time` <[int]|[Date]> + + +## async method: Clock.skipTime +* since: v1.45 +- returns: <[int]> + +Advance the clock by jumping forward in time, equivalent to running [`method: Clock.setTime`] with the new target time. + +When fake timers are installed, [`method: Clock.skipTime`] only fires due timers at most once, while [`method: Clock.runFor`] fires all the timers up to the current time. +Returns fake milliseconds since the unix epoch. **Usage** ```js -await page.clock.tick(1000); -await page.clock.tick('30:00'); +await page.clock.skipTime(1000); +await page.clock.skipTime('30:00'); ``` ```python async -await page.clock.tick(1000); -await page.clock.tick('30:00') +await page.clock.skipTime(1000); +await page.clock.skipTime('30:00') ``` ```python sync -page.clock.tick(1000); -page.clock.tick('30:00') +page.clock.skipTime(1000); +page.clock.skipTime('30:00') ``` ```java -page.clock().tick(1000); -page.clock().tick("30:00"); +page.clock().skipTime(1000); +page.clock().skipTime("30:00"); ``` ```csharp -await page.Clock.TickAsync(1000); -await page.Clock.TickAsync("30:00"); +await page.Clock.SkipTimeAsync(1000); +await page.Clock.SkipTimeAsync("30:00"); ``` -### param: Clock.tick.time +### param: Clock.skipTime.time * since: v1.45 - `time` <[int]|[string]> diff --git a/docs/src/clock.md b/docs/src/clock.md index 95e8df0e56..4339e4bc60 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -21,254 +21,240 @@ Accurately simulating time-dependent behavior is essential for verifying the cor By default, the clock starts at the unix epoch (timestamp of 0). You can override it using the `now` option. ```js -await page.clock.install(); -await page.clock.install({ now: new Date('2020-02-02') }); +await page.clock.setTime(new Date('2020-02-02')); +await page.clock.installFakeTimers(new Date('2020-02-02')); ``` -## Freeze Date.now +## Mock Date.now -Sometimes you only need to fake `Date.now` and no other time-related functions. +Most of the time, you only need to fake `Date.now` and no other time-related functions. That way the time flows naturally, but `Date.now` returns a fixed value. ```html - +
``` ```js -// Initialize clock with a specific time, only fake Date.now. -await page.clock.install({ - now: new Date('2024-01-01T10:00:00Z'), - toFake: ['Date'], -}); +await page.clock.setTime(new Date('2024-02-02T10:00:00')); await page.goto('http://localhost:3333'); -await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:00'); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); + +await page.clock.setTime(new Date('2024-02-02T10:30:00')); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); ``` ```python async -# Initialize clock with a specific time, only fake Date.now. -await page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), - toFake=['Date'], -) +page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)) await page.goto('http://localhost:3333') -locator = page.get_by_test_id('my-time') -await expect(locator).to_have_value('2024-01-01T10:00') +locator = page.get_by_test_id('current-time') +await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') + +page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) +await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') ``` ```python sync -# Initialize clock with a specific time, only fake Date.now. -page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), - to_fake=['Date'], -) +page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)) page.goto('http://localhost:3333') -locator = page.get_by_test_id('my-time') -expect(locator).to_have_value('2024-01-01T10:00') +locator = page.get_by_test_id('current-time') +expect(locator).to_have_text('2/2/2024, 10:00:00 AM') + +page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) +expect(locator).to_have_text('2/2/2024, 10:30:00 AM') ``` ```java -// Initialize clock with a specific time, only fake Date.now. -page.clock().install( - new Clock.InstallOptions() - .setNow(Instant.parse("2024-01-01T10:00:00Z")) - .setToFake(new String[]{"Date"}) -); +page.clock().setTime(Instant.parse("2024-02-02T10:00:00")); page.navigate("http://localhost:3333"); -Locator locator = page.getByTestId("my-time"); -assertThat(locator).hasValue("2024-01-01T10:00"); +Locator locator = page.getByTestId("current-time"); +assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); + +page.clock().setTime(Instant.parse("2024-02-02T10:30:00")); +assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); ``` ```csharp // Initialize clock with a specific time, only fake Date.now. -await page.Clock.InstallAsync( - new ClockInstallOptions - { - Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), - ToFake = new[] { "Date" } - }); +await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)); await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("my-time"); -await Expect(locator).ToHaveValueAsync("2024-01-01T10:00"); +var locator = page.GetByTestId("current-time"); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); + +await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 30, 0, DateTimeKind.Pst)); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); ``` -## Assert page at different points in time +## Mock Date.now consistent with the timers -More often you need to simulate the passage of time to test time-dependent behavior. -You can jump the clock forward in time to simulate the passage of time without waiting for real-time to pass. +Sometimes your timers depend on `Date.now` and are confused when the time stands still. +In cases like this you need to ensure that `Date.now` and timers are consistent. +You can achieve this by installing the fake timers. ```html - +
``` ```js // Initialize clock with a specific time, take full control over time. -await page.clock.install({ now: new Date('2024-01-01T10:00:00Z') }); +await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); await page.goto('http://localhost:3333'); -await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:00'); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); // Fast forward time 30 minutes without firing intermediate timers, as if the user // closed and opened the lid of the laptop. -await page.clock.jump('30:00'); -await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:30'); +await page.clock.skipTime('30:00'); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); ``` ```python async # Initialize clock with a specific time, take full control over time. -await page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +await page.clock.install_fake_timers( + datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst) ) await page.goto('http://localhost:3333') -locator = page.get_by_test_id('my-time') -await expect(locator).to_have_value('2024-01-01T10:00') +locator = page.get_by_test_id('current-time') +await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') # Fast forward time 30 minutes without firing intermediate timers, as if the user # closed and opened the lid of the laptop. -await page.clock.jump('30:00') -await expect(locator).to_have_value('2024-01-01T10:30') +await page.clock.skip_time('30:00') +await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') ``` ```python sync # Initialize clock with a specific time, take full control over time. -page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +page.clock.install_fake_timers( + datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst) ) page.goto('http://localhost:3333') -locator = page.get_by_test_id('my-time') -expect(locator).to_have_value('2024-01-01T10:00') +locator = page.get_by_test_id('current-time') +expect(locator).to_have_text('2/2/2024, 10:00:00 AM') # Fast forward time 30 minutes without firing intermediate timers, as if the user # closed and opened the lid of the laptop. -page.clock.jump('30:00') -expect(locator).to_have_value('2024-01-01T10:30') +page.clock.skip_time('30:00') +expect(locator).to_have_text('2/2/2024, 10:30:00 AM') ``` ```java // Initialize clock with a specific time, take full control over time. -page.clock().install( - new Clock.InstallOptions() - .setNow(Instant.parse("2024-01-01T10:00:00Z")) -); +page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); page.navigate("http://localhost:3333"); -Locator locator = page.getByTestId("my-time"); -assertThat(locator).hasValue("2024-01-01T10:00"); +Locator locator = page.getByTestId("current-time"); +assertThat(locator).hasText("2/2/2024, 10:00:00 AM") // Fast forward time 30 minutes without firing intermediate timers, as if the user // closed and opened the lid of the laptop. -page.clock().jump("30:00"); -assertThat(locator).hasValue("2024-01-01T10:30"); +page.clock().skipTime("30:00"); +assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); ``` ```csharp // Initialize clock with a specific time, take full control over time. -await page.Clock.InstallAsync( - new ClockInstallOptions - { - Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), - }); +await page.Clock.InstallFakeTimersAsync( + new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) +); await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("my-time"); -await Expect(locator).ToHaveValueAsync("2024-01-01T10:00"); +var locator = page.GetByTestId("current-time"); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); // Fast forward time 30 minutes without firing intermediate timers, as if the user // closed and opened the lid of the laptop. -await page.Clock.JumpAsync("30:00"); -await Expect(locator).ToHaveValueAsync("2024-01-01T10:30"); +await page.Clock.SkipTimeAsync("30:00"); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); ``` ## Tick through time manually -In some cases, you may want to tick through time manually, firing all timers in the process. -This can be useful when you want to simulate the passage of time in a controlled manner. +In rare cases, you may want to tick through time manually, firing all timers and animation frames in the process to achieve a fine-grained +control over the passage of time. ```html - +
``` ```js // Initialize clock with a specific time, take full control over time. -await page.clock.install({ now: new Date('2024-01-01T10:00:00Z') }); +await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); await page.goto('http://localhost:3333'); // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. -await page.clock.tick(2000); +await page.clock.runFor(2000); +await expect(locator).to_have_text('2/2/2024, 10:00:02 AM') ``` ```python async # Initialize clock with a specific time, take full control over time. -await page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +await page.clock.install_fake_timers( + datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst), ) await page.goto('http://localhost:3333') -locator = page.get_by_test_id('my-time') +locator = page.get_by_test_id('current-time') # Tick through time manually, firing all timers in the process. # In this case, time will be updated in the screen 2 times. -await page.clock.tick(2000) +await page.clock.run_for(2000) +await expect(locator).to_have_text('2/2/2024, 10:00:02 AM') ``` ```python sync # Initialize clock with a specific time, take full control over time. -page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +page.clock.install_fake_timers( + datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst), ) page.goto('http://localhost:3333') -locator = page.get_by_test_id('my-time') +locator = page.get_by_test_id('current-time') # Tick through time manually, firing all timers in the process. # In this case, time will be updated in the screen 2 times. -page.clock.tick(2000) +page.clock.run_for(2000) +expect(locator).to_have_text('2/2/2024, 10:00:02 AM') ``` ```java // Initialize clock with a specific time, take full control over time. -page.clock().install( - new Clock.InstallOptions() - .setNow(Instant.parse("2024-01-01T10:00:00Z")) -); +page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); page.navigate("http://localhost:3333"); -Locator locator = page.getByTestId("my-time"); +Locator locator = page.getByTestId("current-time"); // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. -page.clock().tick(2000); +page.clock().runFor(2000); +assertThat(locator).hasText("2/2/2024, 10:00:02 AM"); ``` ```csharp // Initialize clock with a specific time, take full control over time. -await page.Clock.InstallAsync( - new ClockInstallOptions - { - Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), - }); +await page.Clock.InstallFakeTimersAsync( + new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) +); await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("my-time"); +var locator = page.GetByTestId("current-time"); // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. -await page.Clock.TickAsync(2000); +await page.Clock.RunForAsync(2000); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM"); ``` diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 802939295c..ed903b674e 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -86,7 +86,7 @@ export class Browser extends ChannelOwner implements ap const context = BrowserContext.from(response.context); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); if (!forReuse && !!process.env.PW_FREEZE_TIME) - await this._wrapApiCall(async () => { await context.clock.install(); }, true); + await this._wrapApiCall(async () => { await context.clock.installFakeTimers(new Date(0)); }, true); return context; } diff --git a/packages/playwright-core/src/client/clock.ts b/packages/playwright-core/src/client/clock.ts index 3287f41dbb..792e36a135 100644 --- a/packages/playwright-core/src/client/clock.ts +++ b/packages/playwright-core/src/client/clock.ts @@ -15,7 +15,6 @@ */ import type * as api from '../../types/types'; -import type * as channels from '@protocol/channels'; import type { BrowserContext } from './browserContext'; export class Clock implements api.Clock { @@ -25,35 +24,41 @@ export class Clock implements api.Clock { this._browserContext = browserContext; } - async install(options?: Omit & { now?: number | Date }) { - const now = options && options.now ? (options.now instanceof Date ? options.now.getTime() : options.now) : undefined; - await this._browserContext._channel.clockInstall({ ...options, now }); + async installFakeTimers(time: number | Date, options: { loopLimit?: number } = {}) { + const timeMs = time instanceof Date ? time.getTime() : time; + await this._browserContext._channel.clockInstallFakeTimers({ time: timeMs, loopLimit: options.loopLimit }); } - async jump(time: number | string) { - await this._browserContext._channel.clockJump({ + async runAllTimers(): Promise { + const result = await this._browserContext._channel.clockRunAllTimers(); + return result.fakeTime; + } + + async runFor(time: number | string): Promise { + const result = await this._browserContext._channel.clockRunFor({ timeNumber: typeof time === 'number' ? time : undefined, timeString: typeof time === 'string' ? time : undefined }); - } - - async next(): Promise { - const result = await this._browserContext._channel.clockNext(); return result.fakeTime; } - async runAll(): Promise { - const result = await this._browserContext._channel.clockRunAll(); + async runToLastTimer(): Promise { + const result = await this._browserContext._channel.clockRunToLastTimer(); return result.fakeTime; } - async runToLast(): Promise { - const result = await this._browserContext._channel.clockRunToLast(); + async runToNextTimer(): Promise { + const result = await this._browserContext._channel.clockRunToNextTimer(); return result.fakeTime; } - async tick(time: number | string): Promise { - const result = await this._browserContext._channel.clockTick({ + async setTime(time: number | Date) { + const timeMs = time instanceof Date ? time.getTime() : time; + await this._browserContext._channel.clockSetTime({ time: timeMs }); + } + + async skipTime(time: number | string) { + const result = await this._browserContext._channel.clockSkipTime({ timeNumber: typeof time === 'number' ? time : undefined, timeString: typeof time === 'string' ? time : undefined }); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index ec1d4bffe2..0a60a92ebb 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -963,36 +963,39 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({ enabled: tBoolean, }); scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({})); -scheme.BrowserContextClockInstallParams = tObject({ - now: tOptional(tNumber), - toFake: tOptional(tArray(tString)), +scheme.BrowserContextClockInstallFakeTimersParams = tObject({ + time: tNumber, loopLimit: tOptional(tNumber), - shouldAdvanceTime: tOptional(tBoolean), - advanceTimeDelta: tOptional(tNumber), }); -scheme.BrowserContextClockInstallResult = tOptional(tObject({})); -scheme.BrowserContextClockJumpParams = tObject({ +scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({})); +scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({})); +scheme.BrowserContextClockRunAllTimersResult = tObject({ + fakeTime: tNumber, +}); +scheme.BrowserContextClockRunForParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); -scheme.BrowserContextClockJumpResult = tOptional(tObject({})); -scheme.BrowserContextClockNextParams = tOptional(tObject({})); -scheme.BrowserContextClockNextResult = tObject({ +scheme.BrowserContextClockRunForResult = tObject({ fakeTime: tNumber, }); -scheme.BrowserContextClockRunAllParams = tOptional(tObject({})); -scheme.BrowserContextClockRunAllResult = tObject({ +scheme.BrowserContextClockRunToLastTimerParams = tOptional(tObject({})); +scheme.BrowserContextClockRunToLastTimerResult = tObject({ fakeTime: tNumber, }); -scheme.BrowserContextClockRunToLastParams = tOptional(tObject({})); -scheme.BrowserContextClockRunToLastResult = tObject({ +scheme.BrowserContextClockRunToNextTimerParams = tOptional(tObject({})); +scheme.BrowserContextClockRunToNextTimerResult = tObject({ fakeTime: tNumber, }); -scheme.BrowserContextClockTickParams = tObject({ +scheme.BrowserContextClockSetTimeParams = tObject({ + time: tNumber, +}); +scheme.BrowserContextClockSetTimeResult = tOptional(tObject({})); +scheme.BrowserContextClockSkipTimeParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); -scheme.BrowserContextClockTickResult = tObject({ +scheme.BrowserContextClockSkipTimeResult = tObject({ fakeTime: tNumber, }); scheme.PageInitializer = tObject({ diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index fee151b407..f4d96965f4 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -14,59 +14,94 @@ * limitations under the License. */ -import type * as channels from '@protocol/channels'; import type { BrowserContext } from './browserContext'; import * as fakeTimersSource from '../generated/fakeTimersSource'; export class Clock { private _browserContext: BrowserContext; - private _installed = false; + private _scriptInjected = false; + private _fakeTimersInstalled = false; + private _now = 0; constructor(browserContext: BrowserContext) { this._browserContext = browserContext; } - async install(params: channels.BrowserContextClockInstallOptions) { - if (this._installed) - throw new Error('Cannot install more than one clock per context'); - this._installed = true; + async installFakeTimers(time: number, loopLimit: number | undefined) { + await this._injectScriptIfNeeded(); + await this._addAndEvaluate(`(() => { + globalThis.__pwFakeTimers.clock?.uninstall(); + globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, loopLimit })}); + })();`); + this._now = time; + this._fakeTimersInstalled = true; + } + + async runToNextTimer(): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.next()`); + this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.nextAsync()`); + return this._now; + } + + async runAllTimers(): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runAll()`); + this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runAllAsync()`); + return this._now; + } + + async runToLastTimer(): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runToLast()`); + this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runToLastAsync()`); + return this._now; + } + + async setTime(time: number) { + if (this._fakeTimersInstalled) { + const jump = time - this._now; + if (jump < 0) + throw new Error('Unable to set time into the past when fake timers are installed'); + await this._addAndEvaluate(`globalThis.__pwFakeTimers.clock.jump(${jump})`); + this._now = time; + return this._now; + } + + await this._injectScriptIfNeeded(); + await this._addAndEvaluate(`(() => { + globalThis.__pwFakeTimers.clock?.uninstall(); + globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); + })();`); + this._now = time; + return this._now; + } + + async skipTime(time: number | string) { + const delta = parseTime(time); + await this.setTime(this._now + delta); + return this._now; + } + + async runFor(time: number | string): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.tick(${JSON.stringify(time)})`); + this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.tickAsync(${JSON.stringify(time)})`); + return this._now; + } + + private async _injectScriptIfNeeded() { + if (this._scriptInjected) + return; + this._scriptInjected = true; const script = `(() => { const module = {}; ${fakeTimersSource.source} - globalThis.__pwFakeTimers = (module.exports.install())(${JSON.stringify(params)}); + globalThis.__pwFakeTimers = (module.exports.inject())(); })();`; await this._addAndEvaluate(script); } - async jump(time: number | string) { - this._assertInstalled(); - await this._addAndEvaluate(`globalThis.__pwFakeTimers.jump(${JSON.stringify(time)})`); - } - - async next(): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.next()`); - return await this._evaluateInFrames(`globalThis.__pwFakeTimers.nextAsync()`); - } - - async runAll(): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runAll()`); - return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runAllAsync()`); - } - - async runToLast(): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runToLast()`); - return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runToLastAsync()`); - } - - async tick(time: number | string): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.tick(${JSON.stringify(time)})`); - return await this._evaluateInFrames(`globalThis.__pwFakeTimers.tickAsync(${JSON.stringify(time)})`); - } - private async _addAndEvaluate(script: string) { await this._browserContext.addInitScript(script); return await this._evaluateInFrames(script); @@ -79,7 +114,32 @@ export class Clock { } private _assertInstalled() { - if (!this._installed) + if (!this._fakeTimersInstalled) throw new Error('Clock is not installed'); } } + +// Taken from sinonjs/fake-timerss-src. +function parseTime(time: string | number): number { + if (typeof time === 'number') + return time; + if (!time) + return 0; + + const strings = time.split(':'); + const l = strings.length; + let i = l; + let ms = 0; + let parsed; + + if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(time)) + throw new Error(`tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits`); + + while (i--) { + parsed = parseInt(strings[i], 10); + if (parsed >= 60) + throw new Error(`Invalid time ${time}`); + ms += parsed * Math.pow(60, l - i - 1); + } + return ms * 1000; +} diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 419b99f205..dd1f61f57b 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -312,28 +312,32 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.clock.install(params); + async clockInstallFakeTimers(params: channels.BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.installFakeTimers(params.time, params.loopLimit); } - async clockJump(params: channels.BrowserContextClockJumpParams, metadata?: CallMetadata | undefined): Promise { - await this._context.clock.jump(params.timeString || params.timeNumber || 0); + async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.runAllTimers() }; } - async clockNext(params: channels.BrowserContextClockNextParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.next() }; + async clockRunToLastTimer(params: channels.BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.runToLastTimer() }; } - async clockRunAll(params: channels.BrowserContextClockRunAllParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runAll() }; + async clockRunToNextTimer(params: channels.BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.runToNextTimer() }; } - async clockRunToLast(params: channels.BrowserContextClockRunToLastParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runToLast() }; + async clockSetTime(params: channels.BrowserContextClockSetTimeParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.setTime(params.time); } - async clockTick(params: channels.BrowserContextClockTickParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.tick(params.timeString || params.timeNumber || 0) }; + async clockSkipTime(params: channels.BrowserContextClockSkipTimeParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.skipTime(params.timeString || params.timeNumber || 0) }; + } + + async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.runFor(params.timeString || params.timeNumber || 0) }; } async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise { diff --git a/packages/playwright-core/src/server/injected/fakeTimers.ts b/packages/playwright-core/src/server/injected/fakeTimers.ts index e3d8e5a54d..44974ab8e1 100644 --- a/packages/playwright-core/src/server/injected/fakeTimers.ts +++ b/packages/playwright-core/src/server/injected/fakeTimers.ts @@ -16,9 +16,8 @@ // @ts-ignore import SinonFakeTimers from '../../third_party/fake-timers-src'; -import type * as channels from '@protocol/channels'; -export function install(params: channels.BrowserContextClockInstallOptions) { +export function inject() { // eslint-disable-next-line no-restricted-globals const window = globalThis; const builtin = { @@ -34,7 +33,7 @@ export function install(params: channels.BrowserContextClockInstallOptions) { Intl: window.Intl, Date: window.Date, }; - const result = SinonFakeTimers.install(params); + const result = SinonFakeTimers; result.builtin = builtin; return result; } diff --git a/packages/playwright-core/src/third_party/fake-timers-src.js b/packages/playwright-core/src/third_party/fake-timers-src.js index d4bfdd7305..9602123052 100644 --- a/packages/playwright-core/src/third_party/fake-timers-src.js +++ b/packages/playwright-core/src/third_party/fake-timers-src.js @@ -1654,6 +1654,7 @@ function withGlobal(_global) { * @returns {Clock} */ function install(config) { + console.log('INSTALL', config); if ( arguments.length > 1 || config instanceof Date || diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 11305a90d5..92a3b130df 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17247,81 +17247,35 @@ export interface BrowserServer { */ export interface Clock { /** - * Creates a clock and installs it globally. - * - * **Usage** - * - * ```js - * await page.clock.install(); - * await page.clock.install({ now }); - * await page.clock.install({ now, toFake: ['Date'] }); - * ``` + * Install fake implementations for the following time-related functions: + * - `setTimeout` + * - `clearTimeout` + * - `setInterval` + * - `clearInterval` + * - `requestAnimationFrame` + * - `cancelAnimationFrame` + * - `requestIdleCallback` + * - `cancelIdleCallback` + * - `performance` * + * Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, + * and control the behavior of time-dependent functions. See + * [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) and + * [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) for more information. + * @param time Install fake timers with the specified base time. * @param options */ - install(options?: { + installFakeTimers(time: number|Date, options?: { /** - * Relevant only when using with `shouldAdvanceTime`. Increment mocked time by advanceTimeDelta ms every - * advanceTimeDelta ms change in the real system time (default: 20). - */ - advanceTimeDelta?: number; - - /** - * The maximum number of timers that will be run when calling - * [clock.runAll()](https://playwright.dev/docs/api/class-clock#clock-run-all). Defaults to `1000`. + * The maximum number of timers that will be run in + * [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`. */ loopLimit?: number; - - /** - * Install fake timers with the specified unix epoch (default: 0). - */ - now?: number|Date; - - /** - * Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the real system time shift (e.g., the - * mocked time will be incremented by 20ms for every 20ms change in the real system time). Defaults to `false`. - */ - shouldAdvanceTime?: boolean; - - /** - * An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: - * ['setTimeout'] })` will fake only `setTimeout()`. By default, all the methods are faked. - */ - toFake?: Array<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">; }): Promise; - /** - * Advance the clock by jumping forward in time, firing callbacks at most once. This can be used to simulate the JS - * engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers. - * - * **Usage** - * - * ```js - * await page.clock.jump(1000); - * await page.clock.jump('30:00'); - * ``` - * - * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are - * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. - */ - jump(time: number|string): Promise; - - /** - * Advances the clock to the the moment of the first scheduled timer, firing it. Returns fake milliseconds since the - * unix epoch. - * - * **Usage** - * - * ```js - * await page.clock.next(); - * ``` - * - */ - next(): Promise; - /** * Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be - * run as well. Returns fake milliseconds since the unix epoch. + * run as well. Fake timers must be installed. Returns fake milliseconds since the unix epoch. * * **Details** * @@ -17329,31 +17283,68 @@ export interface Clock { * or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there is an infinite * loop of timers and throws an error. */ - runAll(): Promise; + runAllTimers(): Promise; /** - * This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as - * necessary. If new timers are added while it is executing they will be run only if they would occur before this - * time. This is useful when you want to run a test to completion, but the test recursively sets timers that would - * cause runAll to trigger an infinite loop warning. Returns fake milliseconds since the unix epoch. - */ - runToLast(): Promise; - - /** - * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Returns fake - * milliseconds since the unix epoch. + * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Fake timers must + * be installed. Returns fake milliseconds since the unix epoch. * * **Usage** * * ```js - * await page.clock.tick(1000); - * await page.clock.tick('30:00'); + * await page.clock.runFor(1000); + * await page.clock.runFor('30:00'); * ``` * * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. */ - tick(time: number|string): Promise; + runFor(time: number|string): Promise; + + /** + * This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as + * necessary. If new timers are added while it is executing they will be run only if they would occur before this + * time. This is useful when you want to run a test to completion, but the test recursively sets timers that would + * cause runAll to trigger an infinite loop warning. Fake timers must be installed. Returns fake milliseconds since + * the unix epoch. + */ + runToLastTimer(): Promise; + + /** + * Advances the clock to the the moment of the first scheduled timer, firing it. Fake timers must be installed. + * Returns fake milliseconds since the unix epoch. + */ + runToNextTimer(): Promise; + + /** + * Set the clock to the specified time. + * + * When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as + * a browser) being put to sleep and resumed later, skipping intermediary timers. + * @param time + */ + setTime(time: number|Date): Promise; + + /** + * Advance the clock by jumping forward in time, equivalent to running + * [clock.setTime(time)](https://playwright.dev/docs/api/class-clock#clock-set-time) with the new target time. + * + * When fake timers are installed, [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) + * only fires due timers at most once, while + * [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) fires all the timers up to the + * current time. Returns fake milliseconds since the unix epoch. + * + * **Usage** + * + * ```js + * await page.clock.skipTime(1000); + * await page.clock.skipTime('30:00'); + * ``` + * + * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are + * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + */ + skipTime(time: number|string): Promise; } /** diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index d5dd5c3e33..c8523e8de5 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1460,12 +1460,13 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise; createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise; updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise; - clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise; - clockJump(params: BrowserContextClockJumpParams, metadata?: CallMetadata): Promise; - clockNext(params?: BrowserContextClockNextParams, metadata?: CallMetadata): Promise; - clockRunAll(params?: BrowserContextClockRunAllParams, metadata?: CallMetadata): Promise; - clockRunToLast(params?: BrowserContextClockRunToLastParams, metadata?: CallMetadata): Promise; - clockTick(params: BrowserContextClockTickParams, metadata?: CallMetadata): Promise; + clockInstallFakeTimers(params: BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata): Promise; + clockRunAllTimers(params?: BrowserContextClockRunAllTimersParams, metadata?: CallMetadata): Promise; + clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise; + clockRunToLastTimer(params?: BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata): Promise; + clockRunToNextTimer(params?: BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata): Promise; + clockSetTime(params: BrowserContextClockSetTimeParams, metadata?: CallMetadata): Promise; + clockSkipTime(params: BrowserContextClockSkipTimeParams, metadata?: CallMetadata): Promise; } export type BrowserContextBindingCallEvent = { binding: BindingCallChannel, @@ -1754,54 +1755,56 @@ export type BrowserContextUpdateSubscriptionOptions = { }; export type BrowserContextUpdateSubscriptionResult = void; -export type BrowserContextClockInstallParams = { - now?: number, - toFake?: string[], +export type BrowserContextClockInstallFakeTimersParams = { + time: number, loopLimit?: number, - shouldAdvanceTime?: boolean, - advanceTimeDelta?: number, }; -export type BrowserContextClockInstallOptions = { - now?: number, - toFake?: string[], +export type BrowserContextClockInstallFakeTimersOptions = { loopLimit?: number, - shouldAdvanceTime?: boolean, - advanceTimeDelta?: number, }; -export type BrowserContextClockInstallResult = void; -export type BrowserContextClockJumpParams = { - timeNumber?: number, - timeString?: string, -}; -export type BrowserContextClockJumpOptions = { - timeNumber?: number, - timeString?: string, -}; -export type BrowserContextClockJumpResult = void; -export type BrowserContextClockNextParams = {}; -export type BrowserContextClockNextOptions = {}; -export type BrowserContextClockNextResult = { +export type BrowserContextClockInstallFakeTimersResult = void; +export type BrowserContextClockRunAllTimersParams = {}; +export type BrowserContextClockRunAllTimersOptions = {}; +export type BrowserContextClockRunAllTimersResult = { fakeTime: number, }; -export type BrowserContextClockRunAllParams = {}; -export type BrowserContextClockRunAllOptions = {}; -export type BrowserContextClockRunAllResult = { - fakeTime: number, -}; -export type BrowserContextClockRunToLastParams = {}; -export type BrowserContextClockRunToLastOptions = {}; -export type BrowserContextClockRunToLastResult = { - fakeTime: number, -}; -export type BrowserContextClockTickParams = { +export type BrowserContextClockRunForParams = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockTickOptions = { +export type BrowserContextClockRunForOptions = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockTickResult = { +export type BrowserContextClockRunForResult = { + fakeTime: number, +}; +export type BrowserContextClockRunToLastTimerParams = {}; +export type BrowserContextClockRunToLastTimerOptions = {}; +export type BrowserContextClockRunToLastTimerResult = { + fakeTime: number, +}; +export type BrowserContextClockRunToNextTimerParams = {}; +export type BrowserContextClockRunToNextTimerOptions = {}; +export type BrowserContextClockRunToNextTimerResult = { + fakeTime: number, +}; +export type BrowserContextClockSetTimeParams = { + time: number, +}; +export type BrowserContextClockSetTimeOptions = { + +}; +export type BrowserContextClockSetTimeResult = void; +export type BrowserContextClockSkipTimeParams = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockSkipTimeOptions = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockSkipTimeResult = { fakeTime: number, }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 8056debdcd..7f6bd3bfe1 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1204,34 +1204,35 @@ BrowserContext: - requestFailed enabled: boolean - clockInstall: + clockInstallFakeTimers: parameters: - now: number? - toFake: - type: array? - items: string + time: number loopLimit: number? - shouldAdvanceTime: boolean? - advanceTimeDelta: number? - clockJump: + clockRunAllTimers: + returns: + fakeTime: number + + clockRunFor: parameters: timeNumber: number? timeString: string? - - clockNext: returns: fakeTime: number - clockRunAll: + clockRunToLastTimer: returns: fakeTime: number - clockRunToLast: + clockRunToNextTimer: returns: fakeTime: number - clockTick: + clockSetTime: + parameters: + time: number + + clockSkipTime: parameters: timeNumber: number? timeString: string? diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index 7e001e6641..ccc9e8773c 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -34,87 +34,87 @@ const it = test.extend<{ calls: { params: any[] }[] }>({ } }); -it.describe('tick', () => { +it.describe('runFor', () => { it('triggers immediately without specified delay', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub); }); - await page.clock.tick(0); + await page.clock.runFor(0); expect(calls).toHaveLength(1); }); it('does not trigger without sufficient delay', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); - await page.clock.tick(10); + await page.clock.runFor(10); expect(calls).toEqual([]); }); it('triggers after sufficient delay', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls).toHaveLength(1); }); it('triggers simultaneous timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); }); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls).toHaveLength(2); }); it('triggers multiple simultaneous timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); setTimeout(window.stub, 99); setTimeout(window.stub, 100); }); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls.length).toBe(4); }); it('waits after setTimeout was called', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 150); }); - await page.clock.tick(50); + await page.clock.runFor(50); expect(calls).toEqual([]); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls).toHaveLength(1); }); it('triggers event when some throw', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { throw new Error(); }, 100); setTimeout(window.stub, 120); }); - await expect(page.clock.tick(120)).rejects.toThrow(); + await expect(page.clock.runFor(120)).rejects.toThrow(); expect(calls).toHaveLength(1); }); it('creates updated Date while ticking', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(() => { window.stub(new Date().getTime()); }, 10); }); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls).toEqual([ { params: [10] }, { params: [20] }, @@ -130,117 +130,117 @@ it.describe('tick', () => { }); it('passes 8 seconds', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 4000); }); - await page.clock.tick('08'); + await page.clock.runFor('08'); expect(calls.length).toBe(2); }); it('passes 1 minute', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 6000); }); - await page.clock.tick('01:00'); + await page.clock.runFor('01:00'); expect(calls.length).toBe(10); }); it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 10000); }); - await page.clock.tick('02:34:10'); + await page.clock.runFor('02:34:10'); expect(calls.length).toBe(925); }); it('throws for invalid format', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 10000); }); - await expect(page.clock.tick('12:02:34:10')).rejects.toThrow(); + await expect(page.clock.runFor('12:02:34:10')).rejects.toThrow(); expect(calls).toEqual([]); }); it('returns the current now value', async ({ page }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); const value = 200; - await page.clock.tick(value); + await page.clock.runFor(value); expect(await page.evaluate(() => Date.now())).toBe(value); }); }); -it.describe('jump', () => { +it.describe('skipTime', () => { it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub('should not be logged'); }, 1000); }); - await page.clock.jump(500); + await page.clock.skipTime(500); expect(calls).toEqual([]); }); it('pushes back execution time for skipped timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(Date.now()); }, 1000); }); - await page.clock.jump(2000); + await page.clock.skipTime(2000); expect(calls).toEqual([{ params: [2000] }]); }); it('supports string time arguments', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(Date.now()); }, 100000); // 100000 = 1:40 }); - await page.clock.jump('01:50'); + await page.clock.skipTime('01:50'); expect(calls).toEqual([{ params: [110000] }]); }); }); -it.describe('runAll', () => { +it.describe('runAllTimers', () => { it('if there are no timers just return', async ({ page }) => { - await page.clock.install(); - await page.clock.runAll(); + await page.clock.installFakeTimers(0); + await page.clock.runAllTimers(); }); it('runs all timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 50); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls.length).toBe(2); }); it('new timers added while running are also run', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { setTimeout(window.stub, 50); }, 10); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls.length).toBe(1); }); it('new timers added in promises while running are also run', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => { @@ -248,12 +248,12 @@ it.describe('runAll', () => { }); }, 10); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls.length).toBe(1); }); it('throws before allowing infinite recursion', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const recursiveCallback = () => { window.stub(); @@ -261,12 +261,12 @@ it.describe('runAll', () => { }; setTimeout(recursiveCallback, 10); }); - await expect(page.clock.runAll()).rejects.toThrow(); + await expect(page.clock.runAllTimers()).rejects.toThrow(); expect(calls).toHaveLength(1000); }); it('throws before allowing infinite recursion from promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const recursiveCallback = () => { window.stub(); @@ -276,33 +276,33 @@ it.describe('runAll', () => { }; setTimeout(recursiveCallback, 10); }); - await expect(page.clock.runAll()).rejects.toThrow(); + await expect(page.clock.runAllTimers()).rejects.toThrow(); expect(calls).toHaveLength(1000); }); it('the loop limit can be set when creating a clock', async ({ page, calls }) => { - await page.clock.install({ loopLimit: 1 }); + await page.clock.installFakeTimers(0, { loopLimit: 1 }); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 50); }); - await expect(page.clock.runAll()).rejects.toThrow(); + await expect(page.clock.runAllTimers()).rejects.toThrow(); expect(calls).toHaveLength(1); }); it('should settle user-created promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => window.stub()); }, 55); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls).toHaveLength(1); }); it('should settle nested user-created promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => { @@ -312,17 +312,17 @@ it.describe('runAll', () => { }); }, 55); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls).toHaveLength(1); }); it('should settle local promises before firing timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { void Promise.resolve().then(() => window.stub(1)); setTimeout(() => window.stub(2), 55); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls).toEqual([ { params: [1] }, { params: [2] }, @@ -330,69 +330,69 @@ it.describe('runAll', () => { }); }); -it.describe('runToLast', () => { +it.describe('runToLastTimer', () => { it('returns current time when there are no timers', async ({ page }) => { - await page.clock.install(); - const time = await page.clock.runToLast(); + await page.clock.installFakeTimers(0); + const time = await page.clock.runToLastTimer(); expect(time).toBe(0); }); it('runs all existing timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 50); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(2); }); it('returns time of the last timer', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 50); }); - const time = await page.clock.runToLast(); + const time = await page.clock.runToLastTimer(); expect(time).toBe(50); }); it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 10); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(2); }); it('new timers added with a call time later than the last existing timer are NOT run', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(); setTimeout(window.stub, 50); }, 10); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(1); }); it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(() => { setTimeout(window.stub, 50); }, 10); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(2); }); it('new timers cannot cause an infinite loop', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const recursiveCallback = () => { window.stub(); @@ -401,24 +401,24 @@ it.describe('runToLast', () => { setTimeout(recursiveCallback, 0); setTimeout(window.stub, 100); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(102); }); it('should support clocks with start time', async ({ page, calls }) => { - await page.clock.install({ now: 200 }); + await page.clock.installFakeTimers(200); await page.evaluate(async () => { setTimeout(function cb() { window.stub(); setTimeout(cb, 50); }, 50); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(1); }); it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const recursiveCallback = () => { void Promise.resolve().then(() => { @@ -428,23 +428,23 @@ it.describe('runToLast', () => { setTimeout(recursiveCallback, 0); setTimeout(window.stub, 100); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(1); }); it('should settle user-created promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => window.stub()); }, 55); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(1); }); it('should settle nested user-created promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => { @@ -454,17 +454,17 @@ it.describe('runToLast', () => { }); }, 55); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(1); }); it('should settle local promises before firing timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { void Promise.resolve().then(() => window.stub(1)); setTimeout(() => window.stub(2), 55); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls).toEqual([ { params: [1] }, { params: [2] }, @@ -472,14 +472,14 @@ it.describe('runToLast', () => { }); it('should settle user-created promises before firing more timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => window.stub(1)); }, 55); setTimeout(() => window.stub(2), 75); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls).toEqual([ { params: [1] }, { params: [2] }, @@ -489,152 +489,92 @@ it.describe('runToLast', () => { it.describe('stubTimers', () => { it('sets initial timestamp', async ({ page, calls }) => { - await page.clock.install({ now: 1400 }); + await page.clock.installFakeTimers(1400); expect(await page.evaluate(() => Date.now())).toBe(1400); }); it('replaces global setTimeout', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 1000); }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(calls.length).toBe(1); }); it('global fake setTimeout should return id', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); const to = await page.evaluate(() => setTimeout(window.stub, 1000)); expect(typeof to).toBe('number'); }); it('replaces global clearTimeout', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const to = setTimeout(window.stub, 1000); clearTimeout(to); }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(calls).toEqual([]); }); it('replaces global setInterval', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 500); }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(calls.length).toBe(2); }); it('replaces global clearInterval', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const to = setInterval(window.stub, 500); clearInterval(to); }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(calls).toEqual([]); }); it('replaces global performance.now', async ({ page }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); const promise = page.evaluate(async () => { const prev = performance.now(); await new Promise(f => setTimeout(f, 1000)); const next = performance.now(); return { prev, next }; }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(await promise).toEqual({ prev: 0, next: 1000 }); }); it('fakes Date constructor', async ({ page }) => { - await page.clock.install({ now: 0 }); + await page.clock.installFakeTimers(0); const now = await page.evaluate(() => new Date().getTime()); expect(now).toBe(0); }); - - it('does not fake methods not provided', async ({ page }) => { - await page.clock.install({ - now: 0, - toFake: ['Date'], - }); - - // Should not stall. - await page.evaluate(() => { - return new Promise(f => setTimeout(f, 1)); - }); - }); -}); - -it.describe('shouldAdvanceTime', () => { - it('should create an auto advancing timer', async ({ page, calls }) => { - const testDelay = 29; - const now = new Date('2015-09-25'); - await page.clock.install({ now, shouldAdvanceTime: true }); - const pageNow = await page.evaluate(() => Date.now()); - expect(pageNow).toBe(1443139200000); - - await page.evaluate(async testDelay => { - return new Promise(f => { - const timeoutStarted = Date.now(); - setTimeout(() => { - window.stub(Date.now() - timeoutStarted); - f(); - }, testDelay); - }); - }, testDelay); - - expect(calls).toEqual([ - { params: [testDelay] } - ]); - }); - - it('should test setInterval', async ({ page, calls }) => { - const now = new Date('2015-09-25'); - await page.clock.install({ now, shouldAdvanceTime: true }); - - const timeDifference = await page.evaluate(async () => { - return new Promise(f => { - const interval = 20; - const cyclesToTrigger = 3; - const timeoutStarted = Date.now(); - let intervalsTriggered = 0; - const intervalId = setInterval(() => { - if (++intervalsTriggered === cyclesToTrigger) { - clearInterval(intervalId); - const timeDifference = Date.now() - timeoutStarted; - f(timeDifference - interval * cyclesToTrigger); - } - }, interval); - }); - }); - - expect(timeDifference).toBe(0); - }); }); it.describe('popup', () => { it('should tick after popup', async ({ page }) => { const now = new Date('2015-09-25'); - await page.clock.install({ now }); + await page.clock.installFakeTimers(now); const [popup] = await Promise.all([ page.waitForEvent('popup'), page.evaluate(() => window.open('about:blank')), ]); const popupTime = await popup.evaluate(() => Date.now()); expect(popupTime).toBe(now.getTime()); - await page.clock.tick(1000); + await page.clock.runFor(1000); const popupTimeAfter = await popup.evaluate(() => Date.now()); expect(popupTimeAfter).toBe(now.getTime() + 1000); }); it('should tick before popup', async ({ page, browserName }) => { - it.skip(browserName === 'chromium'); const now = new Date('2015-09-25'); - await page.clock.install({ now }); - const newNow = await page.clock.tick(1000); + await page.clock.installFakeTimers(now); + const newNow = await page.clock.runFor(1000); expect(newNow).toBe(now.getTime() + 1000); const [popup] = await Promise.all([ @@ -646,18 +586,18 @@ it.describe('popup', () => { }); }); -it.describe('next', () => { +it.describe('runToNextTimer', () => { it('triggers the next timer', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); - expect(await page.clock.next()).toBe(100); + expect(await page.clock.runToNextTimer()).toBe(100); expect(calls).toHaveLength(1); }); it('does not trigger simultaneous timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(() => { setTimeout(() => { window.stub(); @@ -667,12 +607,12 @@ it.describe('next', () => { }, 100); }); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(1); }); it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(); @@ -688,18 +628,18 @@ it.describe('next', () => { }, 100); }); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(1); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(2); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(3); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(4); }); it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { window.stub(1); setTimeout(() => { @@ -707,20 +647,82 @@ it.describe('next', () => { }, 0); }); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toEqual([{ params: [1] }]); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toEqual([{ params: [1] }, { params: [2] }]); }); it('throws exception thrown by timer', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { throw new Error(); }, 100); }); - await expect(page.clock.next()).rejects.toThrow(); + await expect(page.clock.runToNextTimer()).rejects.toThrow(); + }); +}); + +it.describe('setTime', () => { + it('does not fake methods', async ({ page }) => { + await page.clock.setTime(0); + + // Should not stall. + await page.evaluate(() => { + return new Promise(f => setTimeout(f, 1)); + }); + }); + + it('allows setting time multiple times', async ({ page, calls }) => { + await page.clock.setTime(100); + expect(await page.evaluate(() => Date.now())).toBe(100); + await page.clock.setTime(200); + expect(await page.evaluate(() => Date.now())).toBe(200); + }); + + it('supports skipTime w/o fake timers', async ({ page }) => { + await page.clock.setTime(100); + expect(await page.evaluate(() => Date.now())).toBe(100); + await page.clock.skipTime(20); + expect(await page.evaluate(() => Date.now())).toBe(120); + }); + + it('allows installing fake timers after settings time', async ({ page, calls }) => { + await page.clock.setTime(100); + expect(await page.evaluate(() => Date.now())).toBe(100); + await page.clock.installFakeTimers(200); + await page.evaluate(async () => { + setTimeout(() => window.stub(Date.now())); + }); + await page.clock.runFor(0); + expect(calls).toEqual([{ params: [200] }]); + }); + + it('allows setting time after installing fake timers', async ({ page, calls }) => { + await page.clock.installFakeTimers(200); + await page.evaluate(async () => { + setTimeout(() => window.stub(Date.now())); + }); + await page.clock.setTime(220); + expect(calls).toEqual([{ params: [220] }]); + }); + + it('does not allow flowing time backwards', async ({ page, calls }) => { + await page.clock.installFakeTimers(200); + await expect(page.clock.setTime(180)).rejects.toThrow(); + }); + + it('should turn setTime into jump', async ({ page, calls }) => { + await page.clock.installFakeTimers(0); + await page.evaluate(async () => { + setTimeout(window.stub, 100); + setTimeout(window.stub, 200); + }); + await page.clock.setTime(100); + expect(calls).toHaveLength(1); + await page.clock.setTime(200); + expect(calls).toHaveLength(2); }); }); From 2d7bbe4d736484e856357bbfe4efb345b3c2633a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 4 Jun 2024 17:57:57 +0200 Subject: [PATCH 021/144] test: unflake should pass "key" attribute from JSX in variable (#31141) --- tests/playwright-test/playwright.ct-react.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/playwright-test/playwright.ct-react.spec.ts b/tests/playwright-test/playwright.ct-react.spec.ts index e7f938fb4a..ed3fa47caf 100644 --- a/tests/playwright-test/playwright.ct-react.spec.ts +++ b/tests/playwright-test/playwright.ct-react.spec.ts @@ -290,9 +290,9 @@ test('should pass "key" attribute from JSX in variable', async ({ runInlineTest ); const button = component.getByRole('button'); - expect(button).toHaveText("1"); + await expect(button).toHaveText("1"); await button.click(); - expect(button).toHaveText("10"); + await expect(button).toHaveText("10"); }); `, }, { workers: 1 }); From 76b25e84cc90fc8dd1b8df714ed8dd5b286a1a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Kondratiuk?= Date: Tue, 4 Jun 2024 12:48:56 -0400 Subject: [PATCH 022/144] docs: Improve clock doc (#31147) --- docs/src/api/class-clock.md | 2 +- docs/src/clock.md | 2 +- packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts | 2 +- packages/playwright-core/types/types.d.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 95fa0b69d3..31115db41b 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -106,7 +106,7 @@ Returns fake milliseconds since the unix epoch. * since: v1.45 - returns: <[int]> -Advances the clock to the the moment of the first scheduled timer, firing it. +Advances the clock to the moment of the first scheduled timer, firing it. Fake timers must be installed. Returns fake milliseconds since the unix epoch. diff --git a/docs/src/clock.md b/docs/src/clock.md index 4339e4bc60..5299145916 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -202,7 +202,7 @@ await page.goto('http://localhost:3333'); // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. await page.clock.runFor(2000); -await expect(locator).to_have_text('2/2/2024, 10:00:02 AM') +await expect(locator).to_have_text('2/2/2024, 10:00:02 AM'); ``` ```python async diff --git a/packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts b/packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts index f72ef27eb4..43b0a45869 100644 --- a/packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts +++ b/packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts @@ -378,7 +378,7 @@ export function tokenize(str1: string): CSSTokenInterface[] { }; const consumeEscape = function() { - // Assume the the current character is the \ + // Assume the current character is the \ // and the next code point is not a newline. consume(); if (hexdigit(code)) { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 92a3b130df..dc91c655f0 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17311,8 +17311,8 @@ export interface Clock { runToLastTimer(): Promise; /** - * Advances the clock to the the moment of the first scheduled timer, firing it. Fake timers must be installed. - * Returns fake milliseconds since the unix epoch. + * Advances the clock to the moment of the first scheduled timer, firing it. Fake timers must be installed. Returns + * fake milliseconds since the unix epoch. */ runToNextTimer(): Promise; From 34dac6523ceafcbaef78aa4cec104aae925c2950 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 4 Jun 2024 20:07:59 +0200 Subject: [PATCH 023/144] docs(test-parameterize): use absolute dotenv import (#31149) --- docs/src/test-parameterize-js.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/test-parameterize-js.md b/docs/src/test-parameterize-js.md index a2e7075f52..e6cc791207 100644 --- a/docs/src/test-parameterize-js.md +++ b/docs/src/test-parameterize-js.md @@ -216,8 +216,8 @@ import { defineConfig } from '@playwright/test'; import dotenv from 'dotenv'; import path from 'path'; -// Read from default ".env" file. -dotenv.config(); +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, '.env') }); // Alternatively, read from "../my.env" file. dotenv.config({ path: path.resolve(__dirname, '..', 'my.env') }); From 9a536b0a56a2d083c0a71dfe968bbce509de14bd Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 4 Jun 2024 21:48:44 +0200 Subject: [PATCH 024/144] fix: add libicu74 dependency to webkit Ubuntu 24.04 (#31152) --- packages/playwright-core/src/server/registry/nativeDeps.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/playwright-core/src/server/registry/nativeDeps.ts b/packages/playwright-core/src/server/registry/nativeDeps.ts index 81813880c2..0f67455d26 100644 --- a/packages/playwright-core/src/server/registry/nativeDeps.ts +++ b/packages/playwright-core/src/server/registry/nativeDeps.ts @@ -520,6 +520,7 @@ export const deps: any = { 'gstreamer1.0-plugins-bad', 'gstreamer1.0-plugins-base', 'gstreamer1.0-plugins-good', + 'libicu74', 'libatomic1', 'libatk-bridge2.0-0t64', 'libatk1.0-0t64', From f9c268a8e68862000e61d142753e60635495dff4 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 5 Jun 2024 06:19:32 -0700 Subject: [PATCH 025/144] feat(webkit): roll to r2019 (#31167) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 913af15077..2abf7780cc 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2017", + "revision": "2019", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 384eed65eaccc0267952294b04219ff2f3dc8b3d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 5 Jun 2024 09:25:12 -0700 Subject: [PATCH 026/144] chore: organize fake timers (#31156) --- packages/playwright-core/src/server/clock.ts | 2 +- .../src/server/injected/DEPS.list | 3 - .../src/server/injected/fakeTimers.ts | 972 ++++++++- .../src/third_party/fake-timers-src.js | 1776 ----------------- tests/page/page-clock.frozen.spec.ts | 23 + tests/page/page-clock.spec.ts | 13 + utils/build/build.js | 1 + 7 files changed, 980 insertions(+), 1810 deletions(-) delete mode 100644 packages/playwright-core/src/third_party/fake-timers-src.js create mode 100644 tests/page/page-clock.frozen.spec.ts diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index f4d96965f4..a366288476 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -97,7 +97,7 @@ export class Clock { const script = `(() => { const module = {}; ${fakeTimersSource.source} - globalThis.__pwFakeTimers = (module.exports.inject())(); + globalThis.__pwFakeTimers = (module.exports.inject())(globalThis); })();`; await this._addAndEvaluate(script); } diff --git a/packages/playwright-core/src/server/injected/DEPS.list b/packages/playwright-core/src/server/injected/DEPS.list index 32da641c93..7c5f41d8c9 100644 --- a/packages/playwright-core/src/server/injected/DEPS.list +++ b/packages/playwright-core/src/server/injected/DEPS.list @@ -2,6 +2,3 @@ [*] ../isomorphic/ ../../utils/isomorphic - -[fakeTimers.ts] -../../third_party/fake-timers-src diff --git a/packages/playwright-core/src/server/injected/fakeTimers.ts b/packages/playwright-core/src/server/injected/fakeTimers.ts index 44974ab8e1..fbec480980 100644 --- a/packages/playwright-core/src/server/injected/fakeTimers.ts +++ b/packages/playwright-core/src/server/injected/fakeTimers.ts @@ -1,39 +1,951 @@ /** - * Copyright (c) Microsoft Corporation. + * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -// @ts-ignore -import SinonFakeTimers from '../../third_party/fake-timers-src'; +type ClockMethods = { + Date: DateConstructor; + setTimeout: Window['setTimeout']; + clearTimeout: Window['clearTimeout']; + setInterval: Window['setInterval']; + clearInterval: Window['clearInterval']; + requestAnimationFrame?: Window['requestAnimationFrame']; + cancelAnimationFrame?: (id: number) => void; + requestIdleCallback?: Window['requestIdleCallback']; + cancelIdleCallback?: (id: number) => void; + Intl?: typeof Intl; + performance?: Window['performance']; +}; -export function inject() { - // eslint-disable-next-line no-restricted-globals - const window = globalThis; - const builtin = { - setTimeout: window.setTimeout.bind(window), - clearTimeout: window.clearTimeout.bind(window), - setInterval: window.setInterval.bind(window), - clearInterval: window.clearInterval.bind(window), - requestAnimationFrame: window.requestAnimationFrame.bind(window), - cancelAnimationFrame: window.cancelAnimationFrame.bind(window), - requestIdleCallback: window.requestIdleCallback?.bind(window), - cancelIdleCallback: window.cancelIdleCallback?.bind(window), - performance: window.performance, - Intl: window.Intl, - Date: window.Date, +type ClockConfig = { + now?: number | Date; + loopLimit?: number; +}; + +type InstallConfig = ClockConfig & { + toFake?: (keyof ClockMethods)[]; +}; + +enum TimerType { + Timeout = 'Timeout', + Interval = 'Interval', + Immediate = 'Immediate', + AnimationFrame = 'AnimationFrame', + IdleCallback = 'IdleCallback', +} + +type Timer = { + type: TimerType; + func: TimerHandler; + args: any[]; + delay: number; + callAt: number; + createdAt: number; + id: number; + error?: Error; +}; + +interface Embedder { + postTask(task: () => void): void; + postTaskPeriodically(task: () => void, delay: number): () => void; +} + +class Clock { + readonly start: number; + private _now: number; + private _loopLimit: number; + private _jobs: Timer[] = []; + private _adjustedSystemTime = 0; + private _duringTick = false; + private _timers = new Map(); + private _isNearInfiniteLimit = false; + private _uniqueTimerId = idCounterStart; + private _embedder: Embedder; + readonly disposables: (() => void)[] = []; + + constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) { + const start = Math.floor(getEpoch(startDate)); + this.start = start; + this._now = start; + this._embedder = embedder; + this._loopLimit = loopLimit; + } + + uninstall() { + this.disposables.forEach(dispose => dispose()); + this.disposables.length = 0; + } + + now(): number { + return this._now; + } + + performanceNow(): DOMHighResTimeStamp { + const millisSinceStart = this._now - this._adjustedSystemTime - this.start; + const secsSinceStart = Math.floor(millisSinceStart / 1000); + const millis = secsSinceStart * 1000; + return millis; + } + + private _doTick(tickValue: number | string, isAsync: boolean, resolve?: (time: number) => void, reject?: (error: Error) => void): number | undefined { + const msFloat = + typeof tickValue === 'number' + ? tickValue + : parseTime(tickValue); + const ms = Math.floor(msFloat); + let tickTo = this._now + ms; + + if (msFloat < 0) + throw new TypeError('Negative ticks are not supported'); + + let tickFrom = this._now; + let previous = this._now; + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let timer; + let firstException: Error; + let oldNow: number; + let nextPromiseTick: (() => void) | null; + let compensationCheck: () => void; + let postTimerCall: () => void; + + /* eslint-enable prefer-const */ + + this._duringTick = true; + + // perform microtasks + oldNow = this._now; + this._runJobs(); + if (oldNow !== this._now) { + // compensate for any setSystemTime() call during microtask callback + tickFrom += this._now - oldNow; + tickTo += this._now - oldNow; + } + + const doTickInner = (): number | undefined => { + // perform each timer in the requested range + timer = this._firstTimerInRange(tickFrom, tickTo); + while (timer && tickFrom <= tickTo) { + if (this._timers.has(timer.id)) { + tickFrom = timer.callAt; + this._now = timer.callAt; + oldNow = this._now; + try { + this._runJobs(); + this._callTimer(timer); + } catch (e) { + firstException = firstException || e; + } + + if (isAsync) { + // finish up after native setImmediate callback to allow + // all native es6 promises to process their callbacks after + // each timer fires. + this._embedder.postTask(nextPromiseTick!); + return; + } + compensationCheck(); + } + postTimerCall(); + } + + // perform process.nextTick()s again + oldNow = this._now; + this._runJobs(); + if (oldNow !== this._now) { + // compensate for any setSystemTime() call during process.nextTick() callback + tickFrom += this._now - oldNow; + tickTo += this._now - oldNow; + } + this._duringTick = false; + + // corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo] + timer = this._firstTimerInRange(tickFrom, tickTo); + if (timer) { + try { + this.tick(tickTo - this._now); // do it all again - for the remainder of the requested range + } catch (e) { + firstException = firstException || e; + } + } else { + // no timers remaining in the requested range: move the clock all the way to the end + this._now = tickTo; + } + if (firstException) + throw firstException; + + if (isAsync) + resolve!(this._now); + else + return this._now; + }; + + nextPromiseTick = + isAsync ? + () => { + try { + compensationCheck(); + postTimerCall(); + doTickInner(); + } catch (e) { + reject!(e); + } + } : null; + + compensationCheck = () => { + // compensate for any setSystemTime() call during timer callback + if (oldNow !== this._now) { + tickFrom += this._now - oldNow; + tickTo += this._now - oldNow; + previous += this._now - oldNow; + } + }; + + postTimerCall = () => { + timer = this._firstTimerInRange(previous, tickTo); + previous = tickFrom; + }; + + return doTickInner(); + } + + tick(tickValue: string | number): number { + return this._doTick(tickValue, false)!; + } + + tickAsync(tickValue: string | number): Promise { + return new Promise((resolve, reject) => { + this._embedder.postTask(() => { + try { + this._doTick(tickValue, true, resolve, reject); + } catch (e) { + reject(e); + } + }); + }); + } + + next() { + this._runJobs(); + const timer = this._firstTimer(); + if (!timer) + return this._now; + + this._duringTick = true; + try { + this._now = timer.callAt; + this._callTimer(timer); + this._runJobs(); + return this._now; + } finally { + this._duringTick = false; + } + } + + nextAsync() { + return new Promise((resolve, reject) => { + this._embedder.postTask(() => { + try { + const timer = this._firstTimer(); + if (!timer) { + resolve(this._now); + return; + } + + let err: Error; + this._duringTick = true; + this._now = timer.callAt; + try { + this._callTimer(timer); + } catch (e) { + err = e; + } + this._duringTick = false; + + this._embedder.postTask(() => { + if (err) + reject(err); + else + resolve(this._now); + }); + } catch (e) { + reject(e); + } + }); + }); + } + + runAll() { + this._runJobs(); + for (let i = 0; i < this._loopLimit; i++) { + const numTimers = this._timers.size; + if (numTimers === 0) { + this._resetIsNearInfiniteLimit(); + return this._now; + } + + this.next(); + this._checkIsNearInfiniteLimit(i); + } + + const excessJob = this._firstTimer(); + throw this._getInfiniteLoopError(excessJob!); + } + + runToFrame() { + return this.tick(this.getTimeToNextFrame()); + } + + runAllAsync() { + return new Promise((resolve, reject) => { + let i = 0; + /** + * + */ + const doRun = () => { + this._embedder.postTask(() => { + try { + this._runJobs(); + + let numTimers; + if (i < this._loopLimit) { + if (!this._timers) { + this._resetIsNearInfiniteLimit(); + resolve(this._now); + return; + } + + numTimers = this._timers.size; + if (numTimers === 0) { + this._resetIsNearInfiniteLimit(); + resolve(this._now); + return; + } + + this.next(); + i++; + doRun(); + this._checkIsNearInfiniteLimit(i); + return; + } + + const excessJob = this._firstTimer(); + reject(this._getInfiniteLoopError(excessJob!)); + } catch (e) { + reject(e); + } + }); + }; + doRun(); + }); + } + + runToLast() { + const timer = this._lastTimer(); + if (!timer) { + this._runJobs(); + return this._now; + } + return this.tick(timer.callAt - this._now); + } + + runToLastAsync() { + return new Promise((resolve, reject) => { + this._embedder.postTask(() => { + try { + const timer = this._lastTimer(); + if (!timer) { + this._runJobs(); + resolve(this._now); + return; + } + this.tickAsync(timer.callAt - this._now).then(resolve); + } catch (e) { + reject(e); + } + }); + }); + } + + reset() { + this._timers.clear(); + this._jobs = []; + this._now = this.start; + } + + setSystemTime(systemTime: Date | number) { + // determine time difference + const newNow = getEpoch(systemTime); + const difference = newNow - this._now; + + this._adjustedSystemTime = this._adjustedSystemTime + difference; + // update 'system clock' + this._now = newNow; + + // update timers and intervals to keep them stable + for (const timer of this._timers.values()) { + timer.createdAt += difference; + timer.callAt += difference; + } + } + + jump(tickValue: string | number): number { + const msFloat = + typeof tickValue === 'number' + ? tickValue + : parseTime(tickValue); + const ms = Math.floor(msFloat); + + for (const timer of this._timers.values()) { + if (this._now + ms > timer.callAt) + timer.callAt = this._now + ms; + } + return this.tick(ms); + } + + private _checkIsNearInfiniteLimit(i: number): void { + if (this._loopLimit && i === this._loopLimit - 1) + this._isNearInfiniteLimit = true; + + } + + private _resetIsNearInfiniteLimit() { + this._isNearInfiniteLimit = false; + } + + private _runJobs() { + // runs all microtick-deferred tasks - ecma262/#sec-runjobs + if (!this._jobs) + return; + for (let i = 0; i < this._jobs.length; i++) { + const job = this._jobs[i]; + callFunction(job.func, job.args); + + this._checkIsNearInfiniteLimit(i); + if (this._loopLimit && i > this._loopLimit) + throw this._getInfiniteLoopError(job); + + } + this._resetIsNearInfiniteLimit(); + this._jobs = []; + } + + addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { + if (options.func === undefined) + throw new Error('Callback must be provided to timer calls'); + + let delay = options.delay ? +options.delay : 0; + if (!Number.isFinite(delay)) + delay = 0; + delay = delay > maxTimeout ? 1 : delay; + delay = Math.max(0, delay); + + const timer: Timer = { + type: options.type, + func: options.func, + args: options.args || [], + delay, + callAt: this._now + (delay || (this._duringTick ? 1 : 0)), + createdAt: this._now, + id: this._uniqueTimerId++, + error: this._isNearInfiniteLimit ? new Error() : undefined, + }; + this._timers.set(timer.id, timer); + return timer.id; + } + + private _firstTimerInRange(from: number, to: number): Timer | null { + let firstTimer: Timer | null = null; + for (const timer of this._timers.values()) { + const isInRange = inRange(from, to, timer); + if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1)) + firstTimer = timer; + } + return firstTimer; + } + + countTimers() { + return this._timers.size + this._jobs.length; + } + + private _firstTimer(): Timer | null { + let firstTimer: Timer | null = null; + + for (const timer of this._timers.values()) { + if (!firstTimer || compareTimers(firstTimer, timer) === 1) + firstTimer = timer; + } + return firstTimer; + } + + private _lastTimer(): Timer | null { + let lastTimer: Timer | null = null; + + for (const timer of this._timers.values()) { + if (!lastTimer || compareTimers(lastTimer, timer) === -1) + lastTimer = timer; + } + return lastTimer; + } + + private _callTimer(timer: Timer) { + if (timer.type === TimerType.Interval) + this._timers.get(timer.id)!.callAt += timer.delay; + else + this._timers.delete(timer.id); + callFunction(timer.func, timer.args); + } + + private _getInfiniteLoopError(job: Timer) { + const infiniteLoopError = new Error( + `Aborting after running ${this._loopLimit} timers, assuming an infinite loop!`, + ); + + if (!job.error) + return infiniteLoopError; + + // pattern never matched in Node + const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/; + const clockMethodPattern = new RegExp( + String(Object.keys(this).join('|')), + ); + + let matchedLineIndex = -1; + job.error.stack!.split('\n').some((line, i) => { + // If we've matched a computed target line (e.g. setTimeout) then we + // don't need to look any further. Return true to stop iterating. + const matchedComputedTarget = line.match(computedTargetPattern); + /* istanbul ignore if */ + if (matchedComputedTarget) { + matchedLineIndex = i; + return true; + } + + // If we've matched a clock method line, then there may still be + // others further down the trace. Return false to keep iterating. + const matchedClockMethod = line.match(clockMethodPattern); + if (matchedClockMethod) { + matchedLineIndex = i; + return false; + } + + // If we haven't matched anything on this line, but we matched + // previously and set the matched line index, then we can stop. + // If we haven't matched previously, then we should keep iterating. + return matchedLineIndex >= 0; + }); + + const funcName = typeof job.func === 'function' ? job.func.name : 'anonymous'; + const stack = `${infiniteLoopError}\n${job.type || 'Microtask'} - ${funcName}\n${job.error.stack! + .split('\n') + .slice(matchedLineIndex + 1) + .join('\n')}`; + + try { + Object.defineProperty(infiniteLoopError, 'stack', { + value: stack, + }); + } catch (e) { + // noop + } + + return infiniteLoopError; + } + + getTimeToNextFrame() { + return 16 - ((this._now - this.start) % 16); + } + + clearTimer(timerId: number, type: TimerType) { + if (!timerId) { + // null appears to be allowed in most browsers, and appears to be + // relied upon by some libraries, like Bootstrap carousel + return; + } + + // in Node, the ID is stored as the primitive value for `Timeout` objects + // for `Immediate` objects, no ID exists, so it gets coerced to NaN + const id = Number(timerId); + + if (Number.isNaN(id) || id < idCounterStart) { + const handlerName = getClearHandler(type); + new Error(`Clock: ${handlerName} was invoked to clear a native timer instead of one created by the clock library.`); + } + + const timer = this._timers.get(id); + if (timer) { + if ( + timer.type === type || + (timer.type === 'Timeout' && type === 'Interval') || + (timer.type === 'Interval' && type === 'Timeout') + ) { + this._timers.delete(id); + } else { + const clear = getClearHandler(type); + const schedule = getScheduleHandler(timer.type); + throw new Error( + `Cannot clear timer: timer created with ${schedule}() but cleared with ${clear}()`, + ); + } + } + } + + advanceAutomatically(advanceTimeDelta: number = 20): () => void { + return this._embedder.postTaskPeriodically( + () => this.tick(advanceTimeDelta!), + advanceTimeDelta, + ); + } +} + +function getEpoch(epoch: Date | number | undefined): number { + if (!epoch) + return 0; + if (typeof epoch !== 'number') + return epoch.getTime(); + return epoch; +} + +function inRange(from: number, to: number, timer: Timer): boolean { + return timer && timer.callAt >= from && timer.callAt <= to; +} + +/** + * Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into + * number of milliseconds. This is used to support human-readable strings passed + * to clock.tick() + */ +function parseTime(str: string): number { + if (!str) + return 0; + + const strings = str.split(':'); + const l = strings.length; + let i = l; + let ms = 0; + let parsed; + + if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { + throw new Error( + `Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`, + ); + } + + while (i--) { + parsed = parseInt(strings[i], 10); + if (parsed >= 60) + throw new Error(`Invalid time ${str}`); + ms += parsed * Math.pow(60, l - i - 1); + } + + return ms * 1000; +} + +function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date { + let prop; + for (prop of Object.keys(source) as (keyof DateConstructor)[]) + target[prop] = source[prop]; + target.toString = () => source.toString(); + target.prototype = source.prototype; + target.parse = source.parse; + target.UTC = source.UTC; + target.prototype.toUTCString = source.prototype.toUTCString; + target.isFake = true; + return target; +} + +function createDate(clock: Clock, NativeDate: typeof Date): DateConstructor & Date { + function ClockDate(this: typeof ClockDate, year: number, month: number, date: number, hour: number, minute: number, second: number, ms: number): Date | string { + // the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2. + // This remains so in the 10th edition of 2019 as well. + if (!(this instanceof ClockDate)) + return new NativeDate(clock.now()).toString(); + + // if Date is called as a constructor with 'new' keyword + // Defensive and verbose to avoid potential harm in passing + // explicit undefined when user does not pass argument + switch (arguments.length) { + case 0: + return new NativeDate(clock.now()); + case 1: + return new NativeDate(year); + case 2: + return new NativeDate(year, month); + case 3: + return new NativeDate(year, month, date); + case 4: + return new NativeDate(year, month, date, hour); + case 5: + return new NativeDate(year, month, date, hour, minute); + case 6: + return new NativeDate( + year, + month, + date, + hour, + minute, + second, + ); + default: + return new NativeDate( + year, + month, + date, + hour, + minute, + second, + ms, + ); + } + } + + ClockDate.now = () => clock.now(); + return mirrorDateProperties(ClockDate, NativeDate); +} + +/** + * Mirror Intl by default on our fake implementation + * + * Most of the properties are the original native ones, + * but we need to take control of those that have a + * dependency on the current clock. + */ +function createIntl(clock: Clock, NativeIntl: typeof Intl): typeof Intl { + const ClockIntl: any = {}; + /* + * All properties of Intl are non-enumerable, so we need + * to do a bit of work to get them out. + */ + for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[]) + ClockIntl[key] = NativeIntl[key]; + + ClockIntl.DateTimeFormat = (...args: any[]) => { + const realFormatter = new NativeIntl.DateTimeFormat(...args); + const formatter: Intl.DateTimeFormat = { + formatRange: realFormatter.formatRange.bind(realFormatter), + formatRangeToParts: realFormatter.formatRangeToParts.bind(realFormatter), + resolvedOptions: realFormatter.resolvedOptions.bind(realFormatter), + format: date => realFormatter.format(date || clock.now()), + formatToParts: date => realFormatter.formatToParts(date || clock.now()), + }; + + return formatter; + }; + + ClockIntl.DateTimeFormat.prototype = Object.create( + NativeIntl.DateTimeFormat.prototype, + ); + + ClockIntl.DateTimeFormat.supportedLocalesOf = + NativeIntl.DateTimeFormat.supportedLocalesOf; + + return ClockIntl; +} + +function compareTimers(a: Timer, b: Timer) { + // Sort first by absolute timing + if (a.callAt < b.callAt) + return -1; + if (a.callAt > b.callAt) + return 1; + + // Sort next by immediate, immediate timers take precedence + if (a.type === TimerType.Immediate && b.type !== TimerType.Immediate) + return -1; + if (a.type !== TimerType.Immediate && b.type === TimerType.Immediate) + return 1; + + // Sort next by creation time, earlier-created timers take precedence + if (a.createdAt < b.createdAt) + return -1; + if (a.createdAt > b.createdAt) + return 1; + + // Sort next by id, lower-id timers take precedence + if (a.id < b.id) + return -1; + if (a.id > b.id) + return 1; + + // As timer ids are unique, no fallback `0` is necessary +} + +function callFunction(func: TimerHandler, args: any[]) { + if (typeof func === 'function') + func.apply(null, args); + else + (() => { eval(func); })(); +} + +const maxTimeout = Math.pow(2, 31) - 1; // see https://heycam.github.io/webidl/#abstract-opdef-converttoint +const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs + +function platformOriginals(globalObject: WindowOrWorkerGlobalScope): ClockMethods { + return { + setTimeout: globalObject.setTimeout.bind(globalObject), + clearTimeout: globalObject.clearTimeout.bind(globalObject), + setInterval: globalObject.setInterval.bind(globalObject), + clearInterval: globalObject.clearInterval.bind(globalObject), + requestAnimationFrame: (globalObject as any).requestAnimationFrame ? (globalObject as any).requestAnimationFrame.bind(globalObject) : undefined, + cancelAnimationFrame: (globalObject as any).cancelAnimationFrame ? (globalObject as any).cancelAnimationFrame.bind(globalObject) : undefined, + requestIdleCallback: (globalObject as any).requestIdleCallback ? (globalObject as any).requestIdleCallback.bind(globalObject) : undefined, + cancelIdleCallback: (globalObject as any).cancelIdleCallback ? (globalObject as any).cancelIdleCallback.bind(globalObject) : undefined, + Date: (globalObject as any).Date, + performance: globalObject.performance, + Intl: (globalObject as any).Intl, + }; +} + +/** + * Gets schedule handler name for a given timer type + */ +function getScheduleHandler(type: TimerType) { + if (type === 'IdleCallback' || type === 'AnimationFrame') + return `request${type}`; + + return `set${type}`; +} + +function createApi(clock: Clock, originals: ClockMethods): ClockMethods { + return { + setTimeout: (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => { + const delay = timeout ? +timeout : timeout; + return clock.addTimer({ + type: TimerType.Timeout, + func, + args, + delay + }); + }, + clearTimeout: (timerId: number | undefined): void => { + if (timerId) + clock.clearTimer(timerId, TimerType.Timeout); + }, + setInterval: (func: TimerHandler, timeout?: number | undefined, ...args: any[]): number => { + const delay = timeout ? +timeout : timeout; + return clock.addTimer({ + type: TimerType.Interval, + func, + args, + delay, + }); + }, + clearInterval: (timerId: number | undefined): void => { + if (timerId) + return clock.clearTimer(timerId, TimerType.Interval); + }, + requestAnimationFrame: (callback: FrameRequestCallback): number => { + return clock.addTimer({ + type: TimerType.AnimationFrame, + func: callback, + delay: clock.getTimeToNextFrame(), + get args() { + return [clock.performanceNow()]; + }, + }); + }, + cancelAnimationFrame: (timerId: number): void => { + if (timerId) + return clock.clearTimer(timerId, TimerType.AnimationFrame); + }, + requestIdleCallback: (callback: IdleRequestCallback, options?: IdleRequestOptions | undefined): number => { + let timeToNextIdlePeriod = 0; + + if (clock.countTimers() > 0) + timeToNextIdlePeriod = 50; // const for now + return clock.addTimer({ + type: TimerType.IdleCallback, + func: callback, + args: [], + delay: options?.timeout ? Math.min(options?.timeout, timeToNextIdlePeriod) : timeToNextIdlePeriod, + }); + }, + cancelIdleCallback: (timerId: number): void => { + if (timerId) + return clock.clearTimer(timerId, TimerType.IdleCallback); + }, + Intl: originals.Intl ? createIntl(clock, originals.Intl) : undefined, + Date: createDate(clock, originals.Date), + performance: originals.performance ? fakePerformance(clock, originals.performance) : undefined, + }; +} + +function getClearHandler(type: TimerType) { + if (type === 'IdleCallback' || type === 'AnimationFrame') + return `cancel${type}`; + + return `clear${type}`; +} + +function fakePerformance(clock: Clock, performance: Performance): Performance { + const result: any = { + now: () => clock.performanceNow(), + timeOrigin: clock.start, + __proto__: performance, }; - const result = SinonFakeTimers; - result.builtin = builtin; return result; } + +export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: Clock, api: Partial, originals: Partial } { + const originals = platformOriginals(globalObject); + const embedder = { + postTask: (task: () => void) => { + originals.setTimeout!(task, 0); + }, + postTaskPeriodically: (task: () => void, delay: number) => { + const intervalId = globalObject.setInterval(task, delay); + return () => originals.clearInterval!(intervalId); + }, + }; + + const clock = new Clock(embedder, config.now, config.loopLimit); + const api = createApi(clock, originals); + return { clock, api, originals }; +} + +export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: Clock, api: Partial, originals: Partial } { + if ((globalObject as any).Date?.isFake) { + // Timers are already faked; this is a problem. + // Make the user reset timers before continuing. + throw new TypeError(`Can't install fake timers twice on the same global object.`); + } + + const { clock, api, originals } = createClock(globalObject, config); + const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[]; + + for (const method of toFake) { + if (method === 'Date') { + (globalObject as any).Date = mirrorDateProperties(api.Date, (globalObject as any).Date); + } else if (method === 'Intl') { + (globalObject as any).Intl = api[method]!; + } else if (method === 'performance') { + (globalObject as any).performance = api[method]!; + } else { + (globalObject as any)[method] = (...args: any[]) => { + return (api[method] as any).apply(api, args); + }; + } + clock.disposables.push(() => { + (globalObject as any)[method] = originals[method]; + }); + } + + return { clock, api, originals }; +} + +export function inject(globalObject: WindowOrWorkerGlobalScope) { + return { + install: (config: InstallConfig) => { + const { clock } = install(globalObject, config); + return clock; + }, + builtin: platformOriginals(globalObject), + }; +} diff --git a/packages/playwright-core/src/third_party/fake-timers-src.js b/packages/playwright-core/src/third_party/fake-timers-src.js deleted file mode 100644 index 9602123052..0000000000 --- a/packages/playwright-core/src/third_party/fake-timers-src.js +++ /dev/null @@ -1,1776 +0,0 @@ -/* - * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. All rights reserved. - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -/ - -"use strict"; - -/* - * Local modifications: - * - removed global.process-related code. - * - removed require("@sinonjs/commons") dependency. - */ - -/** - * @typedef {object} IdleDeadline - * @property {boolean} didTimeout - whether or not the callback was called before reaching the optional timeout - * @property {function():number} timeRemaining - a floating-point value providing an estimate of the number of milliseconds remaining in the current idle period - */ - -/** - * Queues a function to be called during a browser's idle periods - * - * @callback RequestIdleCallback - * @param {function(IdleDeadline)} callback - * @param {{timeout: number}} options - an options object - * @returns {number} the id - */ - -/** - * @callback NextTick - * @param {VoidVarArgsFunc} callback - the callback to run - * @param {...*} args - optional arguments to call the callback with - * @returns {void} - */ - -/** - * @callback SetImmediate - * @param {VoidVarArgsFunc} callback - the callback to run - * @param {...*} args - optional arguments to call the callback with - * @returns {NodeImmediate} - */ - -/** - * @callback VoidVarArgsFunc - * @param {...*} callback - the callback to run - * @returns {void} - */ - -/** - * @typedef RequestAnimationFrame - * @property {function(number):void} requestAnimationFrame - * @returns {number} - the id - */ - -/** - * @typedef Performance - * @property {function(): number} now - */ - -/* eslint-disable jsdoc/require-property-description */ -/** - * @typedef {object} Clock - * @property {number} now - the current time - * @property {Date} Date - the Date constructor - * @property {number} loopLimit - the maximum number of timers before assuming an infinite loop - * @property {RequestIdleCallback} requestIdleCallback - * @property {function(number):void} cancelIdleCallback - * @property {setTimeout} setTimeout - * @property {clearTimeout} clearTimeout - * @property {NextTick} nextTick - * @property {queueMicrotask} queueMicrotask - * @property {setInterval} setInterval - * @property {clearInterval} clearInterval - * @property {SetImmediate} setImmediate - * @property {function(NodeImmediate):void} clearImmediate - * @property {function():number} countTimers - * @property {RequestAnimationFrame} requestAnimationFrame - * @property {function(number):void} cancelAnimationFrame - * @property {function():void} runMicrotasks - * @property {function(string | number): number} tick - * @property {function(string | number): Promise} tickAsync - * @property {function(): number} next - * @property {function(): Promise} nextAsync - * @property {function(): number} runAll - * @property {function(): number} runToFrame - * @property {function(): Promise} runAllAsync - * @property {function(): number} runToLast - * @property {function(): Promise} runToLastAsync - * @property {function(): void} reset - * @property {function(number | Date): void} setSystemTime - * @property {function(number): void} jump - * @property {Performance} performance - * @property {function(number[]): number[]} hrtime - process.hrtime (legacy) - * @property {function(): void} uninstall Uninstall the clock. - * @property {Function[]} methods - the methods that are faked - * @property {boolean} [shouldClearNativeTimers] inherited from config - */ -/* eslint-enable jsdoc/require-property-description */ - -/** - * Configuration object for the `install` method. - * - * @typedef {object} Config - * @property {number|Date} [now] a number (in milliseconds) or a Date object (default epoch) - * @property {string[]} [toFake] names of the methods that should be faked. - * @property {number} [loopLimit] the maximum number of timers that will be run when calling runAll() - * @property {boolean} [shouldAdvanceTime] tells FakeTimers to increment mocked time automatically (default false) - * @property {number} [advanceTimeDelta] increment mocked time every <> ms (default: 20ms) - * @property {boolean} [shouldClearNativeTimers] forwards clear timer calls to native functions if they are not fakes (default: false) - */ - -/* eslint-disable jsdoc/require-property-description */ -/** - * The internal structure to describe a scheduled fake timer - * - * @typedef {object} Timer - * @property {Function} func - * @property {*[]} args - * @property {number} delay - * @property {number} callAt - * @property {number} createdAt - * @property {boolean} immediate - * @property {number} id - * @property {Error} [error] - */ - -/** - * A Node timer - * - * @typedef {object} NodeImmediate - * @property {function(): boolean} hasRef - * @property {function(): NodeImmediate} ref - * @property {function(): NodeImmediate} unref - */ -/* eslint-enable jsdoc/require-property-description */ - -/* eslint-disable complexity */ - -/** - * Mocks available features in the specified global namespace. - * - * @param {*} _global Namespace to mock (e.g. `window`) - * @returns {FakeTimers} - */ -function withGlobal(_global) { - const maxTimeout = Math.pow(2, 31) - 1; //see https://heycam.github.io/webidl/#abstract-opdef-converttoint - const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs - const NOOP = function () { - return undefined; - }; - const NOOP_ARRAY = function () { - return []; - }; - const timeoutResult = _global.setTimeout(NOOP, 0); - const addTimerReturnsObject = typeof timeoutResult === "object"; - const performancePresent = - _global.performance && typeof _global.performance.now === "function"; - const hasPerformancePrototype = - _global.Performance && - (typeof _global.Performance).match(/^(function|object)$/); - const hasPerformanceConstructorPrototype = - _global.performance && - _global.performance.constructor && - _global.performance.constructor.prototype; - const queueMicrotaskPresent = _global.hasOwnProperty("queueMicrotask"); - const requestAnimationFramePresent = - _global.requestAnimationFrame && - typeof _global.requestAnimationFrame === "function"; - const cancelAnimationFramePresent = - _global.cancelAnimationFrame && - typeof _global.cancelAnimationFrame === "function"; - const requestIdleCallbackPresent = - _global.requestIdleCallback && - typeof _global.requestIdleCallback === "function"; - const cancelIdleCallbackPresent = - _global.cancelIdleCallback && - typeof _global.cancelIdleCallback === "function"; - const setImmediatePresent = - _global.setImmediate && typeof _global.setImmediate === "function"; - const intlPresent = _global.Intl && typeof _global.Intl === "object"; - - _global.clearTimeout(timeoutResult); - - const NativeDate = _global.Date; - const NativeIntl = _global.Intl; - let uniqueTimerId = idCounterStart; - - /** - * @param {number} num - * @returns {boolean} - */ - function isNumberFinite(num) { - if (Number.isFinite) { - return Number.isFinite(num); - } - - return isFinite(num); - } - - let isNearInfiniteLimit = false; - - /** - * @param {Clock} clock - * @param {number} i - */ - function checkIsNearInfiniteLimit(clock, i) { - if (clock.loopLimit && i === clock.loopLimit - 1) { - isNearInfiniteLimit = true; - } - } - - /** - * - */ - function resetIsNearInfiniteLimit() { - isNearInfiniteLimit = false; - } - - /** - * Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into - * number of milliseconds. This is used to support human-readable strings passed - * to clock.tick() - * - * @param {string} str - * @returns {number} - */ - function parseTime(str) { - if (!str) { - return 0; - } - - const strings = str.split(":"); - const l = strings.length; - let i = l; - let ms = 0; - let parsed; - - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { - throw new Error( - "tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits", - ); - } - - while (i--) { - parsed = parseInt(strings[i], 10); - - if (parsed >= 60) { - throw new Error(`Invalid time ${str}`); - } - - ms += parsed * Math.pow(60, l - i - 1); - } - - return ms * 1000; - } - - /** - * Get the decimal part of the millisecond value as nanoseconds - * - * @param {number} msFloat the number of milliseconds - * @returns {number} an integer number of nanoseconds in the range [0,1e6) - * - * Example: nanoRemainer(123.456789) -> 456789 - */ - function nanoRemainder(msFloat) { - const modulo = 1e6; - const remainder = (msFloat * 1e6) % modulo; - const positiveRemainder = - remainder < 0 ? remainder + modulo : remainder; - - return Math.floor(positiveRemainder); - } - - /** - * Used to grok the `now` parameter to createClock. - * - * @param {Date|number} epoch the system time - * @returns {number} - */ - function getEpoch(epoch) { - if (!epoch) { - return 0; - } - if (typeof epoch.getTime === "function") { - return epoch.getTime(); - } - if (typeof epoch === "number") { - return epoch; - } - throw new TypeError("now should be milliseconds since UNIX epoch"); - } - - /** - * @param {number} from - * @param {number} to - * @param {Timer} timer - * @returns {boolean} - */ - function inRange(from, to, timer) { - return timer && timer.callAt >= from && timer.callAt <= to; - } - - /** - * @param {Clock} clock - * @param {Timer} job - */ - function getInfiniteLoopError(clock, job) { - const infiniteLoopError = new Error( - `Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!`, - ); - - if (!job.error) { - return infiniteLoopError; - } - - // pattern never matched in Node - const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/; - let clockMethodPattern = new RegExp( - String(Object.keys(clock).join("|")), - ); - - if (addTimerReturnsObject) { - // node.js environment - clockMethodPattern = new RegExp( - `\\s+at (Object\\.)?(?:${Object.keys(clock).join("|")})\\s+`, - ); - } - - let matchedLineIndex = -1; - job.error.stack.split("\n").some(function (line, i) { - // If we've matched a computed target line (e.g. setTimeout) then we - // don't need to look any further. Return true to stop iterating. - const matchedComputedTarget = line.match(computedTargetPattern); - /* istanbul ignore if */ - if (matchedComputedTarget) { - matchedLineIndex = i; - return true; - } - - // If we've matched a clock method line, then there may still be - // others further down the trace. Return false to keep iterating. - const matchedClockMethod = line.match(clockMethodPattern); - if (matchedClockMethod) { - matchedLineIndex = i; - return false; - } - - // If we haven't matched anything on this line, but we matched - // previously and set the matched line index, then we can stop. - // If we haven't matched previously, then we should keep iterating. - return matchedLineIndex >= 0; - }); - - const stack = `${infiniteLoopError}\n${job.type || "Microtask"} - ${ - job.func.name || "anonymous" - }\n${job.error.stack - .split("\n") - .slice(matchedLineIndex + 1) - .join("\n")}`; - - try { - Object.defineProperty(infiniteLoopError, "stack", { - value: stack, - }); - } catch (e) { - // noop - } - - return infiniteLoopError; - } - - /** - * @param {Date} target - * @param {Date} source - * @returns {Date} the target after modifications - */ - function mirrorDateProperties(target, source) { - let prop; - for (prop in source) { - if (source.hasOwnProperty(prop)) { - target[prop] = source[prop]; - } - } - - // set special now implementation - if (source.now) { - target.now = function now() { - return target.clock.now; - }; - } else { - delete target.now; - } - - // set special toSource implementation - if (source.toSource) { - target.toSource = function toSource() { - return source.toSource(); - }; - } else { - delete target.toSource; - } - - // set special toString implementation - target.toString = function toString() { - return source.toString(); - }; - - target.prototype = source.prototype; - target.parse = source.parse; - target.UTC = source.UTC; - target.prototype.toUTCString = source.prototype.toUTCString; - target.isFake = true; - - return target; - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function createDate() { - /** - * @param {number} year - * @param {number} month - * @param {number} date - * @param {number} hour - * @param {number} minute - * @param {number} second - * @param {number} ms - * @returns {Date} - */ - function ClockDate(year, month, date, hour, minute, second, ms) { - // the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2. - // This remains so in the 10th edition of 2019 as well. - if (!(this instanceof ClockDate)) { - return new NativeDate(ClockDate.clock.now).toString(); - } - - // if Date is called as a constructor with 'new' keyword - // Defensive and verbose to avoid potential harm in passing - // explicit undefined when user does not pass argument - switch (arguments.length) { - case 0: - return new NativeDate(ClockDate.clock.now); - case 1: - return new NativeDate(year); - case 2: - return new NativeDate(year, month); - case 3: - return new NativeDate(year, month, date); - case 4: - return new NativeDate(year, month, date, hour); - case 5: - return new NativeDate(year, month, date, hour, minute); - case 6: - return new NativeDate( - year, - month, - date, - hour, - minute, - second, - ); - default: - return new NativeDate( - year, - month, - date, - hour, - minute, - second, - ms, - ); - } - } - - return mirrorDateProperties(ClockDate, NativeDate); - } - - /** - * Mirror Intl by default on our fake implementation - * - * Most of the properties are the original native ones, - * but we need to take control of those that have a - * dependency on the current clock. - * - * @returns {object} the partly fake Intl implementation - */ - function createIntl() { - const ClockIntl = {}; - /* - * All properties of Intl are non-enumerable, so we need - * to do a bit of work to get them out. - */ - Object.getOwnPropertyNames(NativeIntl).forEach( - (property) => (ClockIntl[property] = NativeIntl[property]), - ); - - ClockIntl.DateTimeFormat = function (...args) { - const realFormatter = new NativeIntl.DateTimeFormat(...args); - const formatter = {}; - - ["formatRange", "formatRangeToParts", "resolvedOptions"].forEach( - (method) => { - formatter[method] = - realFormatter[method].bind(realFormatter); - }, - ); - - ["format", "formatToParts"].forEach((method) => { - formatter[method] = function (date) { - return realFormatter[method](date || ClockIntl.clock.now); - }; - }); - - return formatter; - }; - - ClockIntl.DateTimeFormat.prototype = Object.create( - NativeIntl.DateTimeFormat.prototype, - ); - - ClockIntl.DateTimeFormat.supportedLocalesOf = - NativeIntl.DateTimeFormat.supportedLocalesOf; - - return ClockIntl; - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function enqueueJob(clock, job) { - // enqueues a microtick-deferred task - ecma262/#sec-enqueuejob - if (!clock.jobs) { - clock.jobs = []; - } - clock.jobs.push(job); - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function runJobs(clock) { - // runs all microtick-deferred tasks - ecma262/#sec-runjobs - if (!clock.jobs) { - return; - } - for (let i = 0; i < clock.jobs.length; i++) { - const job = clock.jobs[i]; - job.func.apply(null, job.args); - - checkIsNearInfiniteLimit(clock, i); - if (clock.loopLimit && i > clock.loopLimit) { - throw getInfiniteLoopError(clock, job); - } - } - resetIsNearInfiniteLimit(); - clock.jobs = []; - } - - /** - * @param {Clock} clock - * @param {Timer} timer - * @returns {number} id of the created timer - */ - function addTimer(clock, timer) { - if (timer.func === undefined) { - throw new Error("Callback must be provided to timer calls"); - } - - if (addTimerReturnsObject) { - // Node.js environment - if (typeof timer.func !== "function") { - throw new TypeError( - `[ERR_INVALID_CALLBACK]: Callback must be a function. Received ${ - timer.func - } of type ${typeof timer.func}`, - ); - } - } - - if (isNearInfiniteLimit) { - timer.error = new Error(); - } - - timer.type = timer.immediate ? "Immediate" : "Timeout"; - - if (timer.hasOwnProperty("delay")) { - if (typeof timer.delay !== "number") { - timer.delay = parseInt(timer.delay, 10); - } - - if (!isNumberFinite(timer.delay)) { - timer.delay = 0; - } - timer.delay = timer.delay > maxTimeout ? 1 : timer.delay; - timer.delay = Math.max(0, timer.delay); - } - - if (timer.hasOwnProperty("interval")) { - timer.type = "Interval"; - timer.interval = timer.interval > maxTimeout ? 1 : timer.interval; - } - - if (timer.hasOwnProperty("animation")) { - timer.type = "AnimationFrame"; - timer.animation = true; - } - - if (timer.hasOwnProperty("idleCallback")) { - timer.type = "IdleCallback"; - timer.idleCallback = true; - } - - if (!clock.timers) { - clock.timers = {}; - } - - timer.id = uniqueTimerId++; - timer.createdAt = clock.now; - timer.callAt = - clock.now + (parseInt(timer.delay) || (clock.duringTick ? 1 : 0)); - - clock.timers[timer.id] = timer; - - if (addTimerReturnsObject) { - const res = { - refed: true, - ref: function () { - this.refed = true; - return res; - }, - unref: function () { - this.refed = false; - return res; - }, - hasRef: function () { - return this.refed; - }, - refresh: function () { - timer.callAt = - clock.now + - (parseInt(timer.delay) || (clock.duringTick ? 1 : 0)); - - // it _might_ have been removed, but if not the assignment is perfectly fine - clock.timers[timer.id] = timer; - - return res; - }, - [Symbol.toPrimitive]: function () { - return timer.id; - }, - }; - return res; - } - - return timer.id; - } - - /* eslint consistent-return: "off" */ - /** - * Timer comparitor - * - * @param {Timer} a - * @param {Timer} b - * @returns {number} - */ - function compareTimers(a, b) { - // Sort first by absolute timing - if (a.callAt < b.callAt) { - return -1; - } - if (a.callAt > b.callAt) { - return 1; - } - - // Sort next by immediate, immediate timers take precedence - if (a.immediate && !b.immediate) { - return -1; - } - if (!a.immediate && b.immediate) { - return 1; - } - - // Sort next by creation time, earlier-created timers take precedence - if (a.createdAt < b.createdAt) { - return -1; - } - if (a.createdAt > b.createdAt) { - return 1; - } - - // Sort next by id, lower-id timers take precedence - if (a.id < b.id) { - return -1; - } - if (a.id > b.id) { - return 1; - } - - // As timer ids are unique, no fallback `0` is necessary - } - - /** - * @param {Clock} clock - * @param {number} from - * @param {number} to - * @returns {Timer} - */ - function firstTimerInRange(clock, from, to) { - const timers = clock.timers; - let timer = null; - let id, isInRange; - - for (id in timers) { - if (timers.hasOwnProperty(id)) { - isInRange = inRange(from, to, timers[id]); - - if ( - isInRange && - (!timer || compareTimers(timer, timers[id]) === 1) - ) { - timer = timers[id]; - } - } - } - - return timer; - } - - /** - * @param {Clock} clock - * @returns {Timer} - */ - function firstTimer(clock) { - const timers = clock.timers; - let timer = null; - let id; - - for (id in timers) { - if (timers.hasOwnProperty(id)) { - if (!timer || compareTimers(timer, timers[id]) === 1) { - timer = timers[id]; - } - } - } - - return timer; - } - - /** - * @param {Clock} clock - * @returns {Timer} - */ - function lastTimer(clock) { - const timers = clock.timers; - let timer = null; - let id; - - for (id in timers) { - if (timers.hasOwnProperty(id)) { - if (!timer || compareTimers(timer, timers[id]) === -1) { - timer = timers[id]; - } - } - } - - return timer; - } - - /** - * @param {Clock} clock - * @param {Timer} timer - */ - function callTimer(clock, timer) { - if (typeof timer.interval === "number") { - clock.timers[timer.id].callAt += timer.interval; - } else { - delete clock.timers[timer.id]; - } - - if (typeof timer.func === "function") { - timer.func.apply(null, timer.args); - } else { - /* eslint no-eval: "off" */ - const eval2 = eval; - (function () { - eval2(timer.func); - })(); - } - } - - /** - * Gets clear handler name for a given timer type - * - * @param {string} ttype - */ - function getClearHandler(ttype) { - if (ttype === "IdleCallback" || ttype === "AnimationFrame") { - return `cancel${ttype}`; - } - return `clear${ttype}`; - } - - /** - * Gets schedule handler name for a given timer type - * - * @param {string} ttype - */ - function getScheduleHandler(ttype) { - if (ttype === "IdleCallback" || ttype === "AnimationFrame") { - return `request${ttype}`; - } - return `set${ttype}`; - } - - /** - * Creates an anonymous function to warn only once - */ - function createWarnOnce() { - let calls = 0; - return function (msg) { - // eslint-disable-next-line - !calls++ && console.warn(msg); - }; - } - const warnOnce = createWarnOnce(); - - /** - * @param {Clock} clock - * @param {number} timerId - * @param {string} ttype - */ - function clearTimer(clock, timerId, ttype) { - if (!timerId) { - // null appears to be allowed in most browsers, and appears to be - // relied upon by some libraries, like Bootstrap carousel - return; - } - - if (!clock.timers) { - clock.timers = {}; - } - - // in Node, the ID is stored as the primitive value for `Timeout` objects - // for `Immediate` objects, no ID exists, so it gets coerced to NaN - const id = Number(timerId); - - if (Number.isNaN(id) || id < idCounterStart) { - const handlerName = getClearHandler(ttype); - - if (clock.shouldClearNativeTimers === true) { - const nativeHandler = clock[`_${handlerName}`]; - return typeof nativeHandler === "function" - ? nativeHandler(timerId) - : undefined; - } - warnOnce( - `FakeTimers: ${handlerName} was invoked to clear a native timer instead of one created by this library.` + - "\nTo automatically clean-up native timers, use `shouldClearNativeTimers`.", - ); - } - - if (clock.timers.hasOwnProperty(id)) { - // check that the ID matches a timer of the correct type - const timer = clock.timers[id]; - if ( - timer.type === ttype || - (timer.type === "Timeout" && ttype === "Interval") || - (timer.type === "Interval" && ttype === "Timeout") - ) { - delete clock.timers[id]; - } else { - const clear = getClearHandler(ttype); - const schedule = getScheduleHandler(timer.type); - throw new Error( - `Cannot clear timer: timer created with ${schedule}() but cleared with ${clear}()`, - ); - } - } - } - - /** - * @param {Clock} clock - * @param {Config} config - * @returns {Timer[]} - */ - function uninstall(clock, config) { - let method, i, l; - const installedHrTime = "_hrtime"; - const installedNextTick = "_nextTick"; - - for (i = 0, l = clock.methods.length; i < l; i++) { - method = clock.methods[i]; - if (method === "performance") { - const originalPerfDescriptor = Object.getOwnPropertyDescriptor( - clock, - `_${method}`, - ); - if ( - originalPerfDescriptor && - originalPerfDescriptor.get && - !originalPerfDescriptor.set - ) { - Object.defineProperty( - _global, - method, - originalPerfDescriptor, - ); - } else if (originalPerfDescriptor.configurable) { - _global[method] = clock[`_${method}`]; - } - } else { - if (_global[method] && _global[method].hadOwnProperty) { - _global[method] = clock[`_${method}`]; - } else { - try { - delete _global[method]; - } catch (ignore) { - /* eslint no-empty: "off" */ - } - } - } - } - - if (config.shouldAdvanceTime === true) { - _global.clearInterval(clock.attachedInterval); - } - - // Prevent multiple executions which will completely remove these props - clock.methods = []; - - // return pending timers, to enable checking what timers remained on uninstall - if (!clock.timers) { - return []; - } - return Object.keys(clock.timers).map(function mapper(key) { - return clock.timers[key]; - }); - } - - /** - * @param {object} target the target containing the method to replace - * @param {string} method the keyname of the method on the target - * @param {Clock} clock - */ - function hijackMethod(target, method, clock) { - clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call( - target, - method, - ); - clock[`_${method}`] = target[method]; - - if (method === "Date") { - const date = mirrorDateProperties(clock[method], target[method]); - target[method] = date; - } else if (method === "Intl") { - target[method] = clock[method]; - } else if (method === "performance") { - const originalPerfDescriptor = Object.getOwnPropertyDescriptor( - target, - method, - ); - // JSDOM has a read only performance field so we have to save/copy it differently - if ( - originalPerfDescriptor && - originalPerfDescriptor.get && - !originalPerfDescriptor.set - ) { - Object.defineProperty( - clock, - `_${method}`, - originalPerfDescriptor, - ); - - const perfDescriptor = Object.getOwnPropertyDescriptor( - clock, - method, - ); - Object.defineProperty(target, method, perfDescriptor); - } else { - target[method] = clock[method]; - } - } else { - target[method] = function () { - return clock[method].apply(clock, arguments); - }; - - Object.defineProperties( - target[method], - Object.getOwnPropertyDescriptors(clock[method]), - ); - } - - target[method].clock = clock; - } - - /** - * @param {Clock} clock - * @param {number} advanceTimeDelta - */ - function doIntervalTick(clock, advanceTimeDelta) { - clock.tick(advanceTimeDelta); - } - - /** - * @typedef {object} Timers - * @property {setTimeout} setTimeout - * @property {clearTimeout} clearTimeout - * @property {setInterval} setInterval - * @property {clearInterval} clearInterval - * @property {Date} Date - * @property {Intl} Intl - * @property {SetImmediate=} setImmediate - * @property {function(NodeImmediate): void=} clearImmediate - * @property {function(number[]):number[]=} hrtime - * @property {NextTick=} nextTick - * @property {Performance=} performance - * @property {RequestAnimationFrame=} requestAnimationFrame - * @property {boolean=} queueMicrotask - * @property {function(number): void=} cancelAnimationFrame - * @property {RequestIdleCallback=} requestIdleCallback - * @property {function(number): void=} cancelIdleCallback - */ - - /** @type {Timers} */ - const timers = { - setTimeout: _global.setTimeout, - clearTimeout: _global.clearTimeout, - setInterval: _global.setInterval, - clearInterval: _global.clearInterval, - Date: _global.Date, - }; - - if (setImmediatePresent) { - timers.setImmediate = _global.setImmediate; - timers.clearImmediate = _global.clearImmediate; - } - - if (performancePresent) { - timers.performance = _global.performance; - } - - if (requestAnimationFramePresent) { - timers.requestAnimationFrame = _global.requestAnimationFrame; - } - - if (queueMicrotaskPresent) { - timers.queueMicrotask = true; - } - - if (cancelAnimationFramePresent) { - timers.cancelAnimationFrame = _global.cancelAnimationFrame; - } - - if (requestIdleCallbackPresent) { - timers.requestIdleCallback = _global.requestIdleCallback; - } - - if (cancelIdleCallbackPresent) { - timers.cancelIdleCallback = _global.cancelIdleCallback; - } - - if (intlPresent) { - timers.Intl = _global.Intl; - } - - const originalSetTimeout = _global.setImmediate || _global.setTimeout; - - /** - * @param {Date|number} [start] the system time - non-integer values are floored - * @param {number} [loopLimit] maximum number of timers that will be run when calling runAll() - * @returns {Clock} - */ - function createClock(start, loopLimit) { - // eslint-disable-next-line no-param-reassign - start = Math.floor(getEpoch(start)); - // eslint-disable-next-line no-param-reassign - loopLimit = loopLimit || 1000; - let nanos = 0; - const adjustedSystemTime = [0, 0]; // [millis, nanoremainder] - - if (NativeDate === undefined) { - throw new Error( - "The global scope doesn't have a `Date` object" + - " (see https://github.com/sinonjs/sinon/issues/1852#issuecomment-419622780)", - ); - } - - const clock = { - now: start, - Date: createDate(), - loopLimit: loopLimit, - }; - - clock.Date.clock = clock; - - //eslint-disable-next-line jsdoc/require-jsdoc - function getTimeToNextFrame() { - return 16 - ((clock.now - start) % 16); - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function hrtime(prev) { - const millisSinceStart = clock.now - adjustedSystemTime[0] - start; - const secsSinceStart = Math.floor(millisSinceStart / 1000); - const remainderInNanos = - (millisSinceStart - secsSinceStart * 1e3) * 1e6 + - nanos - - adjustedSystemTime[1]; - - if (Array.isArray(prev)) { - if (prev[1] > 1e9) { - throw new TypeError( - "Number of nanoseconds can't exceed a billion", - ); - } - - const oldSecs = prev[0]; - let nanoDiff = remainderInNanos - prev[1]; - let secDiff = secsSinceStart - oldSecs; - - if (nanoDiff < 0) { - nanoDiff += 1e9; - secDiff -= 1; - } - - return [secDiff, nanoDiff]; - } - return [secsSinceStart, remainderInNanos]; - } - - /** - * A high resolution timestamp in milliseconds. - * - * @typedef {number} DOMHighResTimeStamp - */ - - /** - * performance.now() - * - * @returns {DOMHighResTimeStamp} - */ - function fakePerformanceNow() { - const hrt = hrtime(); - const millis = hrt[0] * 1000 + hrt[1] / 1e6; - return millis; - } - - if (intlPresent) { - clock.Intl = createIntl(); - clock.Intl.clock = clock; - } - - clock.requestIdleCallback = function requestIdleCallback( - func, - timeout, - ) { - let timeToNextIdlePeriod = 0; - - if (clock.countTimers() > 0) { - timeToNextIdlePeriod = 50; // const for now - } - - const result = addTimer(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 2), - delay: - typeof timeout === "undefined" - ? timeToNextIdlePeriod - : Math.min(timeout, timeToNextIdlePeriod), - idleCallback: true, - }); - - return Number(result); - }; - - clock.cancelIdleCallback = function cancelIdleCallback(timerId) { - return clearTimer(clock, timerId, "IdleCallback"); - }; - - clock.setTimeout = function setTimeout(func, timeout) { - return addTimer(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 2), - delay: timeout, - }); - }; - - clock.clearTimeout = function clearTimeout(timerId) { - return clearTimer(clock, timerId, "Timeout"); - }; - - clock.nextTick = function nextTick(func) { - return enqueueJob(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 1), - error: isNearInfiniteLimit ? new Error() : null, - }); - }; - - clock.queueMicrotask = function queueMicrotask(func) { - return clock.nextTick(func); // explicitly drop additional arguments - }; - - clock.setInterval = function setInterval(func, timeout) { - // eslint-disable-next-line no-param-reassign - timeout = parseInt(timeout, 10); - return addTimer(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 2), - delay: timeout, - interval: timeout, - }); - }; - - clock.clearInterval = function clearInterval(timerId) { - return clearTimer(clock, timerId, "Interval"); - }; - - if (setImmediatePresent) { - clock.setImmediate = function setImmediate(func) { - return addTimer(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 1), - immediate: true, - }); - }; - - clock.clearImmediate = function clearImmediate(timerId) { - return clearTimer(clock, timerId, "Immediate"); - }; - } - - clock.countTimers = function countTimers() { - return ( - Object.keys(clock.timers || {}).length + - (clock.jobs || []).length - ); - }; - - clock.requestAnimationFrame = function requestAnimationFrame(func) { - const result = addTimer(clock, { - func: func, - delay: getTimeToNextFrame(), - get args() { - return [fakePerformanceNow()]; - }, - animation: true, - }); - - return Number(result); - }; - - clock.cancelAnimationFrame = function cancelAnimationFrame(timerId) { - return clearTimer(clock, timerId, "AnimationFrame"); - }; - - clock.runMicrotasks = function runMicrotasks() { - runJobs(clock); - }; - - /** - * @param {number|string} tickValue milliseconds or a string parseable by parseTime - * @param {boolean} isAsync - * @param {Function} resolve - * @param {Function} reject - * @returns {number|undefined} will return the new `now` value or nothing for async - */ - function doTick(tickValue, isAsync, resolve, reject) { - const msFloat = - typeof tickValue === "number" - ? tickValue - : parseTime(tickValue); - const ms = Math.floor(msFloat); - const remainder = nanoRemainder(msFloat); - let nanosTotal = nanos + remainder; - let tickTo = clock.now + ms; - - if (msFloat < 0) { - throw new TypeError("Negative ticks are not supported"); - } - - // adjust for positive overflow - if (nanosTotal >= 1e6) { - tickTo += 1; - nanosTotal -= 1e6; - } - - nanos = nanosTotal; - let tickFrom = clock.now; - let previous = clock.now; - // ESLint fails to detect this correctly - /* eslint-disable prefer-const */ - let timer, - firstException, - oldNow, - nextPromiseTick, - compensationCheck, - postTimerCall; - /* eslint-enable prefer-const */ - - clock.duringTick = true; - - // perform microtasks - oldNow = clock.now; - runJobs(clock); - if (oldNow !== clock.now) { - // compensate for any setSystemTime() call during microtask callback - tickFrom += clock.now - oldNow; - tickTo += clock.now - oldNow; - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function doTickInner() { - // perform each timer in the requested range - timer = firstTimerInRange(clock, tickFrom, tickTo); - // eslint-disable-next-line no-unmodified-loop-condition - while (timer && tickFrom <= tickTo) { - if (clock.timers[timer.id]) { - tickFrom = timer.callAt; - clock.now = timer.callAt; - oldNow = clock.now; - try { - runJobs(clock); - callTimer(clock, timer); - } catch (e) { - firstException = firstException || e; - } - - if (isAsync) { - // finish up after native setImmediate callback to allow - // all native es6 promises to process their callbacks after - // each timer fires. - originalSetTimeout(nextPromiseTick); - return; - } - - compensationCheck(); - } - - postTimerCall(); - } - - // perform process.nextTick()s again - oldNow = clock.now; - runJobs(clock); - if (oldNow !== clock.now) { - // compensate for any setSystemTime() call during process.nextTick() callback - tickFrom += clock.now - oldNow; - tickTo += clock.now - oldNow; - } - clock.duringTick = false; - - // corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo] - timer = firstTimerInRange(clock, tickFrom, tickTo); - if (timer) { - try { - clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range - } catch (e) { - firstException = firstException || e; - } - } else { - // no timers remaining in the requested range: move the clock all the way to the end - clock.now = tickTo; - - // update nanos - nanos = nanosTotal; - } - if (firstException) { - throw firstException; - } - - if (isAsync) { - resolve(clock.now); - } else { - return clock.now; - } - } - - nextPromiseTick = - isAsync && - function () { - try { - compensationCheck(); - postTimerCall(); - doTickInner(); - } catch (e) { - reject(e); - } - }; - - compensationCheck = function () { - // compensate for any setSystemTime() call during timer callback - if (oldNow !== clock.now) { - tickFrom += clock.now - oldNow; - tickTo += clock.now - oldNow; - previous += clock.now - oldNow; - } - }; - - postTimerCall = function () { - timer = firstTimerInRange(clock, previous, tickTo); - previous = tickFrom; - }; - - return doTickInner(); - } - - /** - * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" - * @returns {number} will return the new `now` value - */ - clock.tick = function tick(tickValue) { - return doTick(tickValue, false); - }; - - if (typeof _global.Promise !== "undefined") { - /** - * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" - * @returns {Promise} - */ - clock.tickAsync = function tickAsync(tickValue) { - return new _global.Promise(function (resolve, reject) { - originalSetTimeout(function () { - try { - doTick(tickValue, true, resolve, reject); - } catch (e) { - reject(e); - } - }); - }); - }; - } - - clock.next = function next() { - runJobs(clock); - const timer = firstTimer(clock); - if (!timer) { - return clock.now; - } - - clock.duringTick = true; - try { - clock.now = timer.callAt; - callTimer(clock, timer); - runJobs(clock); - return clock.now; - } finally { - clock.duringTick = false; - } - }; - - if (typeof _global.Promise !== "undefined") { - clock.nextAsync = function nextAsync() { - return new _global.Promise(function (resolve, reject) { - originalSetTimeout(function () { - try { - const timer = firstTimer(clock); - if (!timer) { - resolve(clock.now); - return; - } - - let err; - clock.duringTick = true; - clock.now = timer.callAt; - try { - callTimer(clock, timer); - } catch (e) { - err = e; - } - clock.duringTick = false; - - originalSetTimeout(function () { - if (err) { - reject(err); - } else { - resolve(clock.now); - } - }); - } catch (e) { - reject(e); - } - }); - }); - }; - } - - clock.runAll = function runAll() { - let numTimers, i; - runJobs(clock); - for (i = 0; i < clock.loopLimit; i++) { - if (!clock.timers) { - resetIsNearInfiniteLimit(); - return clock.now; - } - - numTimers = Object.keys(clock.timers).length; - if (numTimers === 0) { - resetIsNearInfiniteLimit(); - return clock.now; - } - - clock.next(); - checkIsNearInfiniteLimit(clock, i); - } - - const excessJob = firstTimer(clock); - throw getInfiniteLoopError(clock, excessJob); - }; - - clock.runToFrame = function runToFrame() { - return clock.tick(getTimeToNextFrame()); - }; - - if (typeof _global.Promise !== "undefined") { - clock.runAllAsync = function runAllAsync() { - return new _global.Promise(function (resolve, reject) { - let i = 0; - /** - * - */ - function doRun() { - originalSetTimeout(function () { - try { - runJobs(clock); - - let numTimers; - if (i < clock.loopLimit) { - if (!clock.timers) { - resetIsNearInfiniteLimit(); - resolve(clock.now); - return; - } - - numTimers = Object.keys( - clock.timers, - ).length; - if (numTimers === 0) { - resetIsNearInfiniteLimit(); - resolve(clock.now); - return; - } - - clock.next(); - - i++; - - doRun(); - checkIsNearInfiniteLimit(clock, i); - return; - } - - const excessJob = firstTimer(clock); - reject(getInfiniteLoopError(clock, excessJob)); - } catch (e) { - reject(e); - } - }); - } - doRun(); - }); - }; - } - - clock.runToLast = function runToLast() { - const timer = lastTimer(clock); - if (!timer) { - runJobs(clock); - return clock.now; - } - - return clock.tick(timer.callAt - clock.now); - }; - - if (typeof _global.Promise !== "undefined") { - clock.runToLastAsync = function runToLastAsync() { - return new _global.Promise(function (resolve, reject) { - originalSetTimeout(function () { - try { - const timer = lastTimer(clock); - if (!timer) { - runJobs(clock); - resolve(clock.now); - } - - resolve(clock.tickAsync(timer.callAt - clock.now)); - } catch (e) { - reject(e); - } - }); - }); - }; - } - - clock.reset = function reset() { - nanos = 0; - clock.timers = {}; - clock.jobs = []; - clock.now = start; - }; - - clock.setSystemTime = function setSystemTime(systemTime) { - // determine time difference - const newNow = getEpoch(systemTime); - const difference = newNow - clock.now; - let id, timer; - - adjustedSystemTime[0] = adjustedSystemTime[0] + difference; - adjustedSystemTime[1] = adjustedSystemTime[1] + nanos; - // update 'system clock' - clock.now = newNow; - nanos = 0; - - // update timers and intervals to keep them stable - for (id in clock.timers) { - if (clock.timers.hasOwnProperty(id)) { - timer = clock.timers[id]; - timer.createdAt += difference; - timer.callAt += difference; - } - } - }; - - /** - * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" - * @returns {number} will return the new `now` value - */ - clock.jump = function jump(tickValue) { - const msFloat = - typeof tickValue === "number" - ? tickValue - : parseTime(tickValue); - const ms = Math.floor(msFloat); - - for (const timer of Object.values(clock.timers)) { - if (clock.now + ms > timer.callAt) { - timer.callAt = clock.now + ms; - } - } - clock.tick(ms); - }; - - if (performancePresent) { - clock.performance = Object.create(null); - clock.performance.now = fakePerformanceNow; - } - - return clock; - } - - /* eslint-disable complexity */ - - /** - * @param {Config=} [config] Optional config - * @returns {Clock} - */ - function install(config) { - console.log('INSTALL', config); - if ( - arguments.length > 1 || - config instanceof Date || - Array.isArray(config) || - typeof config === "number" - ) { - throw new TypeError( - `FakeTimers.install called with ${String( - config, - )} install requires an object parameter`, - ); - } - - if (_global.Date.isFake === true) { - // Timers are already faked; this is a problem. - // Make the user reset timers before continuing. - throw new TypeError( - "Can't install fake timers twice on the same global object.", - ); - } - - // eslint-disable-next-line no-param-reassign - config = typeof config !== "undefined" ? config : {}; - config.shouldAdvanceTime = config.shouldAdvanceTime || false; - config.advanceTimeDelta = config.advanceTimeDelta || 20; - config.shouldClearNativeTimers = - config.shouldClearNativeTimers || false; - - if (config.target) { - throw new TypeError( - "config.target is no longer supported. Use `withGlobal(target)` instead.", - ); - } - - let i, l; - const clock = createClock(config.now, config.loopLimit); - clock.shouldClearNativeTimers = config.shouldClearNativeTimers; - - clock.uninstall = function () { - return uninstall(clock, config); - }; - - clock.methods = config.toFake || []; - - if (clock.methods.length === 0) { - // do not fake nextTick by default - GitHub#126 - clock.methods = Object.keys(timers).filter(function (key) { - return key !== "nextTick" && key !== "queueMicrotask"; - }); - } - - if (config.shouldAdvanceTime === true) { - const intervalTick = doIntervalTick.bind( - null, - clock, - config.advanceTimeDelta, - ); - const intervalId = _global.setInterval( - intervalTick, - config.advanceTimeDelta, - ); - clock.attachedInterval = intervalId; - } - - if (clock.methods.includes("performance")) { - const proto = (() => { - if (hasPerformanceConstructorPrototype) { - return _global.performance.constructor.prototype; - } - if (hasPerformancePrototype) { - return _global.Performance.prototype; - } - })(); - if (proto) { - Object.getOwnPropertyNames(proto).forEach(function (name) { - if (name !== "now") { - clock.performance[name] = - name.indexOf("getEntries") === 0 - ? NOOP_ARRAY - : NOOP; - } - }); - } else if ((config.toFake || []).includes("performance")) { - // user explicitly tried to fake performance when not present - throw new ReferenceError( - "non-existent performance object cannot be faked", - ); - } - } - - for (i = 0, l = clock.methods.length; i < l; i++) { - const nameOfMethodToReplace = clock.methods[i]; - hijackMethod(_global, nameOfMethodToReplace, clock); - } - - return clock; - } - - /* eslint-enable complexity */ - - return { - timers: timers, - createClock: createClock, - install: install, - withGlobal: withGlobal, - }; -} - -/** - * @typedef {object} FakeTimers - * @property {Function} install - * @property {withGlobal} withGlobal - */ - -/* eslint-enable complexity */ - -/** @type {FakeTimers} */ -const defaultImplementation = withGlobal(globalThis); -exports.install = defaultImplementation.install; diff --git a/tests/page/page-clock.frozen.spec.ts b/tests/page/page-clock.frozen.spec.ts new file mode 100644 index 0000000000..3d763a2cc3 --- /dev/null +++ b/tests/page/page-clock.frozen.spec.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test as it, expect } from './pageTest'; + +it.skip(!process.env.PW_FREEZE_TIME); + +it('cock should be frozen', async ({ page }) => { + expect(await page.evaluate('Date.now()')).toBe(0); +}); diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index ccc9e8773c..649ab250bd 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -549,6 +549,19 @@ it.describe('stubTimers', () => { expect(await promise).toEqual({ prev: 0, next: 1000 }); }); + it('replaces global performance.timeOrigin', async ({ page }) => { + await page.clock.installFakeTimers(1000); + const promise = page.evaluate(async () => { + const prev = performance.now(); + await new Promise(f => setTimeout(f, 1000)); + const next = performance.now(); + return { prev, next }; + }); + expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000); + await page.clock.runFor(1000); + expect(await promise).toEqual({ prev: 0, next: 1000 }); + }); + it('fakes Date constructor', async ({ page }) => { await page.clock.installFakeTimers(0); const now = await page.evaluate(() => new Date().getTime()); diff --git a/utils/build/build.js b/utils/build/build.js index 980802735d..56442c0948 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -311,6 +311,7 @@ steps.push({ onChanges.push({ inputs: [ 'packages/playwright-core/src/server/injected/**', + 'packages/playwright-core/src/third_party/**', 'packages/playwright-ct-core/src/injected/**', 'packages/playwright-core/src/utils/isomorphic/**', 'utils/generate_injected.js', From c8b7cda51403e64738bab6b43b72bf895d58c633 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 6 Jun 2024 07:49:45 -0700 Subject: [PATCH 027/144] feat(chromium-tip-of-tree): roll to r1229 (#31187) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 2abf7780cc..6be764325d 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1227", + "revision": "1229", "installByDefault": false, - "browserVersion": "127.0.6510.0" + "browserVersion": "127.0.6523.0" }, { "name": "firefox", From fc6fcc2118bc14771edb554181f0d9d7ae1da769 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 6 Jun 2024 08:55:38 -0700 Subject: [PATCH 028/144] chore: remove same site hack for libsoup on the client (#31192) --- packages/playwright-core/src/server/webkit/wkBrowser.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index b65ca55c21..a8541e73cf 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -268,8 +268,6 @@ export class WKBrowserContext extends BrowserContext { ...c, session: c.expires === -1 || c.expires === undefined, expires: c.expires && c.expires !== -1 ? c.expires * 1000 : c.expires, - // TODO: make WebKit on linux work without eplicit sameSite. - sameSite: c.sameSite ?? (process.platform === 'linux' ? 'Lax' : undefined) })) as Protocol.Playwright.SetCookieParam[]; await this._browser._browserSession.send('Playwright.setCookies', { cookies: cc, browserContextId: this._browserContextId }); } From e259f802d6e4d261752e4161bbcf7e79be8c0735 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:02:58 -0700 Subject: [PATCH 029/144] feat(chromium): roll to r1122 (#31188) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 4 +- packages/playwright-core/browsers.json | 4 +- .../src/server/chromium/protocol.d.ts | 6 ++ .../src/server/deviceDescriptorsSource.json | 96 +++++++++---------- packages/playwright-core/types/protocol.d.ts | 6 ++ 5 files changed, 64 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 740f3a7055..e9285ca940 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-126.0.6478.26-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-126.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-126.0.6478.36-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-126.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 126.0.6478.26 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 126.0.6478.36 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 17.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 126.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 6be764325d..5a79d48597 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,9 +3,9 @@ "browsers": [ { "name": "chromium", - "revision": "1121", + "revision": "1122", "installByDefault": true, - "browserVersion": "126.0.6478.26" + "browserVersion": "126.0.6478.36" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index efa5c36de0..e64c53d6fc 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -12737,6 +12737,12 @@ in which case the content will be scaled to fit the paper size. Argument will be ignored if reloading dataURL origin. */ scriptToEvaluateOnLoad?: string; + /** + * If set, an error will be thrown if the target page's main frame's +loader id does not match the provided id. This prevents accidentally +reloading an unintended target in case there's a racing navigation. + */ + loaderId?: Network.LoaderId; } export type reloadReturnValue = { } diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index dc6fa7c17e..6c59c3377d 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -978,7 +978,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -989,7 +989,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1000,7 +1000,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1011,7 +1011,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1022,7 +1022,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1033,7 +1033,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1044,7 +1044,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1055,7 +1055,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1066,7 +1066,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1077,7 +1077,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1088,7 +1088,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1099,7 +1099,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1110,7 +1110,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1121,7 +1121,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1132,7 +1132,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1143,7 +1143,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1154,7 +1154,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1165,7 +1165,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1176,7 +1176,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1187,7 +1187,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1242,7 +1242,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1253,7 +1253,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1264,7 +1264,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1275,7 +1275,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1286,7 +1286,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1297,7 +1297,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1308,7 +1308,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1319,7 +1319,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1330,7 +1330,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1345,7 +1345,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1360,7 +1360,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1375,7 +1375,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1390,7 +1390,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1405,7 +1405,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1420,7 +1420,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1431,7 +1431,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1442,7 +1442,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1457,7 +1457,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36 Edg/126.0.6478.26", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36 Edg/126.0.6478.36", "screen": { "width": 1792, "height": 1120 @@ -1502,7 +1502,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1517,7 +1517,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.26 Safari/537.36 Edg/126.0.6478.26", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.36 Safari/537.36 Edg/126.0.6478.36", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index efa5c36de0..e64c53d6fc 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -12737,6 +12737,12 @@ in which case the content will be scaled to fit the paper size. Argument will be ignored if reloading dataURL origin. */ scriptToEvaluateOnLoad?: string; + /** + * If set, an error will be thrown if the target page's main frame's +loader id does not match the provided id. This prevents accidentally +reloading an unintended target in case there's a racing navigation. + */ + loaderId?: Network.LoaderId; } export type reloadReturnValue = { } From 8782936a00387c47cb9f956bcb045fea1b04374c Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:17:17 -0700 Subject: [PATCH 030/144] feat(webkit): roll to r2022 (#31191) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 5a79d48597..db28f63c8f 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2019", + "revision": "2022", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 3e86ebc80cff78520dcced94201e9f85f6d92d75 Mon Sep 17 00:00:00 2001 From: Denis Paris Date: Thu, 6 Jun 2024 22:07:56 +0200 Subject: [PATCH 031/144] docs(trace-viewer): add link to trace object docs (#31182) --- docs/src/trace-viewer.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/trace-viewer.md b/docs/src/trace-viewer.md index 55c4f2d445..90ec4a5e7c 100644 --- a/docs/src/trace-viewer.md +++ b/docs/src/trace-viewer.md @@ -160,6 +160,8 @@ Available options to record a trace: You can also use `trace: 'retain-on-failure'` if you do not enable retries but still want traces for failed tests. +There are more granular options available, see [`property: TestOptions.trace`]. + If you are not using Playwright as a Test Runner, use the [`property: BrowserContext.tracing`] API instead. ## Recording a trace From 826343b8a032eddb5e4a492625b0c35ec50ef775 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 6 Jun 2024 15:56:13 -0700 Subject: [PATCH 032/144] chore: rename fakeTimers to clock (#31193) --- docs/src/api/class-browsercontext.md | 2 +- docs/src/api/class-page.md | 2 +- packages/playwright-core/src/server/clock.ts | 40 +- .../injected/{fakeTimers.ts => clock.ts} | 272 +- .../src/server/injected/injectedScript.ts | 10 +- .../src/server/injected/utilityScript.ts | 28 +- packages/playwright-core/types/types.d.ts | 4 +- tests/library/clock.spec.ts | 3086 +++++++++++++++++ utils/generate_injected.js | 2 +- 9 files changed, 3223 insertions(+), 223 deletions(-) rename packages/playwright-core/src/server/injected/{fakeTimers.ts => clock.ts} (81%) create mode 100644 tests/library/clock.spec.ts diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 8cb11dbc0c..f9ab6694c3 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -102,7 +102,7 @@ context.BackgroundPage += (_, backgroundPage) => * since: v1.45 - type: <[Clock]> -Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock. +Playwright has ability to mock clock and passage of time. ## event: BrowserContext.close * since: v1.8 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index b4ee91eb1b..6f8d04ceb4 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -155,7 +155,7 @@ page.Load -= PageLoadHandler; * since: v1.45 - type: <[Clock]> -Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock. +Playwright has ability to mock clock and passage of time. ## event: Page.close * since: v1.8 diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index a366288476..09ac9cfec3 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -15,12 +15,12 @@ */ import type { BrowserContext } from './browserContext'; -import * as fakeTimersSource from '../generated/fakeTimersSource'; +import * as clockSource from '../generated/clockSource'; export class Clock { private _browserContext: BrowserContext; private _scriptInjected = false; - private _fakeTimersInstalled = false; + private _clockInstalled = false; private _now = 0; constructor(browserContext: BrowserContext) { @@ -30,48 +30,48 @@ export class Clock { async installFakeTimers(time: number, loopLimit: number | undefined) { await this._injectScriptIfNeeded(); await this._addAndEvaluate(`(() => { - globalThis.__pwFakeTimers.clock?.uninstall(); - globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, loopLimit })}); + globalThis.__pwClock.clock?.uninstall(); + globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit })}); })();`); this._now = time; - this._fakeTimersInstalled = true; + this._clockInstalled = true; } async runToNextTimer(): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.next()`); - this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.nextAsync()`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.next()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.nextAsync()`); return this._now; } async runAllTimers(): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runAll()`); - this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runAllAsync()`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runAll()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAllAsync()`); return this._now; } async runToLastTimer(): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runToLast()`); - this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runToLastAsync()`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runToLast()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLastAsync()`); return this._now; } async setTime(time: number) { - if (this._fakeTimersInstalled) { + if (this._clockInstalled) { const jump = time - this._now; if (jump < 0) throw new Error('Unable to set time into the past when fake timers are installed'); - await this._addAndEvaluate(`globalThis.__pwFakeTimers.clock.jump(${jump})`); + await this._addAndEvaluate(`globalThis.__pwClock.clock.jump(${jump})`); this._now = time; return this._now; } await this._injectScriptIfNeeded(); await this._addAndEvaluate(`(() => { - globalThis.__pwFakeTimers.clock?.uninstall(); - globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); + globalThis.__pwClock.clock?.uninstall(); + globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); })();`); this._now = time; return this._now; @@ -85,8 +85,8 @@ export class Clock { async runFor(time: number | string): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.tick(${JSON.stringify(time)})`); - this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.tickAsync(${JSON.stringify(time)})`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tickAsync(${JSON.stringify(time)})`); return this._now; } @@ -96,8 +96,8 @@ export class Clock { this._scriptInjected = true; const script = `(() => { const module = {}; - ${fakeTimersSource.source} - globalThis.__pwFakeTimers = (module.exports.inject())(globalThis); + ${clockSource.source} + globalThis.__pwClock = (module.exports.inject())(globalThis); })();`; await this._addAndEvaluate(script); } @@ -114,7 +114,7 @@ export class Clock { } private _assertInstalled() { - if (!this._fakeTimersInstalled) + if (!this._clockInstalled) throw new Error('Clock is not installed'); } } diff --git a/packages/playwright-core/src/server/injected/fakeTimers.ts b/packages/playwright-core/src/server/injected/clock.ts similarity index 81% rename from packages/playwright-core/src/server/injected/fakeTimers.ts rename to packages/playwright-core/src/server/injected/clock.ts index fbec480980..6d9f0a2036 100644 --- a/packages/playwright-core/src/server/injected/fakeTimers.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -10,7 +10,7 @@ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -type ClockMethods = { +export type ClockMethods = { Date: DateConstructor; setTimeout: Window['setTimeout']; clearTimeout: Window['clearTimeout']; @@ -24,12 +24,12 @@ type ClockMethods = { performance?: Window['performance']; }; -type ClockConfig = { +export type ClockConfig = { now?: number | Date; loopLimit?: number; }; -type InstallConfig = ClockConfig & { +export type InstallConfig = ClockConfig & { toFake?: (keyof ClockMethods)[]; }; @@ -44,7 +44,7 @@ enum TimerType { type Timer = { type: TimerType; func: TimerHandler; - args: any[]; + args: () => any[]; delay: number; callAt: number; createdAt: number; @@ -57,15 +57,13 @@ interface Embedder { postTaskPeriodically(task: () => void, delay: number): () => void; } -class Clock { +export class ClockController { readonly start: number; private _now: number; private _loopLimit: number; - private _jobs: Timer[] = []; private _adjustedSystemTime = 0; private _duringTick = false; private _timers = new Map(); - private _isNearInfiniteLimit = false; private _uniqueTimerId = idCounterStart; private _embedder: Embedder; readonly disposables: (() => void)[] = []; @@ -88,10 +86,7 @@ class Clock { } performanceNow(): DOMHighResTimeStamp { - const millisSinceStart = this._now - this._adjustedSystemTime - this.start; - const secsSinceStart = Math.floor(millisSinceStart / 1000); - const millis = secsSinceStart * 1000; - return millis; + return this._now - this._adjustedSystemTime - this.start; } private _doTick(tickValue: number | string, isAsync: boolean, resolve?: (time: number) => void, reject?: (error: Error) => void): number | undefined { @@ -116,13 +111,10 @@ class Clock { let compensationCheck: () => void; let postTimerCall: () => void; - /* eslint-enable prefer-const */ - this._duringTick = true; // perform microtasks oldNow = this._now; - this._runJobs(); if (oldNow !== this._now) { // compensate for any setSystemTime() call during microtask callback tickFrom += this._now - oldNow; @@ -138,7 +130,6 @@ class Clock { this._now = timer.callAt; oldNow = this._now; try { - this._runJobs(); this._callTimer(timer); } catch (e) { firstException = firstException || e; @@ -158,7 +149,6 @@ class Clock { // perform process.nextTick()s again oldNow = this._now; - this._runJobs(); if (oldNow !== this._now) { // compensate for any setSystemTime() call during process.nextTick() callback tickFrom += this._now - oldNow; @@ -220,20 +210,12 @@ class Clock { return this._doTick(tickValue, false)!; } - tickAsync(tickValue: string | number): Promise { - return new Promise((resolve, reject) => { - this._embedder.postTask(() => { - try { - this._doTick(tickValue, true, resolve, reject); - } catch (e) { - reject(e); - } - }); - }); + async tickAsync(tickValue: string | number): Promise { + await new Promise(f => this._embedder.postTask(f)); + return new Promise((resolve, reject) => this._doTick(tickValue, true, resolve, reject)); } next() { - this._runJobs(); const timer = this._firstTimer(); if (!timer) return this._now; @@ -242,117 +224,73 @@ class Clock { try { this._now = timer.callAt; this._callTimer(timer); - this._runJobs(); return this._now; } finally { this._duringTick = false; } } - nextAsync() { - return new Promise((resolve, reject) => { - this._embedder.postTask(() => { - try { - const timer = this._firstTimer(); - if (!timer) { - resolve(this._now); - return; - } + async nextAsync() { + await new Promise(f => this._embedder.postTask(f)); + const timer = this._firstTimer(); + if (!timer) + return this._now; - let err: Error; - this._duringTick = true; - this._now = timer.callAt; - try { - this._callTimer(timer); - } catch (e) { - err = e; - } - this._duringTick = false; + let err: Error | undefined; + this._duringTick = true; + this._now = timer.callAt; + try { + this._callTimer(timer); + } catch (e) { + err = e; + } + this._duringTick = false; - this._embedder.postTask(() => { - if (err) - reject(err); - else - resolve(this._now); - }); - } catch (e) { - reject(e); - } - }); - }); + await new Promise(f => this._embedder.postTask(f)); + if (err) + throw err; + return this._now; } runAll() { - this._runJobs(); for (let i = 0; i < this._loopLimit; i++) { const numTimers = this._timers.size; - if (numTimers === 0) { - this._resetIsNearInfiniteLimit(); + if (numTimers === 0) return this._now; - } - this.next(); - this._checkIsNearInfiniteLimit(i); } const excessJob = this._firstTimer(); - throw this._getInfiniteLoopError(excessJob!); + if (!excessJob) + return; + throw this._getInfiniteLoopError(excessJob); } runToFrame() { return this.tick(this.getTimeToNextFrame()); } - runAllAsync() { - return new Promise((resolve, reject) => { - let i = 0; - /** - * - */ - const doRun = () => { - this._embedder.postTask(() => { - try { - this._runJobs(); + async runAllAsync() { + for (let i = 0; i < this._loopLimit; i++) { + await new Promise(f => this._embedder.postTask(f)); + const numTimers = this._timers.size; + if (numTimers === 0) + return this._now; - let numTimers; - if (i < this._loopLimit) { - if (!this._timers) { - this._resetIsNearInfiniteLimit(); - resolve(this._now); - return; - } + this.next(); + } + await new Promise(f => this._embedder.postTask(f)); - numTimers = this._timers.size; - if (numTimers === 0) { - this._resetIsNearInfiniteLimit(); - resolve(this._now); - return; - } - - this.next(); - i++; - doRun(); - this._checkIsNearInfiniteLimit(i); - return; - } - - const excessJob = this._firstTimer(); - reject(this._getInfiniteLoopError(excessJob!)); - } catch (e) { - reject(e); - } - }); - }; - doRun(); - }); + const excessJob = this._firstTimer(); + if (!excessJob) + return; + throw this._getInfiniteLoopError(excessJob); } runToLast() { const timer = this._lastTimer(); - if (!timer) { - this._runJobs(); + if (!timer) return this._now; - } return this.tick(timer.callAt - this._now); } @@ -362,7 +300,6 @@ class Clock { try { const timer = this._lastTimer(); if (!timer) { - this._runJobs(); resolve(this._now); return; } @@ -376,7 +313,6 @@ class Clock { reset() { this._timers.clear(); - this._jobs = []; this._now = this.start; } @@ -410,34 +346,7 @@ class Clock { return this.tick(ms); } - private _checkIsNearInfiniteLimit(i: number): void { - if (this._loopLimit && i === this._loopLimit - 1) - this._isNearInfiniteLimit = true; - - } - - private _resetIsNearInfiniteLimit() { - this._isNearInfiniteLimit = false; - } - - private _runJobs() { - // runs all microtick-deferred tasks - ecma262/#sec-runjobs - if (!this._jobs) - return; - for (let i = 0; i < this._jobs.length; i++) { - const job = this._jobs[i]; - callFunction(job.func, job.args); - - this._checkIsNearInfiniteLimit(i); - if (this._loopLimit && i > this._loopLimit) - throw this._getInfiniteLoopError(job); - - } - this._resetIsNearInfiniteLimit(); - this._jobs = []; - } - - addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { + addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: () => any[] }): number { if (options.func === undefined) throw new Error('Callback must be provided to timer calls'); @@ -450,12 +359,12 @@ class Clock { const timer: Timer = { type: options.type, func: options.func, - args: options.args || [], + args: options.args || (() => []), delay, callAt: this._now + (delay || (this._duringTick ? 1 : 0)), createdAt: this._now, id: this._uniqueTimerId++, - error: this._isNearInfiniteLimit ? new Error() : undefined, + error: new Error(), }; this._timers.set(timer.id, timer); return timer.id; @@ -472,7 +381,7 @@ class Clock { } countTimers() { - return this._timers.size + this._jobs.length; + return this._timers.size; } private _firstTimer(): Timer | null { @@ -500,7 +409,7 @@ class Clock { this._timers.get(timer.id)!.callAt += timer.delay; else this._timers.delete(timer.id); - callFunction(timer.func, timer.args); + callFunction(timer.func, timer.args()); } private _getInfiniteLoopError(job: Timer) { @@ -548,14 +457,7 @@ class Clock { .slice(matchedLineIndex + 1) .join('\n')}`; - try { - Object.defineProperty(infiniteLoopError, 'stack', { - value: stack, - }); - } catch (e) { - // noop - } - + infiniteLoopError.stack = stack; return infiniteLoopError; } @@ -661,7 +563,7 @@ function mirrorDateProperties(target: any, source: typeof Date): DateConstructor return target; } -function createDate(clock: Clock, NativeDate: typeof Date): DateConstructor & Date { +function createDate(clock: ClockController, NativeDate: typeof Date): DateConstructor & Date { function ClockDate(this: typeof ClockDate, year: number, month: number, date: number, hour: number, minute: number, second: number, ms: number): Date | string { // the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2. // This remains so in the 10th edition of 2019 as well. @@ -717,7 +619,7 @@ function createDate(clock: Clock, NativeDate: typeof Date): DateConstructor & Da * but we need to take control of those that have a * dependency on the current clock. */ -function createIntl(clock: Clock, NativeIntl: typeof Intl): typeof Intl { +function createIntl(clock: ClockController, NativeIntl: typeof Intl): typeof Intl { const ClockIntl: any = {}; /* * All properties of Intl are non-enumerable, so we need @@ -726,7 +628,7 @@ function createIntl(clock: Clock, NativeIntl: typeof Intl): typeof Intl { for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[]) ClockIntl[key] = NativeIntl[key]; - ClockIntl.DateTimeFormat = (...args: any[]) => { + ClockIntl.DateTimeFormat = function(...args: any[]) { const realFormatter = new NativeIntl.DateTimeFormat(...args); const formatter: Intl.DateTimeFormat = { formatRange: realFormatter.formatRange.bind(realFormatter), @@ -787,20 +689,26 @@ function callFunction(func: TimerHandler, args: any[]) { const maxTimeout = Math.pow(2, 31) - 1; // see https://heycam.github.io/webidl/#abstract-opdef-converttoint const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs -function platformOriginals(globalObject: WindowOrWorkerGlobalScope): ClockMethods { - return { - setTimeout: globalObject.setTimeout.bind(globalObject), - clearTimeout: globalObject.clearTimeout.bind(globalObject), - setInterval: globalObject.setInterval.bind(globalObject), - clearInterval: globalObject.clearInterval.bind(globalObject), - requestAnimationFrame: (globalObject as any).requestAnimationFrame ? (globalObject as any).requestAnimationFrame.bind(globalObject) : undefined, - cancelAnimationFrame: (globalObject as any).cancelAnimationFrame ? (globalObject as any).cancelAnimationFrame.bind(globalObject) : undefined, - requestIdleCallback: (globalObject as any).requestIdleCallback ? (globalObject as any).requestIdleCallback.bind(globalObject) : undefined, - cancelIdleCallback: (globalObject as any).cancelIdleCallback ? (globalObject as any).cancelIdleCallback.bind(globalObject) : undefined, +function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: ClockMethods, bound: ClockMethods } { + const raw: ClockMethods = { + setTimeout: globalObject.setTimeout, + clearTimeout: globalObject.clearTimeout, + setInterval: globalObject.setInterval, + clearInterval: globalObject.clearInterval, + requestAnimationFrame: (globalObject as any).requestAnimationFrame ? (globalObject as any).requestAnimationFrame : undefined, + cancelAnimationFrame: (globalObject as any).cancelAnimationFrame ? (globalObject as any).cancelAnimationFrame : undefined, + requestIdleCallback: (globalObject as any).requestIdleCallback ? (globalObject as any).requestIdleCallback : undefined, + cancelIdleCallback: (globalObject as any).cancelIdleCallback ? (globalObject as any).cancelIdleCallback : undefined, Date: (globalObject as any).Date, performance: globalObject.performance, Intl: (globalObject as any).Intl, }; + const bound = { ...raw }; + for (const key of Object.keys(bound) as (keyof ClockMethods)[]) { + if (key !== 'Date' && typeof bound[key] === 'function') + bound[key] = (bound[key] as any).bind(globalObject); + } + return { raw, bound }; } /** @@ -813,14 +721,14 @@ function getScheduleHandler(type: TimerType) { return `set${type}`; } -function createApi(clock: Clock, originals: ClockMethods): ClockMethods { +function createApi(clock: ClockController, originals: ClockMethods): ClockMethods { return { setTimeout: (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => { const delay = timeout ? +timeout : timeout; return clock.addTimer({ type: TimerType.Timeout, func, - args, + args: () => args, delay }); }, @@ -833,7 +741,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods { return clock.addTimer({ type: TimerType.Interval, func, - args, + args: () => args, delay, }); }, @@ -846,9 +754,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods { type: TimerType.AnimationFrame, func: callback, delay: clock.getTimeToNextFrame(), - get args() { - return [clock.performanceNow()]; - }, + args: () => [clock.performanceNow()], }); }, cancelAnimationFrame: (timerId: number): void => { @@ -863,7 +769,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods { return clock.addTimer({ type: TimerType.IdleCallback, func: callback, - args: [], + args: () => [], delay: options?.timeout ? Math.min(options?.timeout, timeToNextIdlePeriod) : timeToNextIdlePeriod, }); }, @@ -884,33 +790,41 @@ function getClearHandler(type: TimerType) { return `clear${type}`; } -function fakePerformance(clock: Clock, performance: Performance): Performance { +function fakePerformance(clock: ClockController, performance: Performance): Performance { const result: any = { now: () => clock.performanceNow(), timeOrigin: clock.start, - __proto__: performance, }; + // eslint-disable-next-line no-proto + for (const key of Object.keys((performance as any).__proto__)) { + if (key === 'now' || key === 'timeOrigin') + continue; + if (key === 'getEntries' || key === 'getEntriesByName' || key === 'getEntriesByType') + result[key] = () => []; + else + result[key] = () => {}; + } return result; } -export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: Clock, api: Partial, originals: Partial } { +export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { const originals = platformOriginals(globalObject); const embedder = { postTask: (task: () => void) => { - originals.setTimeout!(task, 0); + originals.bound.setTimeout(task, 0); }, postTaskPeriodically: (task: () => void, delay: number) => { const intervalId = globalObject.setInterval(task, delay); - return () => originals.clearInterval!(intervalId); + return () => originals.bound.clearInterval(intervalId); }, }; - const clock = new Clock(embedder, config.now, config.loopLimit); - const api = createApi(clock, originals); - return { clock, api, originals }; + const clock = new ClockController(embedder, config.now, config.loopLimit); + const api = createApi(clock, originals.bound); + return { clock, api, originals: originals.raw }; } -export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: Clock, api: Partial, originals: Partial } { +export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { if ((globalObject as any).Date?.isFake) { // Timers are already faked; this is a problem. // Make the user reset timers before continuing. @@ -946,6 +860,6 @@ export function inject(globalObject: WindowOrWorkerGlobalScope) { const { clock } = install(globalObject, config); return clock; }, - builtin: platformOriginals(globalObject), + builtin: platformOriginals(globalObject).bound, }; } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 347d5cb40c..b7a395cfed 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -125,14 +125,14 @@ export class InjectedScript { } builtinSetTimeout(callback: Function, timeout: number) { - if (this.window.__pwFakeTimers?.builtin) - return this.window.__pwFakeTimers.builtin.setTimeout(callback, timeout); + if (this.window.__pwClock?.builtin) + return this.window.__pwClock.builtin.setTimeout(callback, timeout); return setTimeout(callback, timeout); } builtinRequestAnimationFrame(callback: FrameRequestCallback) { - if (this.window.__pwFakeTimers?.builtin) - return this.window.__pwFakeTimers.builtin.requestAnimationFrame(callback); + if (this.window.__pwClock?.builtin) + return this.window.__pwClock.builtin.requestAnimationFrame(callback); return requestAnimationFrame(callback); } @@ -1525,7 +1525,7 @@ function deepEquals(a: any, b: any): boolean { declare global { interface Window { - __pwFakeTimers?: { + __pwClock?: { builtin: { setTimeout: Window['setTimeout'], requestAnimationFrame: Window['requestAnimationFrame'], diff --git a/packages/playwright-core/src/server/injected/utilityScript.ts b/packages/playwright-core/src/server/injected/utilityScript.ts index e8dbbc6ba1..7b046a529a 100644 --- a/packages/playwright-core/src/server/injected/utilityScript.ts +++ b/packages/playwright-core/src/server/injected/utilityScript.ts @@ -79,42 +79,42 @@ export class UtilityScript { // eslint-disable-next-line no-restricted-globals const window = (globalThis as any); window.builtinSetTimeout = (callback: Function, timeout: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.setTimeout(callback, timeout); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.setTimeout(callback, timeout); return setTimeout(callback, timeout); }; window.builtinClearTimeout = (id: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.clearTimeout(id); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.clearTimeout(id); return clearTimeout(id); }; window.builtinSetInterval = (callback: Function, timeout: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.setInterval(callback, timeout); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.setInterval(callback, timeout); return setInterval(callback, timeout); }; window.builtinClearInterval = (id: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.clearInterval(id); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.clearInterval(id); return clearInterval(id); }; window.builtinRequestAnimationFrame = (callback: FrameRequestCallback) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.requestAnimationFrame(callback); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.requestAnimationFrame(callback); return requestAnimationFrame(callback); }; window.builtinCancelAnimationFrame = (id: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.cancelAnimationFrame(id); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.cancelAnimationFrame(id); return cancelAnimationFrame(id); }; - window.builtinDate = window.__pwFakeTimers?.builtin.Date || Date; - window.builtinPerformance = window.__pwFakeTimers?.builtin.performance || performance; + window.builtinDate = window.__pwClock?.builtin.Date || Date; + window.builtinPerformance = window.__pwClock?.builtin.performance || performance; } } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index dc91c655f0..3753d59986 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -4864,7 +4864,7 @@ export interface Page { accessibility: Accessibility; /** - * Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock. + * Playwright has ability to mock clock and passage of time. */ clock: Clock; @@ -8986,7 +8986,7 @@ export interface BrowserContext { /** - * Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock. + * Playwright has ability to mock clock and passage of time. */ clock: Clock; diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts new file mode 100644 index 0000000000..bb351186b5 --- /dev/null +++ b/tests/library/clock.spec.ts @@ -0,0 +1,3086 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from '@playwright/test'; +import { createClock as rawCreateClock, install as rawInstall } from '../../packages/playwright-core/src/server/injected/clock'; +import type { InstallConfig, ClockController, ClockMethods } from '../../packages/playwright-core/src/server/injected/clock'; + +const createClock = (now?: Date | number, loopLimit?: number): ClockController & ClockMethods => { + const { clock, api } = rawCreateClock(globalThis, { now, loopLimit }); + for (const key of Object.keys(api)) + clock[key] = api[key]; + return clock as ClockController & ClockMethods; +}; + +type ClockFixtures = { + clock: ClockController & ClockMethods; + now: Date | number | undefined; + loopLimit: number | undefined; + install: (config?: InstallConfig) => ClockController & ClockMethods; + installEx: (config?: InstallConfig) => { clock: ClockController, api: ClockMethods, originals: ClockMethods }; +}; + +const it = test.extend({ + clock: async ({ now, loopLimit }, use) => { + const clock = createClock(now, loopLimit); + await use(clock); + }, + + now: undefined, + + loopLimit: undefined, + + install: async ({}, use) => { + let clockObject: ClockController & ClockMethods; + const install = (config?: InstallConfig) => { + const { clock, api } = rawInstall(globalThis, config); + for (const key of Object.keys(api)) + clock[key] = api[key]; + clockObject = clock as ClockController & ClockMethods; + return clockObject; + }; + await use(install); + clockObject?.uninstall(); + }, + + installEx: async ({}, use) => { + let clock: ClockController; + await use((config?: InstallConfig) => { + const result = rawInstall(globalThis, config); + clock = result.clock; + return result; + }); + clock?.uninstall(); + }, +}); + +it.describe('setTimeout', () => { + it('throws if no arguments', async ({ clock }) => { + expect(() => { + // @ts-expect-error + clock.setTimeout(); + }).toThrow(); + }); + + it('returns numeric id or object with numeric id', async ({ clock }) => { + const result = clock.setTimeout(() => { }, 10); + expect(result).toEqual(expect.any(Number)); + }); + + it('returns unique id', async ({ clock }) => { + const id1 = clock.setTimeout(() => { }, 10); + const id2 = clock.setTimeout(() => { }, 10); + expect(id2).not.toBe(id1); + }); + + it('starts id from a large number', async ({ clock }) => { + const timer = clock.setTimeout(() => { }, 10); + expect(timer).toBeGreaterThanOrEqual(1e12); + }); + + it('sets timers on instance', async ({ clock }) => { + const clock1 = createClock(); + const clock2 = createClock(); + const stubs = [createStub(), createStub()]; + + clock1.setTimeout(stubs[0], 100); + clock2.setTimeout(stubs[1], 100); + clock2.tick(200); + + expect(stubs[0].called).toBeFalsy(); + expect(stubs[1].called).toBeTruthy(); + }); + + it('parses numeric string times', async ({ clock }) => { + let evalCalled = false; + clock.setTimeout(() => { + evalCalled = true; + // @ts-expect-error + }, '10'); + clock.tick(10); + expect(evalCalled).toBeTruthy(); + }); + + it('parses no-numeric string times', async ({ clock }) => { + let evalCalled = false; + clock.setTimeout(() => { + evalCalled = true; + // @ts-expect-error + }, 'string'); + clock.tick(10); + + expect(evalCalled).toBeTruthy(); + }); + + it('passes setTimeout parameters', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 2, 'the first', 'the second'); + clock.tick(3); + expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); + }); + + it('calls correct timeout on recursive tick', async ({ clock }) => { + const stub = createStub(); + const recurseCallback = () => { + clock.tick(100); + }; + + clock.setTimeout(recurseCallback, 50); + clock.setTimeout(stub, 100); + + clock.tick(50); + expect(stub.called).toBeTruthy(); + }); + + it('does not depend on this', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + clock.tick(100); + expect(stub.called).toBeTruthy(); + }); + + it('is not influenced by forward system clock changes', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 5000); + clock.tick(1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.tick(3990); + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by backward system clock changes', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 5000); + clock.tick(1000); + clock.setSystemTime(new clock.Date().getTime() - 1000); + clock.tick(3990); + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('handles Infinity and negative Infinity correctly', async ({ clock }) => { + const calls = []; + clock.setTimeout(() => { + calls.push('NaN'); + }, NaN); + clock.setTimeout(() => { + calls.push('Infinity'); + }, Number.POSITIVE_INFINITY); + clock.setTimeout(() => { + calls.push('-Infinity'); + }, Number.NEGATIVE_INFINITY); + clock.runAll(); + expect(calls).toEqual(['NaN', 'Infinity', '-Infinity']); + }); + + it.describe('use of eval when not in node', () => { + it.beforeEach(() => { + globalThis.evalCalled = false; + }); + + it.afterEach(() => { + delete globalThis.evalCalled.evalCalled; + }); + + it('evals non-function callbacks', async ({ clock }) => { + clock.setTimeout('globalThis.evalCalled = true', 10); + clock.tick(10); + + expect(globalThis.evalCalled).toBeTruthy(); + }); + + it('only evals on global scope', async ({ clock }) => { + const x = 15; + try { + clock.setTimeout('x', x); + clock.tick(x); + expect(true).toBeFalsy(); + } catch (e) { + expect(e).toBeInstanceOf(ReferenceError); + } + }); + }); +}); + +it.describe('tick', () => { + it('triggers immediately without specified delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub); + clock.tick(0); + expect(stub.called).toBeTruthy(); + }); + + it('does not trigger without sufficient delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + clock.tick(10); + expect(stub.called).toBeFalsy(); + }); + + it('triggers after sufficient delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + clock.tick(100); + expect(stub.called).toBeTruthy(); + }); + + it('triggers simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.tick(100); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('triggers multiple simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.setTimeout(spies[2], 99); + clock.setTimeout(spies[3], 100); + clock.tick(100); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + expect(spies[3].called).toBeTruthy(); + }); + + it('triggers multiple simultaneous timers with zero callAt', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 0); + }), + createStub(), + createStub(), + ]; + + // First spy calls another setTimeout with delay=0 + clock.setTimeout(spies[0], 0); + clock.setTimeout(spies[2], 10); + clock.tick(10); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('waits after setTimeout was called', async ({ clock }) => { + clock.tick(100); + const stub = createStub(); + clock.setTimeout(stub, 150); + clock.tick(50); + expect(stub.called).toBeFalsy(); + clock.tick(100); + expect(stub.called).toBeTruthy(); + }); + + it('mini integration test', async ({ clock }) => { + const stubs = [createStub(), createStub(), createStub()]; + clock.setTimeout(stubs[0], 100); + clock.setTimeout(stubs[1], 120); + clock.tick(10); + clock.tick(89); + expect(stubs[0].called).toBeFalsy(); + expect(stubs[1].called).toBeFalsy(); + clock.setTimeout(stubs[2], 20); + clock.tick(1); + expect(stubs[0].called).toBeTruthy(); + expect(stubs[1].called).toBeFalsy(); + expect(stubs[2].called).toBeFalsy(); + clock.tick(19); + expect(stubs[1].called).toBeFalsy(); + expect(stubs[2].called).toBeTruthy(); + clock.tick(1); + expect(stubs[1].called).toBeTruthy(); + }); + + it('triggers even when some throw', async ({ clock }) => { + const stubs = [createStub().throws(), createStub()]; + + clock.setTimeout(stubs[0], 100); + clock.setTimeout(stubs[1], 120); + + expect(() => { + clock.tick(120); + }).toThrow(); + + expect(stubs[0].called).toBeTruthy(); + expect(stubs[1].called).toBeTruthy(); + }); + + it('calls function with global object or null (strict mode) as this', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + + expect(() => { + clock.tick(100); + }).toThrow(); + + expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); + }); + + it('triggers in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 13); + clock.setTimeout(spies[1], 11); + + clock.tick(15); + + expect(spies[1].calledBefore(spies[0])).toBeTruthy(); + }); + + it('creates updated Date while ticking', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + spy(new clock.Date().getTime()); + }, 10); + + clock.tick(100); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('fires timer in intervals of 13', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 13); + clock.tick(500); + expect(spy.callCount).toBe(38); + }); + + it('fires timer in intervals of "13"', async ({ clock }) => { + const spy = createStub(); + // @ts-expect-error + clock.setInterval(spy, '13'); + clock.tick(500); + expect(spy.callCount).toBe(38); + }); + + it('fires timers in correct order', async ({ clock }) => { + const spy13 = createStub(); + const spy10 = createStub(); + + clock.setInterval(() => { + spy13(new clock.Date().getTime()); + }, 13); + + clock.setInterval(() => { + spy10(new clock.Date().getTime()); + }, 10); + + clock.tick(500); + + expect(spy13.callCount).toBe(38); + expect(spy10.callCount).toBe(50); + + expect(spy13.calledWith(416)).toBeTruthy(); + expect(spy10.calledWith(320)).toBeTruthy(); + }); + + it('triggers timeouts and intervals in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setInterval(spies[0], 10); + clock.setTimeout(spies[1], 50); + + clock.tick(100); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + expect(spies[0].callCount).toBe(10); + expect(spies[1].callCount).toBe(1); + }); + + it('does not fire canceled intervals', async ({ clock }) => { + // eslint-disable-next-line prefer-const + let id; + const callback = createStub(() => { + if (callback.callCount === 3) + clock.clearInterval(id); + }); + + id = clock.setInterval(callback, 10); + clock.tick(100); + + expect(callback.callCount).toBe(3); + }); + + it('passes 8 seconds', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 4000); + + clock.tick('08'); + + expect(spy.callCount).toBe(2); + }); + + it('passes 1 minute', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 6000); + + clock.tick('01:00'); + + expect(spy.callCount).toBe(10); + }); + + it('passes 2 hours, 34 minutes and 10 seconds', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + clock.tick('02:34:10'); + + expect(spy.callCount).toBe(925); + }); + + it('throws for invalid format', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + expect(() => { + clock.tick('12:02:34:10'); + }).toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('throws for invalid minutes', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + expect(() => { + clock.tick('67:10'); + }).toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('throws for negative minutes', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + expect(() => { + clock.tick('-7:10'); + }).toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('treats missing argument as 0', async ({ clock }) => { + // @ts-expect-error + clock.tick(); + + expect(clock.now()).toBe(0); + }); + + it('fires nested setTimeout calls properly', async ({ clock }) => { + let i = 0; + const callback = () => { + ++i; + clock.setTimeout(() => { + callback(); + }, 100); + }; + + callback(); + + clock.tick(1000); + + expect(i).toBe(11); + }); + + it('does not silently catch errors', async ({ clock }) => { + const callback = () => { + throw new Error('oh no!'); + }; + + clock.setTimeout(callback, 1000); + + expect(() => { + clock.tick(1000); + }).toThrow(); + }); + + it('returns the current now value', async ({ clock }) => { + const value = clock.tick(200); + expect(clock.now()).toBe(value); + }); + + it('is not influenced by forward system clock changes', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + clock.tick(1990); + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes 2', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() - 1000); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + clock.tick(1990); + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + throw new Error(); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + + expect(() => { + clock.tick(1990); + }).toThrow(); + + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes when an error is thrown 2', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() - 1000); + throw new Error(); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + + expect(() => { + clock.tick(1990); + }).toThrow(); + + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('throws on negative ticks', async ({ clock }) => { + expect(() => { + clock.tick(-500); + }).toThrow('Negative ticks are not supported'); + }); +}); + +it.describe('tickAsync', () => { + it('triggers immediately without specified delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub); + + await clock.tickAsync(0); + + expect(stub.called).toBeTruthy(); + }); + + it('does not trigger without sufficient delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + + await clock.tickAsync(10); + + expect(stub.called).toBeFalsy(); + }); + + it('triggers after sufficient delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + + await clock.tickAsync(100); + + expect(stub.called).toBeTruthy(); + }); + + it('triggers simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + + await clock.tickAsync(100); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('triggers multiple simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.setTimeout(spies[2], 99); + clock.setTimeout(spies[3], 100); + + await clock.tickAsync(100); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + expect(spies[3].called).toBeTruthy(); + }); + + it('triggers multiple simultaneous timers with zero callAt', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 0); + }), + createStub(), + createStub(), + ]; + + // First spy calls another setTimeout with delay=0 + clock.setTimeout(spies[0], 0); + clock.setTimeout(spies[2], 10); + + await clock.tickAsync(10); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('waits after setTimeout was called', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 150); + + await clock.tickAsync(50); + + expect(stub.called).toBeFalsy(); + + await clock.tickAsync(100); + + expect(stub.called).toBeTruthy(); + }); + + it('mini integration test', async ({ clock }) => { + const stubs = [createStub(), createStub(), createStub()]; + clock.setTimeout(stubs[0], 100); + clock.setTimeout(stubs[1], 120); + + await clock.tickAsync(10); + await clock.tickAsync(89); + + expect(stubs[0].called).toBeFalsy(); + expect(stubs[1].called).toBeFalsy(); + + clock.setTimeout(stubs[2], 20); + await clock.tickAsync(1); + + expect(stubs[0].called).toBeTruthy(); + expect(stubs[1].called).toBeFalsy(); + expect(stubs[2].called).toBeFalsy(); + + await clock.tickAsync(19); + + expect(stubs[1].called).toBeFalsy(); + expect(stubs[2].called).toBeTruthy(); + + await clock.tickAsync(1); + + expect(stubs[1].called).toBeTruthy(); + }); + + it('triggers even when some throw', async ({ clock }) => { + const stubs = [createStub().throws(), createStub()]; + + clock.setTimeout(stubs[0], 100); + clock.setTimeout(stubs[1], 120); + + await expect(clock.tickAsync(120)).rejects.toThrow(); + + expect(stubs[0].called).toBeTruthy(); + expect(stubs[1].called).toBeTruthy(); + }); + + it('calls function with global object or null (strict mode) as this', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + + await expect(clock.tickAsync(100)).rejects.toThrow(); + + expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); + }); + + it('triggers in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 13); + clock.setTimeout(spies[1], 11); + + await clock.tickAsync(15); + + expect(spies[1].calledBefore(spies[0])).toBeTruthy(); + }); + + it('creates updated Date while ticking', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + spy(new clock.Date().getTime()); + }, 10); + + await clock.tickAsync(100); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('creates updated Date while ticking promises', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + void Promise.resolve().then(() => { + spy(new clock.Date().getTime()); + }); + }, 10); + + await clock.tickAsync(100); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('fires timer in intervals of 13', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 13); + + await clock.tickAsync(500); + + expect(spy.callCount).toBe(38); + }); + + it('fires timers in correct order', async ({ clock }) => { + const spy13 = createStub(); + const spy10 = createStub(); + + clock.setInterval(() => { + spy13(new clock.Date().getTime()); + }, 13); + + clock.setInterval(() => { + spy10(new clock.Date().getTime()); + }, 10); + + await clock.tickAsync(500); + + expect(spy13.callCount).toBe(38); + expect(spy10.callCount).toBe(50); + + expect(spy13.calledWith(416)).toBeTruthy(); + expect(spy10.calledWith(320)).toBeTruthy(); + }); + + it('fires promise timers in correct order', async ({ clock }) => { + const spy13 = createStub(); + const spy10 = createStub(); + + clock.setInterval(() => { + void Promise.resolve().then(() => { + spy13(new clock.Date().getTime()); + }); + }, 13); + + clock.setInterval(() => { + void Promise.resolve().then(() => { + spy10(new clock.Date().getTime()); + }); + }, 10); + + await clock.tickAsync(500); + + expect(spy13.callCount).toBe(38); + expect(spy10.callCount).toBe(50); + + expect(spy13.calledWith(416)).toBeTruthy(); + expect(spy10.calledWith(320)).toBeTruthy(); + }); + + it('triggers timeouts and intervals in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setInterval(spies[0], 10); + clock.setTimeout(spies[1], 50); + + await clock.tickAsync(100); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + expect(spies[0].callCount).toBe(10); + expect(spies[1].callCount).toBe(1); + }); + + it('does not fire canceled intervals', async ({ clock }) => { + // eslint-disable-next-line prefer-const + let id; + const callback = createStub(() => { + if (callback.callCount === 3) + clock.clearInterval(id); + }); + + id = clock.setInterval(callback, 10); + await clock.tickAsync(100); + + expect(callback.callCount).toBe(3); + }); + + it('does not fire intervals canceled in a promise', async ({ clock }) => { + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let id; + const callback = createStub(() => { + if (callback.callCount === 3) { + void Promise.resolve().then(() => { + clock.clearInterval(id); + }); + } + }); + + id = clock.setInterval(callback, 10); + await clock.tickAsync(100); + + expect(callback.callCount).toBe(3); + }); + + it('passes 8 seconds', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 4000); + + await clock.tickAsync('08'); + + expect(spy.callCount).toBe(2); + }); + + it('passes 1 minute', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 6000); + + await clock.tickAsync('01:00'); + + expect(spy.callCount).toBe(10); + }); + + it('passes 2 hours, 34 minutes and 10 seconds', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + await clock.tickAsync('02:34:10'); + + expect(spy.callCount).toBe(925); + }); + + it('throws for invalid format', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + await expect(clock.tickAsync('12:02:34:10')).rejects.toThrow(); + expect(spy.callCount).toBe(0); + }); + + it('throws for invalid minutes', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + await expect(clock.tickAsync('67:10')).rejects.toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('throws for negative minutes', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + await expect(clock.tickAsync('-7:10')).rejects.toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('treats missing argument as 0', async ({ clock }) => { + // @ts-expect-error + await clock.tickAsync(); + + expect(clock.now()).toBe(0); + }); + + it('fires nested setTimeout calls properly', async ({ clock }) => { + let i = 0; + const callback = () => { + ++i; + clock.setTimeout(() => { + callback(); + }, 100); + }; + + callback(); + + await clock.tickAsync(1000); + + expect(i).toBe(11); + }); + + it('fires nested setTimeout calls in user-created promises properly', async ({ clock }) => { + let i = 0; + const callback = () => { + void Promise.resolve().then(() => { + ++i; + clock.setTimeout(() => { + void Promise.resolve().then(() => { + callback(); + }); + }, 100); + }); + }; + + callback(); + + await clock.tickAsync(1000); + + expect(i).toBe(11); + }); + + it('does not silently catch errors', async ({ clock }) => { + const callback = () => { + throw new Error('oh no!'); + }; + + clock.setTimeout(callback, 1000); + + await expect(clock.tickAsync(1000)).rejects.toThrow(); + }); + + it('returns the current now value', async ({ clock }) => { + const value = await clock.tickAsync(200); + expect(clock.now()).toBe(value); + }); + + it('is not influenced by forward system clock changes', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + await clock.tickAsync(1990); + expect(stub.callCount).toBe(0); + await clock.tickAsync(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes in promises', async ({ clock }) => { + const callback = () => { + void Promise.resolve().then(() => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + }); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + await clock.tickAsync(1990); + expect(stub.callCount).toBe(0); + await clock.tickAsync(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + throw new Error(); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + + await expect(clock.tickAsync(1990)).rejects.toThrow(); + + expect(stub.callCount).toBe(0); + await clock.tickAsync(20); + expect(stub.callCount).toBe(1); + }); + + it('should settle user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(spy); + }, 100); + + await clock.tickAsync(100); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle chained user-created promises', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub()]; + + clock.setTimeout(() => { + void Promise.resolve() + .then(spies[0]) + .then(spies[1]) + .then(spies[2]); + }, 100); + + await clock.tickAsync(100); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('should settle multiple user-created promises', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub()]; + + clock.setTimeout(() => { + void Promise.resolve().then(spies[0]); + void Promise.resolve().then(spies[1]); + void Promise.resolve().then(spies[2]); + }, 100); + + await clock.tickAsync(100); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('should settle nested user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spy); + }); + }); + }, 100); + + await clock.tickAsync(100); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle user-created promises even if some throw', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + + clock.setTimeout(() => { + void Promise.reject().then(spies[0]).catch(spies[1]); + void Promise.resolve().then(spies[2]).catch(spies[3]); + }, 100); + + await clock.tickAsync(100); + + expect(spies[0].callCount).toBe(0); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + expect(spies[3].callCount).toBe(0); + }); + + it('should settle user-created promises before calling more timeouts', async ({ clock }) => { + const spies = [createStub(), createStub()]; + + clock.setTimeout(() => { + void Promise.resolve().then(spies[0]); + }, 100); + + clock.setTimeout(spies[1], 200); + + await clock.tickAsync(200); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); + + it('should settle local promises before calling timeouts', async ({ clock }) => { + const spies = [createStub(), createStub()]; + + void Promise.resolve().then(spies[0]); + + clock.setTimeout(spies[1], 100); + + await clock.tickAsync(100); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); + + it('should settle local nested promises before calling timeouts', async ({ clock }) => { + const spies = [createStub(), createStub()]; + + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spies[0]); + }); + }); + + clock.setTimeout(spies[1], 100); + + await clock.tickAsync(100); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); +}); + +it.describe('next', () => { + it('triggers the next timer', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + + clock.next(); + + expect(stub.called).toBeTruthy(); + }); + + it('does not trigger simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + + clock.next(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + }); + + it('subsequent calls trigger simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.setTimeout(spies[2], 99); + clock.setTimeout(spies[3], 100); + + clock.next(); + + expect(spies[2].called).toBeTruthy(); + expect(spies[0].called).toBeFalsy(); + expect(spies[1].called).toBeFalsy(); + expect(spies[3].called).toBeFalsy(); + + clock.next(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + expect(spies[3].called).toBeFalsy(); + + clock.next(); + + expect(spies[1].called).toBeTruthy(); + expect(spies[3].called).toBeFalsy(); + + clock.next(); + + expect(spies[3].called).toBeTruthy(); + }); + + it('subsequent calls trigger simultaneous timers with zero callAt', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 0); + }), + createStub(), + createStub(), + ]; + + // First spy calls another setTimeout with delay=0 + clock.setTimeout(spies[0], 0); + clock.setTimeout(spies[2], 10); + + clock.next(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + + clock.next(); + + expect(spies[1].called).toBeTruthy(); + + clock.next(); + + expect(spies[2].called).toBeTruthy(); + }); + + it('throws exception thrown by timer', async ({ clock }) => { + const stub = createStub().throws(); + + clock.setTimeout(stub, 100); + + expect(() => { + clock.next(); + }).toThrow(); + + expect(stub.called).toBeTruthy(); + }); + + it('calls function with global object or null (strict mode) as this', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + + expect(() => { + clock.next(); + }).toThrow(); + + expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); + }); + + it('subsequent calls trigger in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 13); + clock.setTimeout(spies[1], 11); + + clock.next(); + clock.next(); + + expect(spies[1].calledBefore(spies[0])).toBeTruthy(); + }); + + it('creates updated Date while ticking', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + spy(new clock.Date().getTime()); + }, 10); + + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('subsequent calls trigger timeouts and intervals in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setInterval(spies[0], 10); + clock.setTimeout(spies[1], 50); + + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + expect(spies[0].callCount).toBe(5); + expect(spies[1].callCount).toBe(1); + }); + + it('subsequent calls do not fire canceled intervals', async ({ clock }) => { + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let id; + const callback = createStub(() => { + if (callback.callCount === 3) + clock.clearInterval(id); + }); + + id = clock.setInterval(callback, 10); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + + expect(callback.callCount).toBe(3); + }); + + it('advances the clock based on when the timer was supposed to be called', async ({ clock }) => { + clock.setTimeout(createStub(), 55); + clock.next(); + + expect(clock.now()).toBe(55); + }); + + it('returns the current now value', async ({ clock }) => { + clock.setTimeout(createStub(), 55); + const value = clock.next(); + + expect(clock.now()).toBe(value); + }); +}); + +it.describe('nextAsync', () => { + it('triggers the next timer', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + + await clock.nextAsync(); + + expect(stub.called).toBeTruthy(); + }); + + it('does not trigger simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + + await clock.nextAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + }); + + it('subsequent calls trigger simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.setTimeout(spies[2], 99); + clock.setTimeout(spies[3], 100); + + await clock.nextAsync(); + + expect(spies[2].called).toBeTruthy(); + expect(spies[0].called).toBeFalsy(); + expect(spies[1].called).toBeFalsy(); + expect(spies[3].called).toBeFalsy(); + + await clock.nextAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + expect(spies[3].called).toBeFalsy(); + + await clock.nextAsync(); + + expect(spies[1].called).toBeTruthy(); + expect(spies[3].called).toBeFalsy(); + + await clock.nextAsync(); + + expect(spies[3].called).toBeTruthy(); + }); + + it('subsequent calls trigger simultaneous timers with zero callAt', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 0); + }), + createStub(), + createStub(), + ]; + + // First spy calls another setTimeout with delay=0 + clock.setTimeout(spies[0], 0); + clock.setTimeout(spies[2], 10); + + await clock.nextAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + + await clock.nextAsync(); + + expect(spies[1].called).toBeTruthy(); + + await clock.nextAsync(); + + expect(spies[2].called).toBeTruthy(); + }); + + it('throws exception thrown by timer', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + await expect(clock.nextAsync()).rejects.toThrow(); + expect(stub.called).toBeTruthy(); + }); + + it('calls function with global object or null (strict mode) as this', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + await expect(clock.nextAsync()).rejects.toThrow(); + expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); + }); + + it('subsequent calls trigger in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 13); + clock.setTimeout(spies[1], 11); + + await clock.nextAsync(); + await clock.nextAsync(); + + expect(spies[1].calledBefore(spies[0])).toBeTruthy(); + }); + + it('creates updated Date while ticking', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + spy(new clock.Date().getTime()); + }, 10); + + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('subsequent calls trigger timeouts and intervals in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setInterval(spies[0], 10); + clock.setTimeout(spies[1], 50); + + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + expect(spies[0].callCount).toBe(5); + expect(spies[1].callCount).toBe(1); + }); + + it('does not fire canceled intervals', async ({ clock }) => { + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let id; + const callback = createStub(() => { + if (callback.callCount === 3) + clock.clearInterval(id); + }); + + id = clock.setInterval(callback, 10); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + + expect(callback.callCount).toBe(3); + }); + + it('does not fire intervals canceled in promises', async ({ clock }) => { + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let id; + const callback = createStub(() => { + if (callback.callCount === 3) { + void Promise.resolve().then(() => { + clock.clearInterval(id); + }); + } + }); + + id = clock.setInterval(callback, 10); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + + expect(callback.callCount).toBe(3); + }); + + it('advances the clock based on when the timer was supposed to be called', async ({ clock }) => { + clock.setTimeout(createStub(), 55); + await clock.nextAsync(); + + expect(clock.now()).toBe(55); + }); + + it('returns the current now value', async ({ clock }) => { + clock.setTimeout(createStub(), 55); + const value = await clock.nextAsync(); + + expect(clock.now()).toBe(value); + }); + + it('should settle user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(spy); + }, 55); + + await clock.nextAsync(); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle nested user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spy); + }); + }); + }, 55); + + await clock.nextAsync(); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle local promises before firing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + + void Promise.resolve().then(spies[0]); + + clock.setTimeout(spies[1], 55); + + await clock.nextAsync(); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); +}); + +it.describe('runAll', () => { + it('if there are no timers just return', async ({ clock }) => { + clock.runAll(); + }); + + it('runs all timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + + clock.runAll(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added while running are also run', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 50); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + + clock.runAll(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('throws before allowing infinite recursion', async ({ clock }) => { + const recursiveCallback = () => { + clock.setTimeout(recursiveCallback, 10); + }; + recursiveCallback(); + expect(() => clock.runAll()).toThrow(); + }); + + it('the loop limit can be set when creating a clock', async ({}) => { + const clock = createClock(0, 1); + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + expect(() => clock.runAll()).toThrow(); + }); + + it('the loop limit can be set when installing a clock', async ({ install }) => { + const clock = install({ loopLimit: 1 }); + const spies = [createStub(), createStub()]; + setTimeout(spies[0], 10); + setTimeout(spies[1], 50); + + expect(() => clock.runAll()).toThrow(); + }); +}); + +it.describe('runAllAsync', () => { + it('if there are no timers just return', async ({ clock }) => { + await clock.runAllAsync(); + }); + + it('runs all timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + + await clock.runAllAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added while running are also run', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 50); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + + await clock.runAllAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added in promises while running are also run', async ({ clock }) => { + const spies = [ + createStub(() => { + void Promise.resolve().then(() => { + clock.setTimeout(spies[1], 50); + }); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + await clock.runAllAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('throws before allowing infinite recursion', async ({ clock }) => { + const recursiveCallback = () => { + clock.setTimeout(recursiveCallback, 10); + }; + recursiveCallback(); + await expect(clock.runAllAsync()).rejects.toThrow(); + }); + + it('throws before allowing infinite recursion from promises', async ({ clock }) => { + const recursiveCallback = () => { + void Promise.resolve().then(() => { + clock.setTimeout(recursiveCallback, 10); + }); + }; + recursiveCallback(); + await expect(clock.runAllAsync()).rejects.toThrow(); + }); + + it('the loop limit can be set when creating a clock', async ({}) => { + const clock = createClock(0, 1); + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + await expect(clock.runAllAsync()).rejects.toThrow(); + }); + + it('the loop limit can be set when installing a clock', async ({ install }) => { + const clock = install({ loopLimit: 1 }); + const spies = [createStub(), createStub()]; + setTimeout(spies[0], 10); + setTimeout(spies[1], 50); + await expect(clock.runAllAsync()).rejects.toThrow(); + }); + + it('should settle user-created promises', async ({ clock }) => { + const spy = createStub(); + clock.setTimeout(() => { + void Promise.resolve().then(spy); + }, 55); + await clock.runAllAsync(); + expect(spy.called).toBeTruthy(); + }); + + it('should settle nested user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spy); + }); + }); + }, 55); + + await clock.runAllAsync(); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle local promises before firing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + void Promise.resolve().then(spies[0]); + clock.setTimeout(spies[1], 55); + await clock.runAllAsync(); + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); + + it('should settle user-created promises before firing more timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(() => { + void Promise.resolve().then(spies[0]); + }, 55); + clock.setTimeout(spies[1], 75); + await clock.runAllAsync(); + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); +}); + +it.describe('runToLast', () => { + it('returns current time when there are no timers', async ({ clock }) => { + const time = clock.runToLast(); + expect(time).toBe(0); + }); + + it('runs all existing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + clock.runToLast(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('returns time of the last timer', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + const time = clock.runToLast(); + expect(time).toBe(50); + }); + + it('runs all existing timers when two timers are matched for being last', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 10); + clock.runToLast(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added with a call time later than the last existing timer are NOT run', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 50); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + clock.runToLast(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + }); + + it('new timers added with a call time earlier than the last existing timer are run', async ({ clock }) => { + const spies = [ + createStub(), + createStub(() => { + clock.setTimeout(spies[2], 50); + }), + createStub(), + ]; + + clock.setTimeout(spies[0], 100); + // Spy calls another setTimeout + clock.setTimeout(spies[1], 10); + clock.runToLast(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('new timers cannot cause an infinite loop', async ({ clock }) => { + const spy = createStub(); + const recursiveCallback = () => { + clock.setTimeout(recursiveCallback, 0); + }; + + clock.setTimeout(recursiveCallback, 0); + clock.setTimeout(spy, 100); + clock.runToLast(); + expect(spy.called).toBeTruthy(); + }); + + it('should support clocks with start time', async ({ clock }) => { + let invocations = 0; + + clock.setTimeout(function cb() { + invocations++; + clock.setTimeout(cb, 50); + }, 50); + + clock.runToLast(); + + expect(invocations).toBe(1); + }); +}); + +it.describe('runToLastAsync', () => { + it('returns current time when there are no timers', async ({ clock }) => { + const time = await clock.runToLastAsync(); + expect(time).toBe(0); + }); + + it('runs all existing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + await clock.runToLastAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('returns time of the last timer', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + const time = await clock.runToLastAsync(); + expect(time).toBe(50); + }); + + it('runs all existing timers when two timers are matched for being last', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 10); + await clock.runToLastAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added with a call time later than the last existing timer are NOT run', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 50); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + await clock.runToLastAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + }); + + it('new timers added with a call time earlier than the last existing timer are run', async ({ clock }) => { + const spies = [ + createStub(), + createStub(() => { + clock.setTimeout(spies[2], 50); + }), + createStub(), + ]; + + clock.setTimeout(spies[0], 100); + // Spy calls another setTimeout + clock.setTimeout(spies[1], 10); + await clock.runToLastAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('new timers cannot cause an infinite loop', async ({ clock }) => { + const spy = createStub(); + const recursiveCallback = () => { + clock.setTimeout(recursiveCallback, 0); + }; + + clock.setTimeout(recursiveCallback, 0); + clock.setTimeout(spy, 100); + await clock.runToLastAsync(); + expect(spy.called).toBeTruthy(); + }); + + it('should settle user-created promises', async ({ clock }) => { + const spy = createStub(); + clock.setTimeout(() => { + void Promise.resolve().then(spy); + }, 55); + await clock.runToLastAsync(); + expect(spy.called).toBeTruthy(); + }); + + it('should settle nested user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spy); + }); + }); + }, 55); + + await clock.runToLastAsync(); + expect(spy.called).toBeTruthy(); + }); + + it('should settle local promises before firing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + void Promise.resolve().then(spies[0]); + clock.setTimeout(spies[1], 55); + await clock.runToLastAsync(); + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); + + it('should settle user-created promises before firing more timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(() => { + void Promise.resolve().then(spies[0]); + }, 55); + clock.setTimeout(spies[1], 75); + await clock.runToLastAsync(); + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); +}); + +it.describe('clearTimeout', () => { + it('removes timeout', async ({ clock }) => { + const stub = createStub(); + const id = clock.setTimeout(stub, 50); + clock.clearTimeout(id); + await clock.tickAsync(50); + expect(stub.called).toBeFalsy(); + }); + + it('removes interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub, 50); + clock.clearTimeout(id); + await clock.tickAsync(50); + expect(stub.called).toBeFalsy(); + }); + + it('removes interval with undefined interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub); + clock.clearTimeout(id); + await clock.tickAsync(50); + expect(stub.called).toBeFalsy(); + }); + + it('ignores null argument', async ({ clock }) => { + clock.clearTimeout(null); + }); +}); + +it.describe('reset', () => { + it('resets to the time install with - issue #183', async ({ clock }) => { + clock.tick(100); + clock.reset(); + expect(clock.now()).toBe(0); + }); + + it('resets hrTime - issue #206', async ({ clock }) => { + clock.tick(100); + expect(clock.performance.now()).toEqual(100); + clock.reset(); + expect(clock.performance.now()).toEqual(0); + }); +}); + +it.describe('setInterval', () => { + it('throws if no arguments', async ({ clock }) => { + expect(() => { + // @ts-expect-error + clock.setInterval(); + }).toThrow(); + }); + + it('returns numeric id or object with numeric id', async ({ clock }) => { + const result = clock.setInterval(() => {}, 10); + expect(result).toBeGreaterThan(0); + }); + + it('returns unique id', async ({ clock }) => { + const id1 = clock.setInterval(() => {}, 10); + const id2 = clock.setInterval(() => {}, 10); + + expect(id2).not.toEqual(id1); + }); + + it('schedules recurring timeout', async ({ clock }) => { + const stub = createStub(); + clock.setInterval(stub, 10); + clock.tick(99); + + expect(stub.callCount).toBe(9); + }); + + it('is not influenced by forward system clock changes', async ({ clock }) => { + const stub = createStub(); + clock.setInterval(stub, 10); + clock.tick(11); + expect(stub.callCount).toBe(1); + clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.tick(8); + expect(stub.callCount).toBe(1); + clock.tick(3); + expect(stub.callCount).toBe(2); + }); + + it('is not influenced by backward system clock changes', async ({ clock }) => { + const stub = createStub(); + clock.setInterval(stub, 10); + clock.tick(5); + clock.setSystemTime(new clock.Date().getTime() - 1000); + clock.tick(6); + expect(stub.callCount).toBe(1); + clock.tick(10); + expect(stub.callCount).toBe(2); + }); + + it('does not schedule recurring timeout when cleared', async ({ clock }) => { + const stub = createStub(() => { + if (stub.callCount === 3) + clock.clearInterval(id); + }); + + const id = clock.setInterval(stub, 10); + clock.tick(100); + + expect(stub.callCount).toBe(3); + }); + + it('passes setTimeout parameters', async ({ clock }) => { + const stub = createStub(); + clock.setInterval(stub, 2, 'the first', 'the second'); + clock.tick(3); + expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); + }); +}); + +it.describe('clearInterval', () => { + it('removes interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub, 50); + clock.clearInterval(id); + clock.tick(50); + expect(stub.called).toBeFalsy(); + }); + + it('removes interval with undefined interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub); + clock.clearInterval(id); + clock.tick(50); + expect(stub.called).toBeFalsy(); + }); + + it('removes timeout', async ({ clock }) => { + const stub = createStub(); + const id = clock.setTimeout(stub, 50); + clock.clearInterval(id); + clock.tick(50); + expect(stub.called).toBeFalsy(); + }); + + it('ignores null argument', async ({ clock }) => { + clock.clearInterval(null); + }); +}); + +it.describe('date', () => { + it('provides date constructor', async ({ clock }) => { + expect(clock.Date).toEqual(expect.any(Function)); + }); + + it('creates real Date objects', async ({ clock }) => { + const date = new clock.Date(); + expect(Date.prototype.isPrototypeOf(date)).toBeTruthy(); + }); + + it('returns date as string when called as function', async ({ clock }) => { + const date = clock.Date(); + expect(typeof date).toBe('string'); + }); + + it('creates Date objects representing clock time', async ({ clock }) => { + const date = new clock.Date(); + expect(date.getTime()).toBe(new Date(clock.now()).getTime()); + }); + + it('returns date as string representing clock time', async ({ clock }) => { + const date = clock.Date(); + expect(date).toBe(new Date(clock.now()).toString()); + }); + + it('listens to ticking clock', async ({ clock }) => { + const date1 = new clock.Date(); + clock.tick(3); + const date2 = new clock.Date(); + expect(date2.getTime() - date1.getTime()).toBe(3); + }); + + it('listens to system clock changes', async ({ clock }) => { + const date1 = new clock.Date(); + clock.setSystemTime(date1.getTime() + 1000); + const date2 = new clock.Date(); + expect(date2.getTime() - date1.getTime()).toBe(1000); + }); + + it('creates regular date when passing timestamp', async ({ clock }) => { + const date = new Date(); + const fakeDate = new clock.Date(date.getTime()); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing a date as string', async ({ clock }) => { + const date = new Date(); + const fakeDate = new clock.Date(date.toISOString()); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing a date as RFC 2822 string', async ({ clock }) => { + const date = new Date('Sat Apr 12 2014 12:22:00 GMT+1000'); + const fakeDate = new clock.Date('Sat Apr 12 2014 12:22:00 GMT+1000'); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing year, month', async ({ clock }) => { + const date = new Date(2010, 4); + const fakeDate = new clock.Date(2010, 4); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d', async ({ clock }) => { + const date = new Date(2010, 4, 2); + const fakeDate = new clock.Date(2010, 4, 2); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d, h', async ({ clock }) => { + const date = new Date(2010, 4, 2, 12); + const fakeDate = new clock.Date(2010, 4, 2, 12); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d, h, m', async ({ clock }) => { + const date = new Date(2010, 4, 2, 12, 42); + const fakeDate = new clock.Date(2010, 4, 2, 12, 42); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d, h, m, s', async ({ clock }) => { + const date = new Date(2010, 4, 2, 12, 42, 53); + const fakeDate = new clock.Date(2010, 4, 2, 12, 42, 53); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d, h, m, s, ms', async ({ clock }) => { + const date = new Date(2010, 4, 2, 12, 42, 53, 498); + const fakeDate = new clock.Date(2010, 4, 2, 12, 42, 53, 498); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('returns date as string when calling with arguments', async ({ clock }) => { + // @ts-expect-error + const fakeDateStr = clock.Date(2010, 4, 2, 12, 42, 53, 498); + expect(fakeDateStr).toBe(new clock.Date().toString()); + }); + + it('returns date as string when calling with timestamp', async ({ clock }) => { + // @ts-expect-error + const fakeDateStr = clock.Date(1); + expect(fakeDateStr).toBe(new clock.Date().toString()); + }); + + it('mirrors native Date.prototype', async ({ clock }) => { + expect(clock.Date.prototype).toEqual(Date.prototype); + }); + + it('supports now method if present', async ({ clock }) => { + expect(typeof clock.Date.now).toEqual(typeof Date.now); + }); + + it('returns clock.now()', async ({ clock }) => { + const clock_now = clock.Date.now(); + const global_now = Date.now(); + expect(clock_now).toBeGreaterThanOrEqual(clock.now()); + expect(clock_now).toBeLessThanOrEqual(global_now); + }); + + it('mirrors parse method', async ({ clock }) => { + expect(clock.Date.parse).toEqual(Date.parse); + }); + + it('mirrors UTC method', async ({ clock }) => { + expect(clock.Date.UTC).toEqual(Date.UTC); + }); + + it('mirrors toUTCString method', async ({ clock }) => { + expect(clock.Date.prototype.toUTCString).toEqual(Date.prototype.toUTCString); + }); +}); + +it.describe('stubTimers', () => { + it('returns clock object', ({ install }) => { + const clock = install(); + expect(clock).toEqual(expect.any(Object)); + expect(clock.tick).toEqual(expect.any(Function)); + }); + + it('takes an object parameter', ({ install }) => { + const clock = install({}); + expect(clock).toEqual(expect.any(Object)); + }); + + it('sets initial timestamp', ({ install }) => { + const clock = install({ now: 1400 }); + expect(clock.now()).toBe(1400); + }); + + it('replaces global setTimeout', ({ install }) => { + const clock = install(); + const stub = createStub(); + + setTimeout(stub, 1000); + clock.tick(1000); + + expect(stub.called).toBeTruthy(); + }); + + it('global fake setTimeout should return id', ({ install }) => { + install(); + const stub = createStub(); + const to = setTimeout(stub, 1000); + expect(to).toEqual(expect.any(Number)); + }); + + it('replaces global clearTimeout', ({ install }) => { + const clock = install(); + const stub = createStub(); + + clearTimeout(setTimeout(stub, 1000)); + clock.tick(1000); + + expect(stub.called).toBeFalsy(); + }); + + it('replaces global setInterval', ({ install }) => { + const clock = install(); + const stub = createStub(); + + setInterval(stub, 500); + clock.tick(1000); + + expect(stub.callCount).toBe(2); + }); + + it('replaces global clearInterval', ({ install }) => { + const clock = install(); + const stub = createStub(); + + clearInterval(setInterval(stub, 500)); + clock.tick(1000); + + expect(stub.called).toBeFalsy(); + }); + + it('replaces global performance.now', ({ install }) => { + const clock = install(); + const prev = performance.now(); + clock.tick(1000); + const next = performance.now(); + expect(next).toBe(1000); + expect(prev).toBe(0); + }); + + it('uninstalls global performance.now', ({ install }) => { + const oldNow = performance.now; + const clock = install(); + expect(performance.now).toBe(clock.performance.now); + clock.uninstall(); + expect(performance.now).toBe(oldNow); + }); + + it('should let performance.mark still be callable after install() (#136)', ({ install }) => { + it.skip(nodeMajorVersion < 20); + install(); + expect(() => { + performance.mark('a name'); + }).not.toThrow(); + }); + + it('should not alter the global performance properties and methods', ({ install }) => { + it.skip(nodeMajorVersion < 20); + (Performance.prototype as any).someFunc1 = () => {}; + (Performance.prototype as any).someFunc2 = () => {}; + (Performance.prototype as any).someFunc3 = () => {}; + + const clock = install(); + expect((performance as any).someFunc1).toEqual(expect.any(Function)); + expect((performance as any).someFunc2).toEqual(expect.any(Function)); + expect((performance as any).someFunc3).toEqual(expect.any(Function)); + clock.uninstall(); + delete (Performance.prototype as any).someFunc1; + delete (Performance.prototype as any).someFunc2; + delete (Performance.prototype as any).someFunc3; + }); + + it('should replace the getEntries, getEntriesByX methods with noops that return []', ({ install }) => { + it.skip(nodeMajorVersion < 20); + const backupDescriptors = Object.getOwnPropertyDescriptors(Performance); + + function noop() { + return ['foo']; + } + + for (const propName of ['getEntries', 'getEntriesByName', 'getEntriesByType']) { + Object.defineProperty(Performance.prototype, propName, { + writable: true, + }); + } + + (Performance.prototype as any).getEntries = noop; + (Performance.prototype as any).getEntriesByName = noop; + (Performance.prototype as any).getEntriesByType = noop; + + const clock = install(); + + expect(performance.getEntries()).toEqual([]); + expect((performance as any).getEntriesByName()).toEqual([]); + expect((performance as any).getEntriesByType()).toEqual([]); + + clock.uninstall(); + + expect(performance.getEntries()).toEqual(['foo']); + expect((performance as any).getEntriesByName()).toEqual(['foo']); + expect((performance as any).getEntriesByType()).toEqual(['foo']); + + Object.keys(backupDescriptors).forEach(key => { + Object.defineProperty(Performance.prototype, key, backupDescriptors[key]); + }); + }); + + it.fixme('deletes global property on uninstall if it was inherited onto the global object', ({}) => { + // Give the global object an inherited 'setTimeout' method + const proto = { Date, + setTimeout: () => {}, + clearTimeout: () => {}, + setInterval: () => {}, + clearInterval: () => {}, + }; + const myGlobal = Object.create(proto); + + const { clock } = rawInstall(myGlobal, { now: 0, toFake: ['setTimeout'] }); + expect(myGlobal.hasOwnProperty('setTimeout')).toBeTruthy(); + clock.uninstall(); + expect(myGlobal.hasOwnProperty('setTimeout')).toBeFalsy(); + }); + + it('fakes Date constructor', ({ installEx }) => { + const { originals } = installEx({ now: 0 }); + const now = new Date(); + + expect(Date).not.toBe(originals.Date); + expect(now.getTime()).toBe(0); + }); + + it(`fake Date constructor should mirror Date's properties`, ({ clock }) => { + expect(Date).not.toBe(clock.Date); + expect(Date.prototype).toEqual(clock.Date.prototype); + }); + + it('decide on Date.now support at call-time when supported', ({ install }) => { + (Date.now as any) = () => {}; + install({ now: 0 }); + expect(Date.now).toEqual(expect.any(Function)); + }); + + it('mirrors custom Date properties', ({ install }) => { + const f = () => { + return ''; + }; + (Date as any).format = f; + install(); + + expect((Date as any).format).toEqual(f); + }); + + it('uninstalls Date constructor', () => { + const { clock, originals } = rawInstall(globalThis, { now: 0 }); + clock.uninstall(); + expect(Date).toBe(originals.Date); + }); + + it('fakes provided methods', ({ installEx }) => { + const { originals } = installEx({ now: 0, toFake: ['setTimeout', 'Date'] }); + expect(setTimeout).not.toBe(originals.setTimeout); + expect(Date).not.toBe(originals.Date); + }); + + it('resets faked methods', ({ install }) => { + const { clock, originals } = rawInstall(globalThis, { + now: 0, + toFake: ['setTimeout', 'Date'], + }); + clock.uninstall(); + + expect(setTimeout).toBe(originals.setTimeout); + expect(Date).toBe(originals.Date); + }); + + it('does not fake methods not provided', ({ installEx }) => { + const { originals } = installEx({ + now: 0, + toFake: ['setTimeout', 'Date'], + }); + + expect(clearTimeout).toBe(originals.clearTimeout); + expect(setInterval).toBe(originals.setInterval); + expect(clearInterval).toBe(originals.clearInterval); + }); +}); + +it.describe('shouldAdvanceTime', () => { + it('should create an auto advancing timer', async () => { + const testDelay = 29; + const date = new Date('2015-09-25'); + const clock = createClock(date); + clock.advanceAutomatically(); + expect(clock.Date.now()).toBe(1443139200000); + const timeoutStarted = clock.Date.now(); + + let callback: (r: number) => void; + const promise = new Promise(r => callback = r); + + clock.setTimeout(() => { + const timeDifference = clock.Date.now() - timeoutStarted; + callback(timeDifference); + }, testDelay); + expect(await promise).toBe(testDelay); + + }); + + it('should test setInterval', async () => { + const interval = 20; + let intervalsTriggered = 0; + const cyclesToTrigger = 3; + const date = new Date('2015-09-25'); + const clock = createClock(date); + clock.advanceAutomatically(); + expect(clock.Date.now()).toBe(1443139200000); + const timeoutStarted = clock.Date.now(); + + let callback: (r: number) => void; + const promise = new Promise(r => callback = r); + + const intervalId = clock.setInterval(() => { + if (++intervalsTriggered === cyclesToTrigger) { + clock.clearInterval(intervalId); + const timeDifference = clock.Date.now() - timeoutStarted; + callback(timeDifference); + } + }, interval); + + expect(await promise).toBe(interval * cyclesToTrigger); + }); + + it('should not depend on having to stub setInterval or clearInterval to work', async ({ install }) => { + const origSetInterval = globalThis.setInterval; + const origClearInterval = globalThis.clearInterval; + + install({ toFake: ['setTimeout'] }); + expect(globalThis.setInterval).toBe(origSetInterval); + expect(globalThis.clearInterval).toBe(origClearInterval); + }); +}); + +it.describe('requestAnimationFrame', () => { + it('throws if no arguments', async ({ clock }) => { + expect(() => { + // @ts-expect-error + clock.requestAnimationFrame(); + }).toThrow(); + }); + + it('returns numeric id or object with numeric id', async ({ clock }) => { + const result = clock.requestAnimationFrame(() => {}); + expect(result).toEqual(expect.any(Number)); + }); + + it('returns unique id', async ({ clock }) => { + const id1 = clock.requestAnimationFrame(() => {}); + const id2 = clock.requestAnimationFrame(() => {}); + expect(id2).not.toEqual(id1); + }); + + it('should run every 16ms', async ({ clock }) => { + const stub = createStub(); + clock.requestAnimationFrame(stub); + clock.tick(15); + expect(stub.callCount).toBe(0); + clock.tick(1); + expect(stub.callCount).toBe(1); + }); + + it('should be called with performance.now() when available', async ({ clock }) => { + const stub = createStub(); + clock.requestAnimationFrame(stub); + clock.tick(20); + expect(stub.calledWith(16)).toBeTruthy(); + }); + + it('should be called with performance.now() even when performance unavailable', async ({ clock }) => { + const stub = createStub(); + clock.requestAnimationFrame(stub); + clock.tick(20); + expect(stub.calledWith(16)).toBeTruthy(); + }); + + it('should call callback once', async ({ clock }) => { + const stub = createStub(); + clock.requestAnimationFrame(stub); + clock.tick(32); + expect(stub.callCount).toBe(1); + }); + + it('should schedule two callbacks before the next frame at the same time', async ({ clock }) => { + const stub1 = createStub(); + const stub2 = createStub(); + clock.requestAnimationFrame(stub1); + clock.tick(5); + clock.requestAnimationFrame(stub2); + clock.tick(11); + expect(stub1.calledWith(16)).toBeTruthy(); + expect(stub2.calledWith(16)).toBeTruthy(); + }); + + it('should properly schedule callback for 3rd frame', async ({ clock }) => { + const stub1 = createStub(); + const stub2 = createStub(); + clock.requestAnimationFrame(stub1); + clock.tick(57); + clock.requestAnimationFrame(stub2); + clock.tick(10); + expect(stub1.calledWith(16)).toBeTruthy(); + expect(stub2.calledWith(64)).toBeTruthy(); + }); + + it('should schedule for next frame if on current frame', ({ clock }) => { + const stub = createStub(); + clock.tick(16); + clock.requestAnimationFrame(stub); + clock.tick(16); + expect(stub.calledWith(32)).toBeTruthy(); + }); +}); + +it.describe('cancelAnimationFrame', () => { + it('removes animation frame', async ({ clock }) => { + const stub = createStub(); + const id = clock.requestAnimationFrame(stub); + clock.cancelAnimationFrame(id); + clock.tick(16); + expect(stub.called).toBeFalsy(); + }); + + it('does not remove timeout', async ({ clock }) => { + const stub = createStub(); + const id = clock.setTimeout(stub, 50); + expect(() => { + clock.cancelAnimationFrame(id); + }).toThrow(); + clock.tick(50); + expect(stub.called).toBeTruthy(); + }); + + it('does not remove interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub, 50); + expect(() => { + clock.cancelAnimationFrame(id); + }).toThrow(); + clock.tick(50); + expect(stub.called).toBeTruthy(); + }); + + it('ignores null argument', async ({ clock }) => { + clock.cancelAnimationFrame(null); + }); +}); + +it.describe('runToFrame', () => { + it('should tick next frame', async ({ clock }) => { + clock.runToFrame(); + expect(clock.now()).toBe(16); + clock.tick(3); + clock.runToFrame(); + expect(clock.now()).toBe(32); + }); +}); + +it.describe('jump', () => { + it('ignores timers which wouldn\'t be run', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 1000); + clock.jump(500); + expect(stub.called).toBeFalsy(); + }); + + it('pushes back execution time for skipped timers', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(() => { + stub(clock.Date.now()); + }, 1000); + clock.jump(2000); + expect(stub.callCount).toBe(1); + expect(stub.calledWith(2000)).toBeTruthy(); + }); + + it('handles multiple pending timers and types', async ({ clock }) => { + const longTimers = [createStub(), createStub()]; + const shortTimers = [createStub(), createStub(), createStub()]; + clock.setTimeout(longTimers[0], 2000); + clock.setInterval(longTimers[1], 2500); + clock.setTimeout(shortTimers[0], 250); + clock.setInterval(shortTimers[1], 100); + clock.requestAnimationFrame(shortTimers[2]); + clock.jump(1500); + for (const stub of longTimers) + expect(stub.called).toBeFalsy(); + for (const stub of shortTimers) + expect(stub.callCount).toBe(1); + }); + + it('supports string time arguments', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100000); // 100000 = 1:40 + clock.jump('01:50'); + expect(stub.callCount).toBe(1); + }); +}); + +it.describe('performance.now()', () => { + it('should start at 0', async ({ clock }) => { + const result = clock.performance.now(); + expect(result).toBe(0); + }); + + it('should run along with clock.tick', async ({ clock }) => { + clock.tick(5000); + const result = clock.performance.now(); + expect(result).toBe(5000); + }); + + it('should listen to multiple ticks in performance.now', async ({ clock }) => { + for (let i = 0; i < 10; i++) { + const next = clock.performance.now(); + expect(next).toBe(1000 * i); + clock.tick(1000); + } + }); + + it('should run with ticks with timers set', async ({ clock }) => { + clock.setTimeout(() => { + const result = clock.performance.now(); + expect(result).toBe(2500); + }, 2500); + clock.tick(5000); + }); +}); + +it.describe('requestIdleCallback', () => { + it('throws if no arguments', async ({ clock }) => { + expect(() => { + // @ts-expect-error + clock.requestIdleCallback(); + }).toThrow(); + }); + + it('returns numeric id', async ({ clock }) => { + const result = clock.requestIdleCallback(() => {}); + expect(result).toEqual(expect.any(Number)); + }); + + it('returns unique id', async ({ clock }) => { + const id1 = clock.requestIdleCallback(() => {}); + const id2 = clock.requestIdleCallback(() => {}); + expect(id2).not.toEqual(id1); + }); + + it('runs after all timers', async ({ clock }) => { + const stub = createStub(); + clock.requestIdleCallback(stub); + clock.tick(1000); + expect(stub.called).toBeTruthy(); + }); + + it('runs no later than timeout option even if there are any timers', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(() => {}, 10); + clock.setTimeout(() => {}, 30); + clock.requestIdleCallback(stub, { timeout: 20 }); + clock.tick(20); + expect(stub.called).toBeTruthy(); + }); + + it(`doesn't runs if there are any timers and no timeout option`, async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(() => {}, 30); + clock.requestIdleCallback(stub); + clock.tick(35); + expect(stub.called).toBeFalsy(); + }); +}); + +it.describe('cancelIdleCallback', () => { + it('removes idle callback', async ({ clock }) => { + const stub = createStub(); + const callbackId = clock.requestIdleCallback(stub, { timeout: 0 }); + clock.cancelIdleCallback(callbackId); + clock.tick(0); + expect(stub.called).toBeFalsy(); + }); +}); + +it.describe('loop limit stack trace', () => { + const expectedMessage = + 'Aborting after running 5 timers, assuming an infinite loop!'; + it.use({ loopLimit: 5 }); + + it.describe('setTimeout', () => { + it('provides a stack trace for running all async', async ({ clock }) => { + const catchSpy = createStub(); + const recursiveCreateTimer = () => { + clock.setTimeout(recursiveCreateTimer, 10); + }; + + recursiveCreateTimer(); + await clock.runAllAsync().catch(catchSpy); + expect(catchSpy.callCount).toBe(1); + const err = catchSpy.firstCall.args[0]; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); + }); + + it('provides a stack trace for running all sync', ({ clock }) => { + let caughtError = false; + const recursiveCreateTimer = () => { + clock.setTimeout(recursiveCreateTimer, 10); + }; + + recursiveCreateTimer(); + try { + clock.runAll(); + } catch (err) { + caughtError = true; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); + } + expect(caughtError).toBeTruthy(); + }); + }); + + it.describe('requestIdleCallback', () => { + it('provides a stack trace for running all async', async ({ clock }) => { + const catchSpy = createStub(); + const recursiveCreateTimer = () => { + clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); + }; + + recursiveCreateTimer(); + await clock.runAllAsync().catch(catchSpy); + expect(catchSpy.callCount).toBe(1); + const err = catchSpy.firstCall.args[0]; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); + }); + + it('provides a stack trace for running all sync', ({ clock }) => { + let caughtError = false; + const recursiveCreateTimer = () => { + clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); + }; + + recursiveCreateTimer(); + try { + clock.runAll(); + } catch (err) { + caughtError = true; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); + } + expect(caughtError).toBeTruthy(); + }); + }); + + it.describe('setInterval', () => { + it('provides a stack trace for running all async', async ({ clock }) => { + const catchSpy = createStub(); + const recursiveCreateTimer = () => { + clock.setInterval(recursiveCreateTimer, 10); + }; + + recursiveCreateTimer(); + await clock.runAllAsync().catch(catchSpy); + expect(catchSpy.callCount).toBe(1); + const err = catchSpy.firstCall.args[0]; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); + }); + + it('provides a stack trace for running all sync', ({ clock }) => { + let caughtError = false; + const recursiveCreateTimer = () => { + clock.setInterval(recursiveCreateTimer, 10); + }; + + recursiveCreateTimer(); + try { + clock.runAll(); + } catch (err) { + caughtError = true; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); + } + expect(caughtError).toBeTruthy(); + }); + }); + + it.describe('requestAnimationFrame', () => { + it('provides a stack trace for running all async', async ({ clock }) => { + const catchSpy = createStub(); + const recursiveCreateTimer = () => { + clock.requestAnimationFrame(recursiveCreateTimer); + }; + + recursiveCreateTimer(); + await clock.runAllAsync().catch(catchSpy); + expect(catchSpy.callCount).toBe(1); + const err = catchSpy.firstCall.args[0]; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); + }); + + it('provides a stack trace for running all sync', ({ clock }) => { + let caughtError = false; + const recursiveCreateTimer = () => { + clock.requestAnimationFrame(recursiveCreateTimer); + }; + + recursiveCreateTimer(); + try { + clock.runAll(); + } catch (err) { + caughtError = true; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); + } + expect(caughtError).toBeTruthy(); + }); + }); +}); + +it.describe('Intl API', () => { + function isFirstOfMonth(ianaTimeZone, timestamp?: number) { + return ( + new Intl.DateTimeFormat(undefined, { timeZone: ianaTimeZone }) + .formatToParts(timestamp) + .find(part => part.type === 'day').value === '1' + ); + } + + it('Executes formatRange like normal', async ({ clock }) => { + const start = new Date(Date.UTC(2020, 0, 1, 0, 0)); + const end = new Date(Date.UTC(2020, 0, 1, 0, 1)); + const options: Intl.DateTimeFormatOptions = { + timeZone: 'UTC', + hour12: false, + hour: 'numeric', + minute: 'numeric', + }; + expect( + new Intl.DateTimeFormat('en-GB', options).formatRange(start, end), + ).toBe('00:00–00:01'); + }); + + it('Executes formatRangeToParts like normal', async ({ clock }) => { + const start = new Date(Date.UTC(2020, 0, 1, 0, 0)); + const end = new Date(Date.UTC(2020, 0, 1, 0, 1)); + const options: Intl.DateTimeFormatOptions = { + timeZone: 'UTC', + hour12: false, + hour: 'numeric', + minute: 'numeric', + }; + expect(new Intl.DateTimeFormat('en-GB', options).formatRangeToParts(start, end)).toEqual([ + { type: 'hour', value: '00', source: 'startRange' }, + { type: 'literal', value: ':', source: 'startRange' }, + { type: 'minute', value: '00', source: 'startRange' }, + { type: 'literal', value: '–', source: 'shared' }, + { type: 'hour', value: '00', source: 'endRange' }, + { type: 'literal', value: ':', source: 'endRange' }, + { type: 'minute', value: '01', source: 'endRange' }, + ]); + }); + + it('Executes resolvedOptions like normal', async ({ clock }) => { + const options: Intl.DateTimeFormatOptions = { + timeZone: 'UTC', + hour12: false, + hour: '2-digit', + minute: '2-digit', + }; + expect(new Intl.DateTimeFormat('en-GB', options).resolvedOptions()).toEqual({ + locale: 'en-GB', + calendar: 'gregory', + numberingSystem: 'latn', + timeZone: 'UTC', + hour12: false, + hourCycle: 'h23', + hour: '2-digit', + minute: '2-digit', + }); + }); + + it('formatToParts via isFirstOfMonth -> Returns true when passed a timestamp argument that is first of the month', async ({ clock }) => { + // June 1 04:00 UTC - Toronto is June 1 00:00 + expect(isFirstOfMonth('America/Toronto', Date.UTC(2022, 5, 1, 4))).toBeTruthy(); + }); + + it('formatToParts via isFirstOfMonth -> Returns false when passed a timestamp argument that is not first of the month', async ({ clock }) => { + // June 1 00:00 UTC - Toronto is May 31 20:00 + expect(isFirstOfMonth('America/Toronto', Date.UTC(2022, 5, 1))).toBeFalsy(); + }); + + it('formatToParts via isFirstOfMonth -> Returns true when passed no timestamp and system time is first of the month', async ({ install }) => { + // June 1 04:00 UTC - Toronto is June 1 00:00 + install({ now: Date.UTC(2022, 5, 1, 4) }); + expect(isFirstOfMonth('America/Toronto')).toBeTruthy(); + }); + + it('formatToParts via isFirstOfMonth -> Returns false when passed no timestamp and system time is not first of the month', async ({ install }) => { + // June 1 00:00 UTC - Toronto is May 31 20:00 + install({ now: Date.UTC(2022, 5, 1) }); + expect(isFirstOfMonth('America/Toronto')).toBeFalsy(); + }); + + it('Executes supportedLocalesOf like normal', async ({ installEx }) => { + const { originals } = installEx(); + expect(Intl.DateTimeFormat.supportedLocalesOf([])).toEqual( + originals.Intl.DateTimeFormat.supportedLocalesOf([]), + ); + }); + + it('Creates a RelativeTimeFormat like normal', async ({ clock }) => { + const rtf = new Intl.RelativeTimeFormat('en-GB', { + numeric: 'auto', + }); + expect(rtf.format(2, 'day')).toBe('in 2 days'); + }); +}); + +interface Stub { + called: boolean; + callCount: number; + calls: { receiver: any, args: any[], time: bigint }[]; + firstCall: { args: any[] } | undefined; + calledOn: (thisObj: any) => boolean; + calledBefore: (other: Stub) => boolean; + calledWithExactly: (...args: any[]) => void; + calledWith(arg: any): void; + (...args: any[]): void; + throws: () => Stub; +} + +const createStub = (body?: () => void): Stub => { + const allFirstArgs = new Set(); + const stub: Stub = function(...args: any[]) { + stub.calls.push({ receiver: this, args, time: process.hrtime.bigint() }); + allFirstArgs.add(args[0]); + if (body) + body(); + } as any; + + stub.calls = []; + const stubAny = stub as any; + stubAny.__defineGetter__('callCount', () => stub.calls.length); + stubAny.__defineGetter__('called', () => stub.calls.length > 0); + stubAny.__defineGetter__('firstCall', () => stub.calls[0]); + + stub.calledOn = thisObj => stub.calls[0].receiver === thisObj; + + stub.calledWithExactly = (...args) => { + expect(stub.calls[0].args).toEqual(args); + return true; + }; + stub.calledWith = arg => { + expect(allFirstArgs).toContain(arg); + return true; + }; + stub.calledBefore = other => { + expect(other.calls[0].time).toBeGreaterThan(stub.calls[0].time); + return true; + }; + stub.throws = () => createStub(() => { throw new Error(''); }); + return stub; +}; + +const nodeMajorVersion = +process.versions.node.split('.')[0]; diff --git a/utils/generate_injected.js b/utils/generate_injected.js index a39583ab38..80c3be5ac0 100644 --- a/utils/generate_injected.js +++ b/utils/generate_injected.js @@ -51,7 +51,7 @@ const injectedScripts = [ true, ], [ - path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'fakeTimers.ts'), + path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'clock.ts'), path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), true, From dd3a41287e8edaf537481e72316a3dead548b645 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 6 Jun 2024 19:26:30 -0700 Subject: [PATCH 033/144] chore: simplify doTick (#31196) --- packages/playwright-core/src/server/clock.ts | 13 +- .../src/server/injected/clock.ts | 215 +-- tests/library/clock.spec.ts | 1182 ++++------------- 3 files changed, 285 insertions(+), 1125 deletions(-) diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index 09ac9cfec3..dd7f194a1d 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -39,22 +39,19 @@ export class Clock { async runToNextTimer(): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwClock.clock.next()`); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.nextAsync()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.next()`); return this._now; } async runAllTimers(): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runAll()`); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAllAsync()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAll()`); return this._now; } async runToLastTimer(): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runToLast()`); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLastAsync()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLast()`); return this._now; } @@ -85,8 +82,8 @@ export class Clock { async runFor(time: number | string): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tickAsync(${JSON.stringify(time)})`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.recordTick(${JSON.stringify(time)})`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`); return this._now; } diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index 6d9f0a2036..af846b42c8 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -89,149 +89,59 @@ export class ClockController { return this._now - this._adjustedSystemTime - this.start; } - private _doTick(tickValue: number | string, isAsync: boolean, resolve?: (time: number) => void, reject?: (error: Error) => void): number | undefined { - const msFloat = - typeof tickValue === 'number' - ? tickValue - : parseTime(tickValue); - const ms = Math.floor(msFloat); - let tickTo = this._now + ms; - + private async _doTick(msFloat: number): Promise { if (msFloat < 0) throw new TypeError('Negative ticks are not supported'); + const ms = Math.floor(msFloat); + let tickTo = this._now + ms; let tickFrom = this._now; let previous = this._now; - // ESLint fails to detect this correctly - /* eslint-disable prefer-const */ - let timer; - let firstException: Error; - let oldNow: number; - let nextPromiseTick: (() => void) | null; - let compensationCheck: () => void; - let postTimerCall: () => void; - + let firstException: Error | undefined; this._duringTick = true; - // perform microtasks - oldNow = this._now; - if (oldNow !== this._now) { - // compensate for any setSystemTime() call during microtask callback - tickFrom += this._now - oldNow; - tickTo += this._now - oldNow; - } - - const doTickInner = (): number | undefined => { - // perform each timer in the requested range - timer = this._firstTimerInRange(tickFrom, tickTo); - while (timer && tickFrom <= tickTo) { - if (this._timers.has(timer.id)) { - tickFrom = timer.callAt; - this._now = timer.callAt; - oldNow = this._now; - try { - this._callTimer(timer); - } catch (e) { - firstException = firstException || e; - } - - if (isAsync) { - // finish up after native setImmediate callback to allow - // all native es6 promises to process their callbacks after - // each timer fires. - this._embedder.postTask(nextPromiseTick!); - return; - } - compensationCheck(); - } - postTimerCall(); + // perform each timer in the requested range + let timer = this._firstTimerInRange(tickFrom, tickTo); + while (timer && tickFrom <= tickTo) { + tickFrom = timer.callAt; + this._now = timer.callAt; + const oldNow = this._now; + try { + this._callTimer(timer); + await new Promise(f => this._embedder.postTask(f)); + } catch (e) { + firstException = firstException || e; } - // perform process.nextTick()s again - oldNow = this._now; - if (oldNow !== this._now) { - // compensate for any setSystemTime() call during process.nextTick() callback - tickFrom += this._now - oldNow; - tickTo += this._now - oldNow; - } - this._duringTick = false; - - // corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo] - timer = this._firstTimerInRange(tickFrom, tickTo); - if (timer) { - try { - this.tick(tickTo - this._now); // do it all again - for the remainder of the requested range - } catch (e) { - firstException = firstException || e; - } - } else { - // no timers remaining in the requested range: move the clock all the way to the end - this._now = tickTo; - } - if (firstException) - throw firstException; - - if (isAsync) - resolve!(this._now); - else - return this._now; - }; - - nextPromiseTick = - isAsync ? - () => { - try { - compensationCheck(); - postTimerCall(); - doTickInner(); - } catch (e) { - reject!(e); - } - } : null; - - compensationCheck = () => { // compensate for any setSystemTime() call during timer callback if (oldNow !== this._now) { tickFrom += this._now - oldNow; tickTo += this._now - oldNow; previous += this._now - oldNow; } - }; - postTimerCall = () => { timer = this._firstTimerInRange(previous, tickTo); previous = tickFrom; - }; - - return doTickInner(); - } - - tick(tickValue: string | number): number { - return this._doTick(tickValue, false)!; - } - - async tickAsync(tickValue: string | number): Promise { - await new Promise(f => this._embedder.postTask(f)); - return new Promise((resolve, reject) => this._doTick(tickValue, true, resolve, reject)); - } - - next() { - const timer = this._firstTimer(); - if (!timer) - return this._now; - - this._duringTick = true; - try { - this._now = timer.callAt; - this._callTimer(timer); - return this._now; - } finally { - this._duringTick = false; } + + this._duringTick = false; + this._now = tickTo; + if (firstException) + throw firstException; + + return this._now; } - async nextAsync() { - await new Promise(f => this._embedder.postTask(f)); + async recordTick(tickValue: string | number) { + const msFloat = parseTime(tickValue); + this._now += msFloat; + } + + async tick(tickValue: string | number): Promise { + return await this._doTick(parseTime(tickValue)); + } + + async next() { const timer = this._firstTimer(); if (!timer) return this._now; @@ -241,45 +151,29 @@ export class ClockController { this._now = timer.callAt; try { this._callTimer(timer); + await new Promise(f => this._embedder.postTask(f)); } catch (e) { err = e; } this._duringTick = false; - await new Promise(f => this._embedder.postTask(f)); if (err) throw err; return this._now; } - runAll() { - for (let i = 0; i < this._loopLimit; i++) { - const numTimers = this._timers.size; - if (numTimers === 0) - return this._now; - this.next(); - } - - const excessJob = this._firstTimer(); - if (!excessJob) - return; - throw this._getInfiniteLoopError(excessJob); - } - - runToFrame() { + async runToFrame() { return this.tick(this.getTimeToNextFrame()); } - async runAllAsync() { + async runAll() { for (let i = 0; i < this._loopLimit; i++) { - await new Promise(f => this._embedder.postTask(f)); const numTimers = this._timers.size; if (numTimers === 0) return this._now; - this.next(); + await this.next(); } - await new Promise(f => this._embedder.postTask(f)); const excessJob = this._firstTimer(); if (!excessJob) @@ -287,28 +181,11 @@ export class ClockController { throw this._getInfiniteLoopError(excessJob); } - runToLast() { + async runToLast() { const timer = this._lastTimer(); if (!timer) return this._now; - return this.tick(timer.callAt - this._now); - } - - runToLastAsync() { - return new Promise((resolve, reject) => { - this._embedder.postTask(() => { - try { - const timer = this._lastTimer(); - if (!timer) { - resolve(this._now); - return; - } - this.tickAsync(timer.callAt - this._now).then(resolve); - } catch (e) { - reject(e); - } - }); - }); + return await this.tick(timer.callAt - this._now); } reset() { @@ -332,18 +209,15 @@ export class ClockController { } } - jump(tickValue: string | number): number { - const msFloat = - typeof tickValue === 'number' - ? tickValue - : parseTime(tickValue); + async jump(tickValue: string | number): Promise { + const msFloat = parseTime(tickValue); const ms = Math.floor(msFloat); for (const timer of this._timers.values()) { if (this._now + ms > timer.callAt) timer.callAt = this._now + ms; } - return this.tick(ms); + return await this.tick(ms); } addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: () => any[] }): number { @@ -524,9 +398,12 @@ function inRange(from: number, to: number, timer: Timer): boolean { * number of milliseconds. This is used to support human-readable strings passed * to clock.tick() */ -function parseTime(str: string): number { - if (!str) +function parseTime(value: number | string): number { + if (typeof value === 'number') + return value; + if (!value) return 0; + const str = value; const strings = str.split(':'); const l = strings.length; diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index bb351186b5..cf27d2744b 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -98,7 +98,7 @@ it.describe('setTimeout', () => { clock1.setTimeout(stubs[0], 100); clock2.setTimeout(stubs[1], 100); - clock2.tick(200); + await clock2.tick(200); expect(stubs[0].called).toBeFalsy(); expect(stubs[1].called).toBeTruthy(); @@ -110,7 +110,7 @@ it.describe('setTimeout', () => { evalCalled = true; // @ts-expect-error }, '10'); - clock.tick(10); + await clock.tick(10); expect(evalCalled).toBeTruthy(); }); @@ -120,7 +120,7 @@ it.describe('setTimeout', () => { evalCalled = true; // @ts-expect-error }, 'string'); - clock.tick(10); + await clock.tick(10); expect(evalCalled).toBeTruthy(); }); @@ -128,49 +128,49 @@ it.describe('setTimeout', () => { it('passes setTimeout parameters', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 2, 'the first', 'the second'); - clock.tick(3); + await clock.tick(3); expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); }); it('calls correct timeout on recursive tick', async ({ clock }) => { const stub = createStub(); const recurseCallback = () => { - clock.tick(100); + void clock.tick(100); }; clock.setTimeout(recurseCallback, 50); clock.setTimeout(stub, 100); - clock.tick(50); + await clock.tick(50); expect(stub.called).toBeTruthy(); }); it('does not depend on this', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - clock.tick(100); + await clock.tick(100); expect(stub.called).toBeTruthy(); }); it('is not influenced by forward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 5000); - clock.tick(1000); + await clock.tick(1000); clock.setSystemTime(new clock.Date().getTime() + 1000); - clock.tick(3990); + await clock.tick(3990); expect(stub.callCount).toBe(0); - clock.tick(20); + await clock.tick(20); expect(stub.callCount).toBe(1); }); it('is not influenced by backward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 5000); - clock.tick(1000); + await clock.tick(1000); clock.setSystemTime(new clock.Date().getTime() - 1000); - clock.tick(3990); + await clock.tick(3990); expect(stub.callCount).toBe(0); - clock.tick(20); + await clock.tick(20); expect(stub.callCount).toBe(1); }); @@ -185,7 +185,7 @@ it.describe('setTimeout', () => { clock.setTimeout(() => { calls.push('-Infinity'); }, Number.NEGATIVE_INFINITY); - clock.runAll(); + await clock.runAll(); expect(calls).toEqual(['NaN', 'Infinity', '-Infinity']); }); @@ -200,7 +200,7 @@ it.describe('setTimeout', () => { it('evals non-function callbacks', async ({ clock }) => { clock.setTimeout('globalThis.evalCalled = true', 10); - clock.tick(10); + await clock.tick(10); expect(globalThis.evalCalled).toBeTruthy(); }); @@ -209,7 +209,7 @@ it.describe('setTimeout', () => { const x = 15; try { clock.setTimeout('x', x); - clock.tick(x); + await clock.tick(x); expect(true).toBeFalsy(); } catch (e) { expect(e).toBeInstanceOf(ReferenceError); @@ -222,21 +222,21 @@ it.describe('tick', () => { it('triggers immediately without specified delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub); - clock.tick(0); + await clock.tick(0); expect(stub.called).toBeTruthy(); }); it('does not trigger without sufficient delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - clock.tick(10); + await clock.tick(10); expect(stub.called).toBeFalsy(); }); it('triggers after sufficient delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - clock.tick(100); + await clock.tick(100); expect(stub.called).toBeTruthy(); }); @@ -244,7 +244,7 @@ it.describe('tick', () => { const spies = [createStub(), createStub()]; clock.setTimeout(spies[0], 100); clock.setTimeout(spies[1], 100); - clock.tick(100); + await clock.tick(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); }); @@ -255,7 +255,7 @@ it.describe('tick', () => { clock.setTimeout(spies[1], 100); clock.setTimeout(spies[2], 99); clock.setTimeout(spies[3], 100); - clock.tick(100); + await clock.tick(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); expect(spies[2].called).toBeTruthy(); @@ -274,19 +274,19 @@ it.describe('tick', () => { // First spy calls another setTimeout with delay=0 clock.setTimeout(spies[0], 0); clock.setTimeout(spies[2], 10); - clock.tick(10); + await clock.tick(10); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); expect(spies[2].called).toBeTruthy(); }); it('waits after setTimeout was called', async ({ clock }) => { - clock.tick(100); + await clock.tick(100); const stub = createStub(); clock.setTimeout(stub, 150); - clock.tick(50); + await clock.tick(50); expect(stub.called).toBeFalsy(); - clock.tick(100); + await clock.tick(100); expect(stub.called).toBeTruthy(); }); @@ -294,19 +294,19 @@ it.describe('tick', () => { const stubs = [createStub(), createStub(), createStub()]; clock.setTimeout(stubs[0], 100); clock.setTimeout(stubs[1], 120); - clock.tick(10); - clock.tick(89); + await clock.tick(10); + await clock.tick(89); expect(stubs[0].called).toBeFalsy(); expect(stubs[1].called).toBeFalsy(); clock.setTimeout(stubs[2], 20); - clock.tick(1); + await clock.tick(1); expect(stubs[0].called).toBeTruthy(); expect(stubs[1].called).toBeFalsy(); expect(stubs[2].called).toBeFalsy(); - clock.tick(19); + await clock.tick(19); expect(stubs[1].called).toBeFalsy(); expect(stubs[2].called).toBeTruthy(); - clock.tick(1); + await clock.tick(1); expect(stubs[1].called).toBeTruthy(); }); @@ -316,9 +316,7 @@ it.describe('tick', () => { clock.setTimeout(stubs[0], 100); clock.setTimeout(stubs[1], 120); - expect(() => { - clock.tick(120); - }).toThrow(); + await expect(clock.tick(120)).rejects.toThrow(); expect(stubs[0].called).toBeTruthy(); expect(stubs[1].called).toBeTruthy(); @@ -327,11 +325,7 @@ it.describe('tick', () => { it('calls function with global object or null (strict mode) as this', async ({ clock }) => { const stub = createStub().throws(); clock.setTimeout(stub, 100); - - expect(() => { - clock.tick(100); - }).toThrow(); - + await expect(clock.tick(100)).rejects.toThrow(); expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); }); @@ -340,7 +334,7 @@ it.describe('tick', () => { clock.setTimeout(spies[0], 13); clock.setTimeout(spies[1], 11); - clock.tick(15); + await clock.tick(15); expect(spies[1].calledBefore(spies[0])).toBeTruthy(); }); @@ -352,7 +346,7 @@ it.describe('tick', () => { spy(new clock.Date().getTime()); }, 10); - clock.tick(100); + await clock.tick(100); expect(spy.callCount).toBe(10); expect(spy.calledWith(10)).toBeTruthy(); @@ -370,7 +364,7 @@ it.describe('tick', () => { it('fires timer in intervals of 13', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 13); - clock.tick(500); + await clock.tick(500); expect(spy.callCount).toBe(38); }); @@ -378,7 +372,7 @@ it.describe('tick', () => { const spy = createStub(); // @ts-expect-error clock.setInterval(spy, '13'); - clock.tick(500); + await clock.tick(500); expect(spy.callCount).toBe(38); }); @@ -394,7 +388,7 @@ it.describe('tick', () => { spy10(new clock.Date().getTime()); }, 10); - clock.tick(500); + await clock.tick(500); expect(spy13.callCount).toBe(38); expect(spy10.callCount).toBe(50); @@ -408,7 +402,7 @@ it.describe('tick', () => { clock.setInterval(spies[0], 10); clock.setTimeout(spies[1], 50); - clock.tick(100); + await clock.tick(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); expect(spies[0].callCount).toBe(10); @@ -422,37 +416,29 @@ it.describe('tick', () => { if (callback.callCount === 3) clock.clearInterval(id); }); - id = clock.setInterval(callback, 10); - clock.tick(100); - + await clock.tick(100); expect(callback.callCount).toBe(3); }); it('passes 8 seconds', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 4000); - - clock.tick('08'); - + await clock.tick('08'); expect(spy.callCount).toBe(2); }); it('passes 1 minute', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 6000); - - clock.tick('01:00'); - + await clock.tick('01:00'); expect(spy.callCount).toBe(10); }); it('passes 2 hours, 34 minutes and 10 seconds', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 10000); - - clock.tick('02:34:10'); - + await clock.tick('02:34:10'); expect(spy.callCount).toBe(925); }); @@ -460,9 +446,7 @@ it.describe('tick', () => { const spy = createStub(); clock.setInterval(spy, 10000); - expect(() => { - clock.tick('12:02:34:10'); - }).toThrow(); + await expect(clock.tick('12:02:34:10')).rejects.toThrow(); expect(spy.callCount).toBe(0); }); @@ -470,11 +454,7 @@ it.describe('tick', () => { it('throws for invalid minutes', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 10000); - - expect(() => { - clock.tick('67:10'); - }).toThrow(); - + await expect(clock.tick('67:10')).rejects.toThrow(); expect(spy.callCount).toBe(0); }); @@ -482,16 +462,13 @@ it.describe('tick', () => { const spy = createStub(); clock.setInterval(spy, 10000); - expect(() => { - clock.tick('-7:10'); - }).toThrow(); - + await expect(clock.tick('-7:10')).rejects.toThrow(); expect(spy.callCount).toBe(0); }); it('treats missing argument as 0', async ({ clock }) => { // @ts-expect-error - clock.tick(); + await clock.tick(); expect(clock.now()).toBe(0); }); @@ -506,9 +483,7 @@ it.describe('tick', () => { }; callback(); - - clock.tick(1000); - + await clock.tick(1000); expect(i).toBe(11); }); @@ -516,16 +491,12 @@ it.describe('tick', () => { const callback = () => { throw new Error('oh no!'); }; - clock.setTimeout(callback, 1000); - - expect(() => { - clock.tick(1000); - }).toThrow(); + await expect(clock.tick(1000)).rejects.toThrow(); }); it('returns the current now value', async ({ clock }) => { - const value = clock.tick(200); + const value = await clock.tick(200); expect(clock.now()).toBe(value); }); @@ -536,9 +507,9 @@ it.describe('tick', () => { const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - clock.tick(1990); + await clock.tick(1990); expect(stub.callCount).toBe(0); - clock.tick(20); + await clock.tick(20); expect(stub.callCount).toBe(1); }); @@ -549,9 +520,9 @@ it.describe('tick', () => { const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - clock.tick(1990); + await clock.tick(1990); expect(stub.callCount).toBe(0); - clock.tick(20); + await clock.tick(20); expect(stub.callCount).toBe(1); }); @@ -564,12 +535,10 @@ it.describe('tick', () => { clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - expect(() => { - clock.tick(1990); - }).toThrow(); + await expect(clock.tick(1990)).rejects.toThrow(); expect(stub.callCount).toBe(0); - clock.tick(20); + await clock.tick(20); expect(stub.callCount).toBe(1); }); @@ -582,188 +551,15 @@ it.describe('tick', () => { clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - expect(() => { - clock.tick(1990); - }).toThrow(); + await expect(clock.tick(1990)).rejects.toThrow(); expect(stub.callCount).toBe(0); - clock.tick(20); + await clock.tick(20); expect(stub.callCount).toBe(1); }); it('throws on negative ticks', async ({ clock }) => { - expect(() => { - clock.tick(-500); - }).toThrow('Negative ticks are not supported'); - }); -}); - -it.describe('tickAsync', () => { - it('triggers immediately without specified delay', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub); - - await clock.tickAsync(0); - - expect(stub.called).toBeTruthy(); - }); - - it('does not trigger without sufficient delay', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub, 100); - - await clock.tickAsync(10); - - expect(stub.called).toBeFalsy(); - }); - - it('triggers after sufficient delay', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub, 100); - - await clock.tickAsync(100); - - expect(stub.called).toBeTruthy(); - }); - - it('triggers simultaneous timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 100); - clock.setTimeout(spies[1], 100); - - await clock.tickAsync(100); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('triggers multiple simultaneous timers', async ({ clock }) => { - const spies = [createStub(), createStub(), createStub(), createStub()]; - clock.setTimeout(spies[0], 100); - clock.setTimeout(spies[1], 100); - clock.setTimeout(spies[2], 99); - clock.setTimeout(spies[3], 100); - - await clock.tickAsync(100); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - expect(spies[2].called).toBeTruthy(); - expect(spies[3].called).toBeTruthy(); - }); - - it('triggers multiple simultaneous timers with zero callAt', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 0); - }), - createStub(), - createStub(), - ]; - - // First spy calls another setTimeout with delay=0 - clock.setTimeout(spies[0], 0); - clock.setTimeout(spies[2], 10); - - await clock.tickAsync(10); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - expect(spies[2].called).toBeTruthy(); - }); - - it('waits after setTimeout was called', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub, 150); - - await clock.tickAsync(50); - - expect(stub.called).toBeFalsy(); - - await clock.tickAsync(100); - - expect(stub.called).toBeTruthy(); - }); - - it('mini integration test', async ({ clock }) => { - const stubs = [createStub(), createStub(), createStub()]; - clock.setTimeout(stubs[0], 100); - clock.setTimeout(stubs[1], 120); - - await clock.tickAsync(10); - await clock.tickAsync(89); - - expect(stubs[0].called).toBeFalsy(); - expect(stubs[1].called).toBeFalsy(); - - clock.setTimeout(stubs[2], 20); - await clock.tickAsync(1); - - expect(stubs[0].called).toBeTruthy(); - expect(stubs[1].called).toBeFalsy(); - expect(stubs[2].called).toBeFalsy(); - - await clock.tickAsync(19); - - expect(stubs[1].called).toBeFalsy(); - expect(stubs[2].called).toBeTruthy(); - - await clock.tickAsync(1); - - expect(stubs[1].called).toBeTruthy(); - }); - - it('triggers even when some throw', async ({ clock }) => { - const stubs = [createStub().throws(), createStub()]; - - clock.setTimeout(stubs[0], 100); - clock.setTimeout(stubs[1], 120); - - await expect(clock.tickAsync(120)).rejects.toThrow(); - - expect(stubs[0].called).toBeTruthy(); - expect(stubs[1].called).toBeTruthy(); - }); - - it('calls function with global object or null (strict mode) as this', async ({ clock }) => { - const stub = createStub().throws(); - clock.setTimeout(stub, 100); - - await expect(clock.tickAsync(100)).rejects.toThrow(); - - expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); - }); - - it('triggers in the order scheduled', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 13); - clock.setTimeout(spies[1], 11); - - await clock.tickAsync(15); - - expect(spies[1].calledBefore(spies[0])).toBeTruthy(); - }); - - it('creates updated Date while ticking', async ({ clock }) => { - const spy = createStub(); - - clock.setInterval(() => { - spy(new clock.Date().getTime()); - }, 10); - - await clock.tickAsync(100); - - expect(spy.callCount).toBe(10); - expect(spy.calledWith(10)).toBeTruthy(); - expect(spy.calledWith(20)).toBeTruthy(); - expect(spy.calledWith(30)).toBeTruthy(); - expect(spy.calledWith(40)).toBeTruthy(); - expect(spy.calledWith(50)).toBeTruthy(); - expect(spy.calledWith(60)).toBeTruthy(); - expect(spy.calledWith(70)).toBeTruthy(); - expect(spy.calledWith(80)).toBeTruthy(); - expect(spy.calledWith(90)).toBeTruthy(); - expect(spy.calledWith(100)).toBeTruthy(); + await expect(clock.tick(-500)).rejects.toThrow('Negative ticks are not supported'); }); it('creates updated Date while ticking promises', async ({ clock }) => { @@ -775,7 +571,7 @@ it.describe('tickAsync', () => { }); }, 10); - await clock.tickAsync(100); + await clock.tick(100); expect(spy.callCount).toBe(10); expect(spy.calledWith(10)).toBeTruthy(); @@ -790,36 +586,6 @@ it.describe('tickAsync', () => { expect(spy.calledWith(100)).toBeTruthy(); }); - it('fires timer in intervals of 13', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 13); - - await clock.tickAsync(500); - - expect(spy.callCount).toBe(38); - }); - - it('fires timers in correct order', async ({ clock }) => { - const spy13 = createStub(); - const spy10 = createStub(); - - clock.setInterval(() => { - spy13(new clock.Date().getTime()); - }, 13); - - clock.setInterval(() => { - spy10(new clock.Date().getTime()); - }, 10); - - await clock.tickAsync(500); - - expect(spy13.callCount).toBe(38); - expect(spy10.callCount).toBe(50); - - expect(spy13.calledWith(416)).toBeTruthy(); - expect(spy10.calledWith(320)).toBeTruthy(); - }); - it('fires promise timers in correct order', async ({ clock }) => { const spy13 = createStub(); const spy10 = createStub(); @@ -836,7 +602,7 @@ it.describe('tickAsync', () => { }); }, 10); - await clock.tickAsync(500); + await clock.tick(500); expect(spy13.callCount).toBe(38); expect(spy10.callCount).toBe(50); @@ -845,32 +611,6 @@ it.describe('tickAsync', () => { expect(spy10.calledWith(320)).toBeTruthy(); }); - it('triggers timeouts and intervals in the order scheduled', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setInterval(spies[0], 10); - clock.setTimeout(spies[1], 50); - - await clock.tickAsync(100); - - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - expect(spies[0].callCount).toBe(10); - expect(spies[1].callCount).toBe(1); - }); - - it('does not fire canceled intervals', async ({ clock }) => { - // eslint-disable-next-line prefer-const - let id; - const callback = createStub(() => { - if (callback.callCount === 3) - clock.clearInterval(id); - }); - - id = clock.setInterval(callback, 10); - await clock.tickAsync(100); - - expect(callback.callCount).toBe(3); - }); - it('does not fire intervals canceled in a promise', async ({ clock }) => { // ESLint fails to detect this correctly /* eslint-disable prefer-const */ @@ -884,86 +624,11 @@ it.describe('tickAsync', () => { }); id = clock.setInterval(callback, 10); - await clock.tickAsync(100); + await clock.tick(100); expect(callback.callCount).toBe(3); }); - it('passes 8 seconds', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 4000); - - await clock.tickAsync('08'); - - expect(spy.callCount).toBe(2); - }); - - it('passes 1 minute', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 6000); - - await clock.tickAsync('01:00'); - - expect(spy.callCount).toBe(10); - }); - - it('passes 2 hours, 34 minutes and 10 seconds', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - - await clock.tickAsync('02:34:10'); - - expect(spy.callCount).toBe(925); - }); - - it('throws for invalid format', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - await expect(clock.tickAsync('12:02:34:10')).rejects.toThrow(); - expect(spy.callCount).toBe(0); - }); - - it('throws for invalid minutes', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - - await expect(clock.tickAsync('67:10')).rejects.toThrow(); - - expect(spy.callCount).toBe(0); - }); - - it('throws for negative minutes', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - - await expect(clock.tickAsync('-7:10')).rejects.toThrow(); - - expect(spy.callCount).toBe(0); - }); - - it('treats missing argument as 0', async ({ clock }) => { - // @ts-expect-error - await clock.tickAsync(); - - expect(clock.now()).toBe(0); - }); - - it('fires nested setTimeout calls properly', async ({ clock }) => { - let i = 0; - const callback = () => { - ++i; - clock.setTimeout(() => { - callback(); - }, 100); - }; - - callback(); - - await clock.tickAsync(1000); - - expect(i).toBe(11); - }); - it('fires nested setTimeout calls in user-created promises properly', async ({ clock }) => { let i = 0; const callback = () => { @@ -979,39 +644,12 @@ it.describe('tickAsync', () => { callback(); - await clock.tickAsync(1000); - + // Clock API is async. + await new Promise(setImmediate); + await clock.tick(1000); expect(i).toBe(11); }); - it('does not silently catch errors', async ({ clock }) => { - const callback = () => { - throw new Error('oh no!'); - }; - - clock.setTimeout(callback, 1000); - - await expect(clock.tickAsync(1000)).rejects.toThrow(); - }); - - it('returns the current now value', async ({ clock }) => { - const value = await clock.tickAsync(200); - expect(clock.now()).toBe(value); - }); - - it('is not influenced by forward system clock changes', async ({ clock }) => { - const callback = () => { - clock.setSystemTime(new clock.Date().getTime() + 1000); - }; - const stub = createStub(); - clock.setTimeout(callback, 1000); - clock.setTimeout(stub, 2000); - await clock.tickAsync(1990); - expect(stub.callCount).toBe(0); - await clock.tickAsync(20); - expect(stub.callCount).toBe(1); - }); - it('is not influenced by forward system clock changes in promises', async ({ clock }) => { const callback = () => { void Promise.resolve().then(() => { @@ -1021,25 +659,9 @@ it.describe('tickAsync', () => { const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await clock.tickAsync(1990); + await clock.tick(1990); expect(stub.callCount).toBe(0); - await clock.tickAsync(20); - expect(stub.callCount).toBe(1); - }); - - it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => { - const callback = () => { - clock.setSystemTime(new clock.Date().getTime() + 1000); - throw new Error(); - }; - const stub = createStub(); - clock.setTimeout(callback, 1000); - clock.setTimeout(stub, 2000); - - await expect(clock.tickAsync(1990)).rejects.toThrow(); - - expect(stub.callCount).toBe(0); - await clock.tickAsync(20); + await clock.tick(20); expect(stub.callCount).toBe(1); }); @@ -1050,7 +672,7 @@ it.describe('tickAsync', () => { void Promise.resolve().then(spy); }, 100); - await clock.tickAsync(100); + await clock.tick(100); expect(spy.called).toBeTruthy(); }); @@ -1065,7 +687,7 @@ it.describe('tickAsync', () => { .then(spies[2]); }, 100); - await clock.tickAsync(100); + await clock.tick(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); @@ -1081,7 +703,7 @@ it.describe('tickAsync', () => { void Promise.resolve().then(spies[2]); }, 100); - await clock.tickAsync(100); + await clock.tick(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); @@ -1099,7 +721,7 @@ it.describe('tickAsync', () => { }); }, 100); - await clock.tickAsync(100); + await clock.tick(100); expect(spy.called).toBeTruthy(); }); @@ -1112,7 +734,7 @@ it.describe('tickAsync', () => { void Promise.resolve().then(spies[2]).catch(spies[3]); }, 100); - await clock.tickAsync(100); + await clock.tick(100); expect(spies[0].callCount).toBe(0); expect(spies[1].called).toBeTruthy(); @@ -1129,20 +751,20 @@ it.describe('tickAsync', () => { clock.setTimeout(spies[1], 200); - await clock.tickAsync(200); + await clock.tick(200); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); it('should settle local promises before calling timeouts', async ({ clock }) => { const spies = [createStub(), createStub()]; - void Promise.resolve().then(spies[0]); - clock.setTimeout(spies[1], 100); - await clock.tickAsync(100); + // Clock API is async. + await new Promise(setImmediate); + await clock.tick(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); @@ -1157,7 +779,9 @@ it.describe('tickAsync', () => { clock.setTimeout(spies[1], 100); - await clock.tickAsync(100); + // Clock API is async. + await new Promise(setImmediate); + await clock.tick(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); @@ -1168,7 +792,7 @@ it.describe('next', () => { const stub = createStub(); clock.setTimeout(stub, 100); - clock.next(); + await clock.next(); expect(stub.called).toBeTruthy(); }); @@ -1178,7 +802,7 @@ it.describe('next', () => { clock.setTimeout(spies[0], 100); clock.setTimeout(spies[1], 100); - clock.next(); + await clock.next(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeFalsy(); @@ -1191,25 +815,25 @@ it.describe('next', () => { clock.setTimeout(spies[2], 99); clock.setTimeout(spies[3], 100); - clock.next(); + await clock.next(); expect(spies[2].called).toBeTruthy(); expect(spies[0].called).toBeFalsy(); expect(spies[1].called).toBeFalsy(); expect(spies[3].called).toBeFalsy(); - clock.next(); + await clock.next(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeFalsy(); expect(spies[3].called).toBeFalsy(); - clock.next(); + await clock.next(); expect(spies[1].called).toBeTruthy(); expect(spies[3].called).toBeFalsy(); - clock.next(); + await clock.next(); expect(spies[3].called).toBeTruthy(); }); @@ -1227,40 +851,31 @@ it.describe('next', () => { clock.setTimeout(spies[0], 0); clock.setTimeout(spies[2], 10); - clock.next(); + await clock.next(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeFalsy(); - clock.next(); + await clock.next(); expect(spies[1].called).toBeTruthy(); - clock.next(); + await clock.next(); expect(spies[2].called).toBeTruthy(); }); it('throws exception thrown by timer', async ({ clock }) => { const stub = createStub().throws(); - clock.setTimeout(stub, 100); - - expect(() => { - clock.next(); - }).toThrow(); - + await expect(clock.next()).rejects.toThrow(); expect(stub.called).toBeTruthy(); }); it('calls function with global object or null (strict mode) as this', async ({ clock }) => { const stub = createStub().throws(); clock.setTimeout(stub, 100); - - expect(() => { - clock.next(); - }).toThrow(); - + await expect(clock.next()).rejects.toThrow(); expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); }); @@ -1269,8 +884,8 @@ it.describe('next', () => { clock.setTimeout(spies[0], 13); clock.setTimeout(spies[1], 11); - clock.next(); - clock.next(); + await clock.next(); + await clock.next(); expect(spies[1].calledBefore(spies[0])).toBeTruthy(); }); @@ -1282,16 +897,16 @@ it.describe('next', () => { spy(new clock.Date().getTime()); }, 10); - clock.next(); - clock.next(); - clock.next(); - clock.next(); - clock.next(); - clock.next(); - clock.next(); - clock.next(); - clock.next(); - clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); expect(spy.callCount).toBe(10); expect(spy.calledWith(10)).toBeTruthy(); @@ -1311,12 +926,12 @@ it.describe('next', () => { clock.setInterval(spies[0], 10); clock.setTimeout(spies[1], 50); - clock.next(); - clock.next(); - clock.next(); - clock.next(); - clock.next(); - clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); expect(spies[0].callCount).toBe(5); @@ -1333,197 +948,27 @@ it.describe('next', () => { }); id = clock.setInterval(callback, 10); - clock.next(); - clock.next(); - clock.next(); - clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); expect(callback.callCount).toBe(3); }); it('advances the clock based on when the timer was supposed to be called', async ({ clock }) => { clock.setTimeout(createStub(), 55); - clock.next(); + await clock.next(); expect(clock.now()).toBe(55); }); it('returns the current now value', async ({ clock }) => { clock.setTimeout(createStub(), 55); - const value = clock.next(); + const value = await clock.next(); expect(clock.now()).toBe(value); }); -}); - -it.describe('nextAsync', () => { - it('triggers the next timer', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub, 100); - - await clock.nextAsync(); - - expect(stub.called).toBeTruthy(); - }); - - it('does not trigger simultaneous timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 100); - clock.setTimeout(spies[1], 100); - - await clock.nextAsync(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - }); - - it('subsequent calls trigger simultaneous timers', async ({ clock }) => { - const spies = [createStub(), createStub(), createStub(), createStub()]; - clock.setTimeout(spies[0], 100); - clock.setTimeout(spies[1], 100); - clock.setTimeout(spies[2], 99); - clock.setTimeout(spies[3], 100); - - await clock.nextAsync(); - - expect(spies[2].called).toBeTruthy(); - expect(spies[0].called).toBeFalsy(); - expect(spies[1].called).toBeFalsy(); - expect(spies[3].called).toBeFalsy(); - - await clock.nextAsync(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - expect(spies[3].called).toBeFalsy(); - - await clock.nextAsync(); - - expect(spies[1].called).toBeTruthy(); - expect(spies[3].called).toBeFalsy(); - - await clock.nextAsync(); - - expect(spies[3].called).toBeTruthy(); - }); - - it('subsequent calls trigger simultaneous timers with zero callAt', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 0); - }), - createStub(), - createStub(), - ]; - - // First spy calls another setTimeout with delay=0 - clock.setTimeout(spies[0], 0); - clock.setTimeout(spies[2], 10); - - await clock.nextAsync(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - - await clock.nextAsync(); - - expect(spies[1].called).toBeTruthy(); - - await clock.nextAsync(); - - expect(spies[2].called).toBeTruthy(); - }); - - it('throws exception thrown by timer', async ({ clock }) => { - const stub = createStub().throws(); - clock.setTimeout(stub, 100); - await expect(clock.nextAsync()).rejects.toThrow(); - expect(stub.called).toBeTruthy(); - }); - - it('calls function with global object or null (strict mode) as this', async ({ clock }) => { - const stub = createStub().throws(); - clock.setTimeout(stub, 100); - await expect(clock.nextAsync()).rejects.toThrow(); - expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); - }); - - it('subsequent calls trigger in the order scheduled', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 13); - clock.setTimeout(spies[1], 11); - - await clock.nextAsync(); - await clock.nextAsync(); - - expect(spies[1].calledBefore(spies[0])).toBeTruthy(); - }); - - it('creates updated Date while ticking', async ({ clock }) => { - const spy = createStub(); - - clock.setInterval(() => { - spy(new clock.Date().getTime()); - }, 10); - - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - - expect(spy.callCount).toBe(10); - expect(spy.calledWith(10)).toBeTruthy(); - expect(spy.calledWith(20)).toBeTruthy(); - expect(spy.calledWith(30)).toBeTruthy(); - expect(spy.calledWith(40)).toBeTruthy(); - expect(spy.calledWith(50)).toBeTruthy(); - expect(spy.calledWith(60)).toBeTruthy(); - expect(spy.calledWith(70)).toBeTruthy(); - expect(spy.calledWith(80)).toBeTruthy(); - expect(spy.calledWith(90)).toBeTruthy(); - expect(spy.calledWith(100)).toBeTruthy(); - }); - - it('subsequent calls trigger timeouts and intervals in the order scheduled', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setInterval(spies[0], 10); - clock.setTimeout(spies[1], 50); - - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - expect(spies[0].callCount).toBe(5); - expect(spies[1].callCount).toBe(1); - }); - - it('does not fire canceled intervals', async ({ clock }) => { - // ESLint fails to detect this correctly - /* eslint-disable prefer-const */ - let id; - const callback = createStub(() => { - if (callback.callCount === 3) - clock.clearInterval(id); - }); - - id = clock.setInterval(callback, 10); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - - expect(callback.callCount).toBe(3); - }); it('does not fire intervals canceled in promises', async ({ clock }) => { // ESLint fails to detect this correctly @@ -1538,28 +983,14 @@ it.describe('nextAsync', () => { }); id = clock.setInterval(callback, 10); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); - await clock.nextAsync(); + await clock.next(); + await clock.next(); + await clock.next(); + await clock.next(); expect(callback.callCount).toBe(3); }); - it('advances the clock based on when the timer was supposed to be called', async ({ clock }) => { - clock.setTimeout(createStub(), 55); - await clock.nextAsync(); - - expect(clock.now()).toBe(55); - }); - - it('returns the current now value', async ({ clock }) => { - clock.setTimeout(createStub(), 55); - const value = await clock.nextAsync(); - - expect(clock.now()).toBe(value); - }); - it('should settle user-created promises', async ({ clock }) => { const spy = createStub(); @@ -1567,7 +998,7 @@ it.describe('nextAsync', () => { void Promise.resolve().then(spy); }, 55); - await clock.nextAsync(); + await clock.next(); expect(spy.called).toBeTruthy(); }); @@ -1583,27 +1014,26 @@ it.describe('nextAsync', () => { }); }, 55); - await clock.nextAsync(); + await clock.next(); expect(spy.called).toBeTruthy(); }); it('should settle local promises before firing timers', async ({ clock }) => { const spies = [createStub(), createStub()]; - void Promise.resolve().then(spies[0]); - clock.setTimeout(spies[1], 55); - await clock.nextAsync(); - + // Clock API is async. + await new Promise(setImmediate); + await clock.next(); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); }); it.describe('runAll', () => { it('if there are no timers just return', async ({ clock }) => { - clock.runAll(); + await clock.runAll(); }); it('runs all timers', async ({ clock }) => { @@ -1611,7 +1041,7 @@ it.describe('runAll', () => { clock.setTimeout(spies[0], 10); clock.setTimeout(spies[1], 50); - clock.runAll(); + await clock.runAll(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); @@ -1628,7 +1058,7 @@ it.describe('runAll', () => { // Spy calls another setTimeout clock.setTimeout(spies[0], 10); - clock.runAll(); + await clock.runAll(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); @@ -1639,7 +1069,7 @@ it.describe('runAll', () => { clock.setTimeout(recursiveCallback, 10); }; recursiveCallback(); - expect(() => clock.runAll()).toThrow(); + await expect(clock.runAll()).rejects.toThrow(); }); it('the loop limit can be set when creating a clock', async ({}) => { @@ -1647,7 +1077,7 @@ it.describe('runAll', () => { const spies = [createStub(), createStub()]; clock.setTimeout(spies[0], 10); clock.setTimeout(spies[1], 50); - expect(() => clock.runAll()).toThrow(); + await expect(clock.runAll()).rejects.toThrow(); }); it('the loop limit can be set when installing a clock', async ({ install }) => { @@ -1656,66 +1086,7 @@ it.describe('runAll', () => { setTimeout(spies[0], 10); setTimeout(spies[1], 50); - expect(() => clock.runAll()).toThrow(); - }); -}); - -it.describe('runAllAsync', () => { - it('if there are no timers just return', async ({ clock }) => { - await clock.runAllAsync(); - }); - - it('runs all timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - - await clock.runAllAsync(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('new timers added while running are also run', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 50); - }), - createStub(), - ]; - - // Spy calls another setTimeout - clock.setTimeout(spies[0], 10); - - await clock.runAllAsync(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('new timers added in promises while running are also run', async ({ clock }) => { - const spies = [ - createStub(() => { - void Promise.resolve().then(() => { - clock.setTimeout(spies[1], 50); - }); - }), - createStub(), - ]; - - // Spy calls another setTimeout - clock.setTimeout(spies[0], 10); - await clock.runAllAsync(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('throws before allowing infinite recursion', async ({ clock }) => { - const recursiveCallback = () => { - clock.setTimeout(recursiveCallback, 10); - }; - recursiveCallback(); - await expect(clock.runAllAsync()).rejects.toThrow(); + await expect(clock.runAll()).rejects.toThrow(); }); it('throws before allowing infinite recursion from promises', async ({ clock }) => { @@ -1725,23 +1096,10 @@ it.describe('runAllAsync', () => { }); }; recursiveCallback(); - await expect(clock.runAllAsync()).rejects.toThrow(); - }); - it('the loop limit can be set when creating a clock', async ({}) => { - const clock = createClock(0, 1); - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - await expect(clock.runAllAsync()).rejects.toThrow(); - }); - - it('the loop limit can be set when installing a clock', async ({ install }) => { - const clock = install({ loopLimit: 1 }); - const spies = [createStub(), createStub()]; - setTimeout(spies[0], 10); - setTimeout(spies[1], 50); - await expect(clock.runAllAsync()).rejects.toThrow(); + // Clock API is async. + await new Promise(setImmediate); + await expect(clock.runAll()).rejects.toThrow(); }); it('should settle user-created promises', async ({ clock }) => { @@ -1749,7 +1107,7 @@ it.describe('runAllAsync', () => { clock.setTimeout(() => { void Promise.resolve().then(spy); }, 55); - await clock.runAllAsync(); + await clock.runAll(); expect(spy.called).toBeTruthy(); }); @@ -1764,7 +1122,7 @@ it.describe('runAllAsync', () => { }); }, 55); - await clock.runAllAsync(); + await clock.runAll(); expect(spy.called).toBeTruthy(); }); @@ -1773,7 +1131,10 @@ it.describe('runAllAsync', () => { const spies = [createStub(), createStub()]; void Promise.resolve().then(spies[0]); clock.setTimeout(spies[1], 55); - await clock.runAllAsync(); + + // Clock API is async. + await new Promise(setImmediate); + await clock.runAll(); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); @@ -1783,14 +1144,14 @@ it.describe('runAllAsync', () => { void Promise.resolve().then(spies[0]); }, 55); clock.setTimeout(spies[1], 75); - await clock.runAllAsync(); + await clock.runAll(); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); }); it.describe('runToLast', () => { it('returns current time when there are no timers', async ({ clock }) => { - const time = clock.runToLast(); + const time = await clock.runToLast(); expect(time).toBe(0); }); @@ -1798,7 +1159,7 @@ it.describe('runToLast', () => { const spies = [createStub(), createStub()]; clock.setTimeout(spies[0], 10); clock.setTimeout(spies[1], 50); - clock.runToLast(); + await clock.runToLast(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); }); @@ -1807,7 +1168,7 @@ it.describe('runToLast', () => { const spies = [createStub(), createStub()]; clock.setTimeout(spies[0], 10); clock.setTimeout(spies[1], 50); - const time = clock.runToLast(); + const time = await clock.runToLast(); expect(time).toBe(50); }); @@ -1815,7 +1176,7 @@ it.describe('runToLast', () => { const spies = [createStub(), createStub()]; clock.setTimeout(spies[0], 10); clock.setTimeout(spies[1], 10); - clock.runToLast(); + await clock.runToLast(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); }); @@ -1830,7 +1191,7 @@ it.describe('runToLast', () => { // Spy calls another setTimeout clock.setTimeout(spies[0], 10); - clock.runToLast(); + await clock.runToLast(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeFalsy(); }); @@ -1847,7 +1208,7 @@ it.describe('runToLast', () => { clock.setTimeout(spies[0], 100); // Spy calls another setTimeout clock.setTimeout(spies[1], 10); - clock.runToLast(); + await clock.runToLast(); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); expect(spies[2].called).toBeTruthy(); @@ -1861,7 +1222,7 @@ it.describe('runToLast', () => { clock.setTimeout(recursiveCallback, 0); clock.setTimeout(spy, 100); - clock.runToLast(); + await clock.runToLast(); expect(spy.called).toBeTruthy(); }); @@ -1873,95 +1234,17 @@ it.describe('runToLast', () => { clock.setTimeout(cb, 50); }, 50); - clock.runToLast(); + await clock.runToLast(); expect(invocations).toBe(1); }); -}); - -it.describe('runToLastAsync', () => { - it('returns current time when there are no timers', async ({ clock }) => { - const time = await clock.runToLastAsync(); - expect(time).toBe(0); - }); - - it('runs all existing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - await clock.runToLastAsync(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('returns time of the last timer', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - const time = await clock.runToLastAsync(); - expect(time).toBe(50); - }); - - it('runs all existing timers when two timers are matched for being last', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 10); - await clock.runToLastAsync(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('new timers added with a call time later than the last existing timer are NOT run', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 50); - }), - createStub(), - ]; - - // Spy calls another setTimeout - clock.setTimeout(spies[0], 10); - await clock.runToLastAsync(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - }); - - it('new timers added with a call time earlier than the last existing timer are run', async ({ clock }) => { - const spies = [ - createStub(), - createStub(() => { - clock.setTimeout(spies[2], 50); - }), - createStub(), - ]; - - clock.setTimeout(spies[0], 100); - // Spy calls another setTimeout - clock.setTimeout(spies[1], 10); - await clock.runToLastAsync(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - expect(spies[2].called).toBeTruthy(); - }); - - it('new timers cannot cause an infinite loop', async ({ clock }) => { - const spy = createStub(); - const recursiveCallback = () => { - clock.setTimeout(recursiveCallback, 0); - }; - - clock.setTimeout(recursiveCallback, 0); - clock.setTimeout(spy, 100); - await clock.runToLastAsync(); - expect(spy.called).toBeTruthy(); - }); it('should settle user-created promises', async ({ clock }) => { const spy = createStub(); clock.setTimeout(() => { void Promise.resolve().then(spy); }, 55); - await clock.runToLastAsync(); + await clock.runToLast(); expect(spy.called).toBeTruthy(); }); @@ -1976,7 +1259,7 @@ it.describe('runToLastAsync', () => { }); }, 55); - await clock.runToLastAsync(); + await clock.runToLast(); expect(spy.called).toBeTruthy(); }); @@ -1984,7 +1267,10 @@ it.describe('runToLastAsync', () => { const spies = [createStub(), createStub()]; void Promise.resolve().then(spies[0]); clock.setTimeout(spies[1], 55); - await clock.runToLastAsync(); + + // Clock API is async. + await new Promise(setImmediate); + await clock.runToLast(); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); @@ -1994,7 +1280,7 @@ it.describe('runToLastAsync', () => { void Promise.resolve().then(spies[0]); }, 55); clock.setTimeout(spies[1], 75); - await clock.runToLastAsync(); + await clock.runToLast(); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); }); @@ -2004,7 +1290,7 @@ it.describe('clearTimeout', () => { const stub = createStub(); const id = clock.setTimeout(stub, 50); clock.clearTimeout(id); - await clock.tickAsync(50); + await clock.tick(50); expect(stub.called).toBeFalsy(); }); @@ -2012,7 +1298,7 @@ it.describe('clearTimeout', () => { const stub = createStub(); const id = clock.setInterval(stub, 50); clock.clearTimeout(id); - await clock.tickAsync(50); + await clock.tick(50); expect(stub.called).toBeFalsy(); }); @@ -2020,7 +1306,7 @@ it.describe('clearTimeout', () => { const stub = createStub(); const id = clock.setInterval(stub); clock.clearTimeout(id); - await clock.tickAsync(50); + await clock.tick(50); expect(stub.called).toBeFalsy(); }); @@ -2031,13 +1317,13 @@ it.describe('clearTimeout', () => { it.describe('reset', () => { it('resets to the time install with - issue #183', async ({ clock }) => { - clock.tick(100); + await clock.tick(100); clock.reset(); expect(clock.now()).toBe(0); }); it('resets hrTime - issue #206', async ({ clock }) => { - clock.tick(100); + await clock.tick(100); expect(clock.performance.now()).toEqual(100); clock.reset(); expect(clock.performance.now()).toEqual(0); @@ -2067,7 +1353,7 @@ it.describe('setInterval', () => { it('schedules recurring timeout', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - clock.tick(99); + await clock.tick(99); expect(stub.callCount).toBe(9); }); @@ -2075,23 +1361,23 @@ it.describe('setInterval', () => { it('is not influenced by forward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - clock.tick(11); + await clock.tick(11); expect(stub.callCount).toBe(1); clock.setSystemTime(new clock.Date().getTime() + 1000); - clock.tick(8); + await clock.tick(8); expect(stub.callCount).toBe(1); - clock.tick(3); + await clock.tick(3); expect(stub.callCount).toBe(2); }); it('is not influenced by backward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - clock.tick(5); + await clock.tick(5); clock.setSystemTime(new clock.Date().getTime() - 1000); - clock.tick(6); + await clock.tick(6); expect(stub.callCount).toBe(1); - clock.tick(10); + await clock.tick(10); expect(stub.callCount).toBe(2); }); @@ -2102,7 +1388,7 @@ it.describe('setInterval', () => { }); const id = clock.setInterval(stub, 10); - clock.tick(100); + await clock.tick(100); expect(stub.callCount).toBe(3); }); @@ -2110,7 +1396,7 @@ it.describe('setInterval', () => { it('passes setTimeout parameters', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 2, 'the first', 'the second'); - clock.tick(3); + await clock.tick(3); expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); }); }); @@ -2120,7 +1406,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setInterval(stub, 50); clock.clearInterval(id); - clock.tick(50); + await clock.tick(50); expect(stub.called).toBeFalsy(); }); @@ -2128,7 +1414,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setInterval(stub); clock.clearInterval(id); - clock.tick(50); + await clock.tick(50); expect(stub.called).toBeFalsy(); }); @@ -2136,7 +1422,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setTimeout(stub, 50); clock.clearInterval(id); - clock.tick(50); + await clock.tick(50); expect(stub.called).toBeFalsy(); }); @@ -2172,7 +1458,7 @@ it.describe('date', () => { it('listens to ticking clock', async ({ clock }) => { const date1 = new clock.Date(); - clock.tick(3); + await clock.tick(3); const date2 = new clock.Date(); expect(date2.getTime() - date1.getTime()).toBe(3); }); @@ -2279,79 +1565,79 @@ it.describe('date', () => { }); it.describe('stubTimers', () => { - it('returns clock object', ({ install }) => { + it('returns clock object', async ({ install }) => { const clock = install(); expect(clock).toEqual(expect.any(Object)); expect(clock.tick).toEqual(expect.any(Function)); }); - it('takes an object parameter', ({ install }) => { + it('takes an object parameter', async ({ install }) => { const clock = install({}); expect(clock).toEqual(expect.any(Object)); }); - it('sets initial timestamp', ({ install }) => { + it('sets initial timestamp', async ({ install }) => { const clock = install({ now: 1400 }); expect(clock.now()).toBe(1400); }); - it('replaces global setTimeout', ({ install }) => { + it('replaces global setTimeout', async ({ install }) => { const clock = install(); const stub = createStub(); setTimeout(stub, 1000); - clock.tick(1000); + await clock.tick(1000); expect(stub.called).toBeTruthy(); }); - it('global fake setTimeout should return id', ({ install }) => { + it('global fake setTimeout should return id', async ({ install }) => { install(); const stub = createStub(); const to = setTimeout(stub, 1000); expect(to).toEqual(expect.any(Number)); }); - it('replaces global clearTimeout', ({ install }) => { + it('replaces global clearTimeout', async ({ install }) => { const clock = install(); const stub = createStub(); clearTimeout(setTimeout(stub, 1000)); - clock.tick(1000); + await clock.tick(1000); expect(stub.called).toBeFalsy(); }); - it('replaces global setInterval', ({ install }) => { + it('replaces global setInterval', async ({ install }) => { const clock = install(); const stub = createStub(); setInterval(stub, 500); - clock.tick(1000); + await clock.tick(1000); expect(stub.callCount).toBe(2); }); - it('replaces global clearInterval', ({ install }) => { + it('replaces global clearInterval', async ({ install }) => { const clock = install(); const stub = createStub(); clearInterval(setInterval(stub, 500)); - clock.tick(1000); + await clock.tick(1000); expect(stub.called).toBeFalsy(); }); - it('replaces global performance.now', ({ install }) => { + it('replaces global performance.now', async ({ install }) => { const clock = install(); const prev = performance.now(); - clock.tick(1000); + await clock.tick(1000); const next = performance.now(); expect(next).toBe(1000); expect(prev).toBe(0); }); - it('uninstalls global performance.now', ({ install }) => { + it('uninstalls global performance.now', async ({ install }) => { const oldNow = performance.now; const clock = install(); expect(performance.now).toBe(clock.performance.now); @@ -2359,7 +1645,7 @@ it.describe('stubTimers', () => { expect(performance.now).toBe(oldNow); }); - it('should let performance.mark still be callable after install() (#136)', ({ install }) => { + it('should let performance.mark still be callable after install() (#136)', async ({ install }) => { it.skip(nodeMajorVersion < 20); install(); expect(() => { @@ -2367,7 +1653,7 @@ it.describe('stubTimers', () => { }).not.toThrow(); }); - it('should not alter the global performance properties and methods', ({ install }) => { + it('should not alter the global performance properties and methods', async ({ install }) => { it.skip(nodeMajorVersion < 20); (Performance.prototype as any).someFunc1 = () => {}; (Performance.prototype as any).someFunc2 = () => {}; @@ -2383,7 +1669,7 @@ it.describe('stubTimers', () => { delete (Performance.prototype as any).someFunc3; }); - it('should replace the getEntries, getEntriesByX methods with noops that return []', ({ install }) => { + it('should replace the getEntries, getEntriesByX methods with noops that return []', async ({ install }) => { it.skip(nodeMajorVersion < 20); const backupDescriptors = Object.getOwnPropertyDescriptors(Performance); @@ -2442,18 +1728,18 @@ it.describe('stubTimers', () => { expect(now.getTime()).toBe(0); }); - it(`fake Date constructor should mirror Date's properties`, ({ clock }) => { + it(`fake Date constructor should mirror Date's properties`, async ({ clock }) => { expect(Date).not.toBe(clock.Date); expect(Date.prototype).toEqual(clock.Date.prototype); }); - it('decide on Date.now support at call-time when supported', ({ install }) => { + it('decide on Date.now support at call-time when supported', async ({ install }) => { (Date.now as any) = () => {}; install({ now: 0 }); expect(Date.now).toEqual(expect.any(Function)); }); - it('mirrors custom Date properties', ({ install }) => { + it('mirrors custom Date properties', async ({ install }) => { const f = () => { return ''; }; @@ -2475,7 +1761,7 @@ it.describe('stubTimers', () => { expect(Date).not.toBe(originals.Date); }); - it('resets faked methods', ({ install }) => { + it('resets faked methods', async ({ install }) => { const { clock, originals } = rawInstall(globalThis, { now: 0, toFake: ['setTimeout', 'Date'], @@ -2574,30 +1860,30 @@ it.describe('requestAnimationFrame', () => { it('should run every 16ms', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - clock.tick(15); + await clock.tick(15); expect(stub.callCount).toBe(0); - clock.tick(1); + await clock.tick(1); expect(stub.callCount).toBe(1); }); it('should be called with performance.now() when available', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - clock.tick(20); + await clock.tick(20); expect(stub.calledWith(16)).toBeTruthy(); }); it('should be called with performance.now() even when performance unavailable', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - clock.tick(20); + await clock.tick(20); expect(stub.calledWith(16)).toBeTruthy(); }); it('should call callback once', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - clock.tick(32); + await clock.tick(32); expect(stub.callCount).toBe(1); }); @@ -2605,9 +1891,9 @@ it.describe('requestAnimationFrame', () => { const stub1 = createStub(); const stub2 = createStub(); clock.requestAnimationFrame(stub1); - clock.tick(5); + await clock.tick(5); clock.requestAnimationFrame(stub2); - clock.tick(11); + await clock.tick(11); expect(stub1.calledWith(16)).toBeTruthy(); expect(stub2.calledWith(16)).toBeTruthy(); }); @@ -2616,18 +1902,18 @@ it.describe('requestAnimationFrame', () => { const stub1 = createStub(); const stub2 = createStub(); clock.requestAnimationFrame(stub1); - clock.tick(57); + await clock.tick(57); clock.requestAnimationFrame(stub2); - clock.tick(10); + await clock.tick(10); expect(stub1.calledWith(16)).toBeTruthy(); expect(stub2.calledWith(64)).toBeTruthy(); }); - it('should schedule for next frame if on current frame', ({ clock }) => { + it('should schedule for next frame if on current frame', async ({ clock }) => { const stub = createStub(); - clock.tick(16); + await clock.tick(16); clock.requestAnimationFrame(stub); - clock.tick(16); + await clock.tick(16); expect(stub.calledWith(32)).toBeTruthy(); }); }); @@ -2637,7 +1923,7 @@ it.describe('cancelAnimationFrame', () => { const stub = createStub(); const id = clock.requestAnimationFrame(stub); clock.cancelAnimationFrame(id); - clock.tick(16); + await clock.tick(16); expect(stub.called).toBeFalsy(); }); @@ -2647,7 +1933,7 @@ it.describe('cancelAnimationFrame', () => { expect(() => { clock.cancelAnimationFrame(id); }).toThrow(); - clock.tick(50); + await clock.tick(50); expect(stub.called).toBeTruthy(); }); @@ -2657,7 +1943,7 @@ it.describe('cancelAnimationFrame', () => { expect(() => { clock.cancelAnimationFrame(id); }).toThrow(); - clock.tick(50); + await clock.tick(50); expect(stub.called).toBeTruthy(); }); @@ -2668,10 +1954,10 @@ it.describe('cancelAnimationFrame', () => { it.describe('runToFrame', () => { it('should tick next frame', async ({ clock }) => { - clock.runToFrame(); + await clock.runToFrame(); expect(clock.now()).toBe(16); - clock.tick(3); - clock.runToFrame(); + await clock.tick(3); + await clock.runToFrame(); expect(clock.now()).toBe(32); }); }); @@ -2680,7 +1966,7 @@ it.describe('jump', () => { it('ignores timers which wouldn\'t be run', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 1000); - clock.jump(500); + await clock.jump(500); expect(stub.called).toBeFalsy(); }); @@ -2689,7 +1975,7 @@ it.describe('jump', () => { clock.setTimeout(() => { stub(clock.Date.now()); }, 1000); - clock.jump(2000); + await clock.jump(2000); expect(stub.callCount).toBe(1); expect(stub.calledWith(2000)).toBeTruthy(); }); @@ -2702,7 +1988,7 @@ it.describe('jump', () => { clock.setTimeout(shortTimers[0], 250); clock.setInterval(shortTimers[1], 100); clock.requestAnimationFrame(shortTimers[2]); - clock.jump(1500); + await clock.jump(1500); for (const stub of longTimers) expect(stub.called).toBeFalsy(); for (const stub of shortTimers) @@ -2712,7 +1998,7 @@ it.describe('jump', () => { it('supports string time arguments', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100000); // 100000 = 1:40 - clock.jump('01:50'); + await clock.jump('01:50'); expect(stub.callCount).toBe(1); }); }); @@ -2724,7 +2010,7 @@ it.describe('performance.now()', () => { }); it('should run along with clock.tick', async ({ clock }) => { - clock.tick(5000); + await clock.tick(5000); const result = clock.performance.now(); expect(result).toBe(5000); }); @@ -2733,7 +2019,7 @@ it.describe('performance.now()', () => { for (let i = 0; i < 10; i++) { const next = clock.performance.now(); expect(next).toBe(1000 * i); - clock.tick(1000); + await clock.tick(1000); } }); @@ -2742,7 +2028,7 @@ it.describe('performance.now()', () => { const result = clock.performance.now(); expect(result).toBe(2500); }, 2500); - clock.tick(5000); + await clock.tick(5000); }); }); @@ -2768,7 +2054,7 @@ it.describe('requestIdleCallback', () => { it('runs after all timers', async ({ clock }) => { const stub = createStub(); clock.requestIdleCallback(stub); - clock.tick(1000); + await clock.tick(1000); expect(stub.called).toBeTruthy(); }); @@ -2777,7 +2063,7 @@ it.describe('requestIdleCallback', () => { clock.setTimeout(() => {}, 10); clock.setTimeout(() => {}, 30); clock.requestIdleCallback(stub, { timeout: 20 }); - clock.tick(20); + await clock.tick(20); expect(stub.called).toBeTruthy(); }); @@ -2785,7 +2071,7 @@ it.describe('requestIdleCallback', () => { const stub = createStub(); clock.setTimeout(() => {}, 30); clock.requestIdleCallback(stub); - clock.tick(35); + await clock.tick(35); expect(stub.called).toBeFalsy(); }); }); @@ -2795,7 +2081,7 @@ it.describe('cancelIdleCallback', () => { const stub = createStub(); const callbackId = clock.requestIdleCallback(stub, { timeout: 0 }); clock.cancelIdleCallback(callbackId); - clock.tick(0); + await clock.tick(0); expect(stub.called).toBeFalsy(); }); }); @@ -2813,14 +2099,14 @@ it.describe('loop limit stack trace', () => { }; recursiveCreateTimer(); - await clock.runAllAsync().catch(catchSpy); + await clock.runAll().catch(catchSpy); expect(catchSpy.callCount).toBe(1); const err = catchSpy.firstCall.args[0]; expect(err.message).toBe(expectedMessage); expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); }); - it('provides a stack trace for running all sync', ({ clock }) => { + it('provides a stack trace for running all sync', async ({ clock }) => { let caughtError = false; const recursiveCreateTimer = () => { clock.setTimeout(recursiveCreateTimer, 10); @@ -2828,7 +2114,7 @@ it.describe('loop limit stack trace', () => { recursiveCreateTimer(); try { - clock.runAll(); + await clock.runAll(); } catch (err) { caughtError = true; expect(err.message).toBe(expectedMessage); @@ -2846,14 +2132,14 @@ it.describe('loop limit stack trace', () => { }; recursiveCreateTimer(); - await clock.runAllAsync().catch(catchSpy); + await clock.runAll().catch(catchSpy); expect(catchSpy.callCount).toBe(1); const err = catchSpy.firstCall.args[0]; expect(err.message).toBe(expectedMessage); expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); }); - it('provides a stack trace for running all sync', ({ clock }) => { + it('provides a stack trace for running all sync', async ({ clock }) => { let caughtError = false; const recursiveCreateTimer = () => { clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); @@ -2861,7 +2147,7 @@ it.describe('loop limit stack trace', () => { recursiveCreateTimer(); try { - clock.runAll(); + await clock.runAll(); } catch (err) { caughtError = true; expect(err.message).toBe(expectedMessage); @@ -2879,14 +2165,14 @@ it.describe('loop limit stack trace', () => { }; recursiveCreateTimer(); - await clock.runAllAsync().catch(catchSpy); + await clock.runAll().catch(catchSpy); expect(catchSpy.callCount).toBe(1); const err = catchSpy.firstCall.args[0]; expect(err.message).toBe(expectedMessage); expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); }); - it('provides a stack trace for running all sync', ({ clock }) => { + it('provides a stack trace for running all sync', async ({ clock }) => { let caughtError = false; const recursiveCreateTimer = () => { clock.setInterval(recursiveCreateTimer, 10); @@ -2894,7 +2180,7 @@ it.describe('loop limit stack trace', () => { recursiveCreateTimer(); try { - clock.runAll(); + await clock.runAll(); } catch (err) { caughtError = true; expect(err.message).toBe(expectedMessage); @@ -2912,14 +2198,14 @@ it.describe('loop limit stack trace', () => { }; recursiveCreateTimer(); - await clock.runAllAsync().catch(catchSpy); + await clock.runAll().catch(catchSpy); expect(catchSpy.callCount).toBe(1); const err = catchSpy.firstCall.args[0]; expect(err.message).toBe(expectedMessage); expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); }); - it('provides a stack trace for running all sync', ({ clock }) => { + it('provides a stack trace for running all sync', async ({ clock }) => { let caughtError = false; const recursiveCreateTimer = () => { clock.requestAnimationFrame(recursiveCreateTimer); @@ -2927,7 +2213,7 @@ it.describe('loop limit stack trace', () => { recursiveCreateTimer(); try { - clock.runAll(); + await clock.runAll(); } catch (err) { caughtError = true; expect(err.message).toBe(expectedMessage); From 05e10a8d0f9696eabac2ca7935039b70121beb84 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 7 Jun 2024 00:22:08 -0700 Subject: [PATCH 034/144] feat(firefox): roll to r1453 (#31199) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index db28f63c8f..cb4332ede1 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,7 +15,7 @@ }, { "name": "firefox", - "revision": "1452", + "revision": "1453", "installByDefault": true, "browserVersion": "126.0" }, From 43d6d012d4394ad0853a91e14369d26945220239 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 7 Jun 2024 00:22:22 -0700 Subject: [PATCH 035/144] feat(firefox-beta): roll to r1453 (#31197) --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index cb4332ede1..fc053c9b7a 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -21,7 +21,7 @@ }, { "name": "firefox-beta", - "revision": "1452", + "revision": "1453", "installByDefault": false, "browserVersion": "127.0b3" }, From e280d0bd35584516b49023812cfaa923ac2faa49 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sun, 9 Jun 2024 14:50:50 -0700 Subject: [PATCH 036/144] chore(clock): split wall and monotonic time (#31198) --- .../src/server/injected/clock.ts | 168 ++++++++---------- tests/library/clock.spec.ts | 23 +-- tests/page/page-clock.spec.ts | 4 +- 3 files changed, 86 insertions(+), 109 deletions(-) diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index af846b42c8..508d7a59e5 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -44,7 +44,7 @@ enum TimerType { type Timer = { type: TimerType; func: TimerHandler; - args: () => any[]; + args: any[]; delay: number; callAt: number; createdAt: number; @@ -58,10 +58,9 @@ interface Embedder { } export class ClockController { - readonly start: number; - private _now: number; + readonly timeOrigin: number; + private _now: { time: number, ticks: number, timeFrozen: boolean }; private _loopLimit: number; - private _adjustedSystemTime = 0; private _duringTick = false; private _timers = new Map(); private _uniqueTimerId = idCounterStart; @@ -70,8 +69,8 @@ export class ClockController { constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) { const start = Math.floor(getEpoch(startDate)); - this.start = start; - this._now = start; + this.timeOrigin = start; + this._now = { time: start, ticks: 0, timeFrozen: false }; this._embedder = embedder; this._loopLimit = loopLimit; } @@ -82,11 +81,22 @@ export class ClockController { } now(): number { - return this._now; + return this._now.time; + } + + setTime(now: Date | number, options: { freeze?: boolean } = {}) { + this._now.time = getEpoch(now); + this._now.timeFrozen = !!options.freeze; } performanceNow(): DOMHighResTimeStamp { - return this._now - this._adjustedSystemTime - this.start; + return this._now.ticks; + } + + private _advanceNow(toTicks: number) { + if (!this._now.timeFrozen) + this._now.time += toTicks - this._now.ticks; + this._now.ticks = toTicks; } private async _doTick(msFloat: number): Promise { @@ -94,119 +104,72 @@ export class ClockController { throw new TypeError('Negative ticks are not supported'); const ms = Math.floor(msFloat); - let tickTo = this._now + ms; - let tickFrom = this._now; - let previous = this._now; + const tickTo = this._now.ticks + ms; + let tickFrom = this._now.ticks; + let previous = this._now.ticks; let firstException: Error | undefined; - this._duringTick = true; - - // perform each timer in the requested range let timer = this._firstTimerInRange(tickFrom, tickTo); while (timer && tickFrom <= tickTo) { tickFrom = timer.callAt; - this._now = timer.callAt; - const oldNow = this._now; - try { - this._callTimer(timer); - await new Promise(f => this._embedder.postTask(f)); - } catch (e) { - firstException = firstException || e; - } - - // compensate for any setSystemTime() call during timer callback - if (oldNow !== this._now) { - tickFrom += this._now - oldNow; - tickTo += this._now - oldNow; - previous += this._now - oldNow; - } - + const error = await this._callTimer(timer).catch(e => e); + firstException = firstException || error; timer = this._firstTimerInRange(previous, tickTo); previous = tickFrom; } - this._duringTick = false; - this._now = tickTo; + this._advanceNow(tickTo); if (firstException) throw firstException; - return this._now; + return this._now.ticks; } async recordTick(tickValue: string | number) { const msFloat = parseTime(tickValue); - this._now += msFloat; + this._advanceNow(this._now.ticks + msFloat); } async tick(tickValue: string | number): Promise { return await this._doTick(parseTime(tickValue)); } - async next() { + async next(): Promise { const timer = this._firstTimer(); if (!timer) - return this._now; - - let err: Error | undefined; - this._duringTick = true; - this._now = timer.callAt; - try { - this._callTimer(timer); - await new Promise(f => this._embedder.postTask(f)); - } catch (e) { - err = e; - } - this._duringTick = false; - - if (err) - throw err; - return this._now; + return this._now.ticks; + await this._callTimer(timer); + return this._now.ticks; } - async runToFrame() { + async runToFrame(): Promise { return this.tick(this.getTimeToNextFrame()); } - async runAll() { + async runAll(): Promise { for (let i = 0; i < this._loopLimit; i++) { const numTimers = this._timers.size; if (numTimers === 0) - return this._now; + return this._now.ticks; await this.next(); } const excessJob = this._firstTimer(); if (!excessJob) - return; + return this._now.ticks; throw this._getInfiniteLoopError(excessJob); } - async runToLast() { + async runToLast(): Promise { const timer = this._lastTimer(); if (!timer) - return this._now; - return await this.tick(timer.callAt - this._now); + return this._now.ticks; + return await this.tick(timer.callAt - this._now.ticks); } reset() { this._timers.clear(); - this._now = this.start; - } - - setSystemTime(systemTime: Date | number) { - // determine time difference - const newNow = getEpoch(systemTime); - const difference = newNow - this._now; - - this._adjustedSystemTime = this._adjustedSystemTime + difference; - // update 'system clock' - this._now = newNow; - - // update timers and intervals to keep them stable - for (const timer of this._timers.values()) { - timer.createdAt += difference; - timer.callAt += difference; - } + this._now = { time: this.timeOrigin, ticks: 0, timeFrozen: false }; } async jump(tickValue: string | number): Promise { @@ -214,13 +177,13 @@ export class ClockController { const ms = Math.floor(msFloat); for (const timer of this._timers.values()) { - if (this._now + ms > timer.callAt) - timer.callAt = this._now + ms; + if (this._now.ticks + ms > timer.callAt) + timer.callAt = this._now.ticks + ms; } return await this.tick(ms); } - addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: () => any[] }): number { + addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { if (options.func === undefined) throw new Error('Callback must be provided to timer calls'); @@ -233,10 +196,10 @@ export class ClockController { const timer: Timer = { type: options.type, func: options.func, - args: options.args || (() => []), + args: options.args || [], delay, - callAt: this._now + (delay || (this._duringTick ? 1 : 0)), - createdAt: this._now, + callAt: this._now.ticks + (delay || (this._duringTick ? 1 : 0)), + createdAt: this._now.ticks, id: this._uniqueTimerId++, error: new Error(), }; @@ -278,12 +241,32 @@ export class ClockController { return lastTimer; } - private _callTimer(timer: Timer) { + private async _callTimer(timer: Timer) { + this._advanceNow(timer.callAt); + if (timer.type === TimerType.Interval) this._timers.get(timer.id)!.callAt += timer.delay; else this._timers.delete(timer.id); - callFunction(timer.func, timer.args()); + + this._duringTick = true; + try { + if (typeof timer.func !== 'function') { + (() => { eval(timer.func); })(); + return; + } + + let args = timer.args; + if (timer.type === TimerType.AnimationFrame) + args = [this._now.ticks]; + else if (timer.type === TimerType.IdleCallback) + args = [{ didTimeout: false, timeRemaining: () => 0 }]; + + timer.func.apply(null, args); + await new Promise(f => this._embedder.postTask(f)); + } finally { + this._duringTick = false; + } } private _getInfiniteLoopError(job: Timer) { @@ -336,7 +319,7 @@ export class ClockController { } getTimeToNextFrame() { - return 16 - ((this._now - this.start) % 16); + return 16 - this._now.ticks % 16; } clearTimer(timerId: number, type: TimerType) { @@ -375,7 +358,7 @@ export class ClockController { advanceAutomatically(advanceTimeDelta: number = 20): () => void { return this._embedder.postTaskPeriodically( - () => this.tick(advanceTimeDelta!), + () => this._doTick(advanceTimeDelta!), advanceTimeDelta, ); } @@ -556,13 +539,6 @@ function compareTimers(a: Timer, b: Timer) { // As timer ids are unique, no fallback `0` is necessary } -function callFunction(func: TimerHandler, args: any[]) { - if (typeof func === 'function') - func.apply(null, args); - else - (() => { eval(func); })(); -} - const maxTimeout = Math.pow(2, 31) - 1; // see https://heycam.github.io/webidl/#abstract-opdef-converttoint const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs @@ -605,7 +581,7 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod return clock.addTimer({ type: TimerType.Timeout, func, - args: () => args, + args, delay }); }, @@ -618,7 +594,7 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod return clock.addTimer({ type: TimerType.Interval, func, - args: () => args, + args, delay, }); }, @@ -631,7 +607,6 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod type: TimerType.AnimationFrame, func: callback, delay: clock.getTimeToNextFrame(), - args: () => [clock.performanceNow()], }); }, cancelAnimationFrame: (timerId: number): void => { @@ -646,7 +621,6 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod return clock.addTimer({ type: TimerType.IdleCallback, func: callback, - args: () => [], delay: options?.timeout ? Math.min(options?.timeout, timeToNextIdlePeriod) : timeToNextIdlePeriod, }); }, @@ -670,7 +644,7 @@ function getClearHandler(type: TimerType) { function fakePerformance(clock: ClockController, performance: Performance): Performance { const result: any = { now: () => clock.performanceNow(), - timeOrigin: clock.start, + timeOrigin: clock.timeOrigin, }; // eslint-disable-next-line no-proto for (const key of Object.keys((performance as any).__proto__)) { diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index cf27d2744b..554b49d1a7 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -156,7 +156,7 @@ it.describe('setTimeout', () => { const stub = createStub(); clock.setTimeout(stub, 5000); await clock.tick(1000); - clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.setTime(new clock.Date().getTime() + 1000); await clock.tick(3990); expect(stub.callCount).toBe(0); await clock.tick(20); @@ -167,7 +167,7 @@ it.describe('setTimeout', () => { const stub = createStub(); clock.setTimeout(stub, 5000); await clock.tick(1000); - clock.setSystemTime(new clock.Date().getTime() - 1000); + clock.setTime(new clock.Date().getTime() - 1000); await clock.tick(3990); expect(stub.callCount).toBe(0); await clock.tick(20); @@ -502,7 +502,7 @@ it.describe('tick', () => { it('is not influenced by forward system clock changes', async ({ clock }) => { const callback = () => { - clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.setTime(new clock.Date().getTime() + 1000); }; const stub = createStub(); clock.setTimeout(callback, 1000); @@ -515,7 +515,7 @@ it.describe('tick', () => { it('is not influenced by forward system clock changes 2', async ({ clock }) => { const callback = () => { - clock.setSystemTime(new clock.Date().getTime() - 1000); + clock.setTime(new clock.Date().getTime() - 1000); }; const stub = createStub(); clock.setTimeout(callback, 1000); @@ -528,7 +528,7 @@ it.describe('tick', () => { it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => { const callback = () => { - clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.setTime(new clock.Date().getTime() + 1000); throw new Error(); }; const stub = createStub(); @@ -544,7 +544,7 @@ it.describe('tick', () => { it('is not influenced by forward system clock changes when an error is thrown 2', async ({ clock }) => { const callback = () => { - clock.setSystemTime(new clock.Date().getTime() - 1000); + clock.setTime(new clock.Date().getTime() - 1000); throw new Error(); }; const stub = createStub(); @@ -653,7 +653,7 @@ it.describe('tick', () => { it('is not influenced by forward system clock changes in promises', async ({ clock }) => { const callback = () => { void Promise.resolve().then(() => { - clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.setTime(new clock.Date().getTime() + 1000); }); }; const stub = createStub(); @@ -1363,7 +1363,7 @@ it.describe('setInterval', () => { clock.setInterval(stub, 10); await clock.tick(11); expect(stub.callCount).toBe(1); - clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.setTime(new clock.Date().getTime() + 1000); await clock.tick(8); expect(stub.callCount).toBe(1); await clock.tick(3); @@ -1374,7 +1374,7 @@ it.describe('setInterval', () => { const stub = createStub(); clock.setInterval(stub, 10); await clock.tick(5); - clock.setSystemTime(new clock.Date().getTime() - 1000); + clock.setTime(new clock.Date().getTime() - 1000); await clock.tick(6); expect(stub.callCount).toBe(1); await clock.tick(10); @@ -1465,7 +1465,7 @@ it.describe('date', () => { it('listens to system clock changes', async ({ clock }) => { const date1 = new clock.Date(); - clock.setSystemTime(date1.getTime() + 1000); + clock.setTime(date1.getTime() + 1000); const date2 = new clock.Date(); expect(date2.getTime() - date1.getTime()).toBe(1000); }); @@ -2056,6 +2056,9 @@ it.describe('requestIdleCallback', () => { clock.requestIdleCallback(stub); await clock.tick(1000); expect(stub.called).toBeTruthy(); + const idleCallbackArg = stub.firstCall.args[0]; + expect(idleCallbackArg.didTimeout).toBeFalsy(); + expect(idleCallbackArg.timeRemaining()).toBe(0); }); it('runs no later than timeout option even if there are any timers', async ({ clock }) => { diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index 649ab250bd..4a6ca00896 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -587,8 +587,8 @@ it.describe('popup', () => { it('should tick before popup', async ({ page, browserName }) => { const now = new Date('2015-09-25'); await page.clock.installFakeTimers(now); - const newNow = await page.clock.runFor(1000); - expect(newNow).toBe(now.getTime() + 1000); + const ticks = await page.clock.runFor(1000); + expect(ticks).toBe(1000); const [popup] = await Promise.all([ page.waitForEvent('popup'), From abaddc01c9efc19c8163198a65b005452dfa795b Mon Sep 17 00:00:00 2001 From: Lee Byonghun Date: Mon, 10 Jun 2024 18:27:54 +0900 Subject: [PATCH 037/144] fix: throw error when workers option is not number or percentage (#31210) --- packages/playwright/src/common/config.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 015dbc1e71..ac7b313820 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -107,7 +107,7 @@ export class FullConfigInternal { const cpus = os.cpus().length; this.config.workers = Math.max(1, Math.floor(cpus * (parseInt(workers, 10) / 100))); } else { - this.config.workers = parseInt(workers, 10); + this.config.workers = parseWorkers(workers); } } else { this.config.workers = workers; @@ -223,6 +223,14 @@ function resolveReporters(reporters: Config['reporter'], rootDir: string): Repor }); } +function parseWorkers(workers: string) { + const parsedWorkers = parseInt(workers, 10); + if (isNaN(parsedWorkers)) + throw new Error(`Workers ${workers} must be a number or percentage.`); + + return parsedWorkers; +} + function resolveProjectDependencies(projects: FullProjectInternal[]) { const teardownSet = new Set(); for (const project of projects) { From 701a405bdfd8e9bfe61dd4ee6b0b91e6fff6c89e Mon Sep 17 00:00:00 2001 From: Carter Sande Date: Mon, 10 Jun 2024 03:44:52 -0600 Subject: [PATCH 038/144] fix(trace-viewer): Rewrite file URIs in snapshots, like blob URIs. (#31113) This allows snapshots of file:/// pages with external stylesheets, images, etc to be rendered correctly in the trace viewer. (Otherwise, it tries to request the file:/// URIs directly and the requests get blocked by the browser.) Fixes #31112. --- packages/trace-viewer/src/snapshotRenderer.ts | 16 ++++++++++++---- tests/library/trace-viewer.spec.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/trace-viewer/src/snapshotRenderer.ts b/packages/trace-viewer/src/snapshotRenderer.ts index b98b359f76..9092d97e8c 100644 --- a/packages/trace-viewer/src/snapshotRenderer.ts +++ b/packages/trace-viewer/src/snapshotRenderer.ts @@ -401,13 +401,20 @@ export function rewriteURLForCustomProtocol(href: string): string { // Pass through if possible. const isBlob = url.protocol === 'blob:'; - if (!isBlob && schemas.includes(url.protocol)) + const isFile = url.protocol === 'file:'; + if (!isBlob && !isFile && schemas.includes(url.protocol)) return href; - // Rewrite blob and custom schemas. + // Rewrite blob, file and custom schemas. const prefix = 'pw-' + url.protocol.slice(0, url.protocol.length - 1); - url.protocol = 'https:'; + if (!isFile) + url.protocol = 'https:'; url.hostname = url.hostname ? `${prefix}--${url.hostname}` : prefix; + if (isFile) { + // File URIs can only have their protocol changed after the hostname + // is set. (For all other URIs, we must set the protocol first.) + url.protocol = 'https:'; + } return url.toString(); } catch { return href; @@ -423,7 +430,8 @@ const urlInCSSRegex = /url\(['"]?([\w-]+:)\/\//ig; function rewriteURLsInStyleSheetForCustomProtocol(text: string): string { return text.replace(urlInCSSRegex, (match: string, protocol: string) => { const isBlob = protocol === 'blob:'; - if (!isBlob && schemas.includes(protocol)) + const isFile = protocol === 'file:'; + if (!isBlob && !isFile && schemas.includes(protocol)) return match; return match.replace(protocol + '//', `https://pw-${protocol.slice(0, -1)}--`); }); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index e0c5055692..c2634df148 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -18,6 +18,7 @@ import type { TraceViewerFixtures } from '../config/traceViewerFixtures'; import { traceViewerFixtures } from '../config/traceViewerFixtures'; import fs from 'fs'; import path from 'path'; +import { pathToFileURL } from 'url'; import { expect, playwrightTest } from '../config/browserTest'; import type { FrameLocator } from '@playwright/test'; @@ -558,6 +559,17 @@ test('should handle src=blob', async ({ page, server, runAndTrace, browserName } expect(size).toBe(10); }); +test('should handle file URIs', async ({ page, runAndTrace, browserName }) => { + test.skip(browserName !== 'chromium'); + + const traceViewer = await runAndTrace(async () => { + await page.goto(pathToFileURL(path.join(__dirname, '..', 'assets', 'one-style.html')).href); + }); + + const frame = await traceViewer.snapshotFrame('goto'); + await expect(frame.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); +}); + test('should preserve currentSrc', async ({ browser, server, showTraceViewer }) => { const traceFile = test.info().outputPath('trace.zip'); const page = await browser.newPage({ deviceScaleFactor: 3 }); From 185cc43dbf8dee8f31e10205c136c66dc4efb6a0 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 10 Jun 2024 19:21:02 +0200 Subject: [PATCH 039/144] devops: fix merge reports on PRs (#31232) --- .github/actions/run-test/action.yml | 2 +- tests/electron/playwright.config.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/actions/run-test/action.yml b/.github/actions/run-test/action.yml index dcf8168367..c193515c45 100644 --- a/.github/actions/run-test/action.yml +++ b/.github/actions/run-test/action.yml @@ -86,7 +86,7 @@ runs: shell: bash - name: Upload blob report # We only merge reports for PRs as per .github/workflows/create_test_report.yml. - if: ${{ !cancelled() && github.event.workflow_run.event == 'pull_request' }} + if: ${{ !cancelled() && github.event_name == 'pull_request' }} uses: ./.github/actions/upload-blob-report with: report_dir: blob-report diff --git a/tests/electron/playwright.config.ts b/tests/electron/playwright.config.ts index b630b38e56..a16b3b480f 100644 --- a/tests/electron/playwright.config.ts +++ b/tests/electron/playwright.config.ts @@ -35,7 +35,8 @@ const config: Config = { reporter: process.env.CI ? [ ['dot'], ['json', { outputFile: path.join(outputDir, 'report.json') }], - ['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }], + // Needed since tests/electron/package.json exists which would otherwise be picked up as tests/electron/ (outputDir) + ['blob', { fileName: path.join(__dirname, '../../blob-report/', `${process.env.PWTEST_BOT_NAME}.zip`) }], ] : 'line', projects: [], globalSetup: './globalSetup.ts' From 98637ea5b52f562b6958867a18add47e0d9fe7e8 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 10 Jun 2024 19:28:29 +0200 Subject: [PATCH 040/144] chore: update socks dependencies (fix npm audit for 'ip' package) (#31231) Fixes https://github.com/microsoft/playwright/security/dependabot/76 Ran `npm audit fix` locally. --- .../playwright-core/ThirdPartyNotices.txt | 193 +++++++++--------- .../bundles/utils/package-lock.json | 65 ++++-- 2 files changed, 147 insertions(+), 111 deletions(-) diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 6480e85f29..f0b678af04 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -23,10 +23,11 @@ This project incorporates components from the projects listed below. The origina - get-stream@5.2.0 (https://github.com/sindresorhus/get-stream) - graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs) - https-proxy-agent@5.0.0 (https://github.com/TooTallNate/node-https-proxy-agent) -- ip@2.0.1 (https://github.com/indutny/node-ip) +- ip-address@9.0.5 (https://github.com/beaugunderson/ip-address) - is-docker@2.2.1 (https://github.com/sindresorhus/is-docker) - is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl) - jpeg-js@0.4.4 (https://github.com/eugeneware/jpeg-js) +- jsbn@1.1.0 (https://github.com/andyperlitch/jsbn) - mime@3.0.0 (https://github.com/broofa/mime) - minimatch@3.1.2 (https://github.com/isaacs/minimatch) - ms@2.1.2 (https://github.com/zeit/ms) @@ -41,7 +42,8 @@ This project incorporates components from the projects listed below. The origina - signal-exit@3.0.7 (https://github.com/tapjs/signal-exit) - smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer) - socks-proxy-agent@6.1.1 (https://github.com/TooTallNate/node-socks-proxy-agent) -- socks@2.7.0 (https://github.com/JoshGlazebrook/socks) +- socks@2.8.3 (https://github.com/JoshGlazebrook/socks) +- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js) - stack-utils@2.0.5 (https://github.com/tapjs/stack-utils) - wrappy@1.0.2 (https://github.com/npm/wrappy) - ws@8.4.2 (https://github.com/websockets/ws) @@ -740,100 +742,29 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF https-proxy-agent@5.0.0 AND INFORMATION -%% ip@2.0.1 NOTICES AND INFORMATION BEGIN HERE +%% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE ========================================= -# IP -[![](https://badge.fury.io/js/ip.svg)](https://www.npmjs.com/package/ip) +Copyright (C) 2011 by Beau Gunderson -IP address utilities for node.js +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -## Installation +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -### npm -```shell -npm install ip -``` - -### git - -```shell -git clone https://github.com/indutny/node-ip.git -``` - -## Usage -Get your ip address, compare ip addresses, validate ip addresses, etc. - -```js -var ip = require('ip'); - -ip.address() // my ip address -ip.isEqual('::1', '::0:1'); // true -ip.toBuffer('127.0.0.1') // Buffer([127, 0, 0, 1]) -ip.toString(new Buffer([127, 0, 0, 1])) // 127.0.0.1 -ip.fromPrefixLen(24) // 255.255.255.0 -ip.mask('192.168.1.134', '255.255.255.0') // 192.168.1.0 -ip.cidr('192.168.1.134/26') // 192.168.1.128 -ip.not('255.255.255.0') // 0.0.0.255 -ip.or('192.168.1.134', '0.0.0.255') // 192.168.1.255 -ip.isPrivate('127.0.0.1') // true -ip.isV4Format('127.0.0.1'); // true -ip.isV6Format('::ffff:127.0.0.1'); // true - -// operate on buffers in-place -var buf = new Buffer(128); -var offset = 64; -ip.toBuffer('127.0.0.1', buf, offset); // [127, 0, 0, 1] at offset 64 -ip.toString(buf, offset, 4); // '127.0.0.1' - -// subnet information -ip.subnet('192.168.1.134', '255.255.255.192') -// { networkAddress: '192.168.1.128', -// firstAddress: '192.168.1.129', -// lastAddress: '192.168.1.190', -// broadcastAddress: '192.168.1.191', -// subnetMask: '255.255.255.192', -// subnetMaskLength: 26, -// numHosts: 62, -// length: 64, -// contains: function(addr){...} } -ip.cidrSubnet('192.168.1.134/26') -// Same as previous. - -// range checking -ip.cidrSubnet('192.168.1.134/26').contains('192.168.1.190') // true - - -// ipv4 long conversion -ip.toLong('127.0.0.1'); // 2130706433 -ip.fromLong(2130706433); // '127.0.0.1' -``` - -### License - -This software is licensed under the MIT License. - -Copyright Fedor Indutny, 2012. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the -following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ========================================= -END OF ip@2.0.1 AND INFORMATION +END OF ip-address@9.0.5 AND INFORMATION %% is-docker@2.2.1 NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -893,6 +824,51 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ========================================= END OF jpeg-js@0.4.4 AND INFORMATION +%% jsbn@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Licensing +--------- + +This software is covered under the following copyright: + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + +Address all questions regarding this license to: + + Tom Wu + tjw@cs.Stanford.EDU +========================================= +END OF jsbn@1.1.0 AND INFORMATION + %% mime@3.0.0 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -1359,7 +1335,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF socks-proxy-agent@6.1.1 AND INFORMATION -%% socks@2.7.0 NOTICES AND INFORMATION BEGIN HERE +%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -1382,7 +1358,36 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF socks@2.7.0 AND INFORMATION +END OF socks@2.8.3 AND INFORMATION + +%% sprintf-js@1.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2007-present, Alexandru MΔƒrΔƒΘ™teanu +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this software nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF sprintf-js@1.1.3 AND INFORMATION %% stack-utils@2.0.5 NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -1508,6 +1513,6 @@ END OF yazl@2.5.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 43 +Total Packages: 45 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index dc46c9d6bd..df01638322 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -223,10 +223,17 @@ "node": ">= 6" } }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } }, "node_modules/is-docker": { "version": "2.2.1", @@ -258,6 +265,11 @@ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==" }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -345,15 +357,15 @@ } }, "node_modules/socks": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", - "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -370,6 +382,11 @@ "node": ">= 10" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, "node_modules/stack-utils": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", @@ -562,10 +579,14 @@ "debug": "4" } }, - "ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + } }, "is-docker": { "version": "2.2.1", @@ -585,6 +606,11 @@ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==" }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, "mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -644,11 +670,11 @@ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, "socks": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", - "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "requires": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, @@ -662,6 +688,11 @@ "socks": "^2.6.1" } }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, "stack-utils": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", From b60f99db36728880e6dc0fbd210d52f6ab824b47 Mon Sep 17 00:00:00 2001 From: Matthijs Kok Date: Tue, 11 Jun 2024 01:22:54 +0200 Subject: [PATCH 041/144] docs(class-testproject): fix 'use' type reference (#31200) --- docs/src/test-api/class-testproject.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test-api/class-testproject.md b/docs/src/test-api/class-testproject.md index a15a836010..26b8a954d1 100644 --- a/docs/src/test-api/class-testproject.md +++ b/docs/src/test-api/class-testproject.md @@ -365,7 +365,7 @@ Use [`property: TestConfig.timeout`] to change this option for all projects. ## property: TestProject.use * since: v1.10 -- type: <[Fixtures]> +- type: ?<[TestOptions]> Options for all tests in this project, for example [`property: TestOptions.browserName`]. Learn more about [configuration](../test-configuration.md) and see [available options][TestOptions]. From d9ac51bf870b72499e60be9aded6fe422e606ec8 Mon Sep 17 00:00:00 2001 From: Sander Date: Tue, 11 Jun 2024 02:20:51 +0200 Subject: [PATCH 042/144] docs(ct): update testing library migration guide (#31222) --- docs/src/testing-library-js.md | 57 +++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/docs/src/testing-library-js.md b/docs/src/testing-library-js.md index a5b4350d98..d7f7c05d82 100644 --- a/docs/src/testing-library-js.md +++ b/docs/src/testing-library-js.md @@ -13,20 +13,25 @@ If you use DOM Testing Library in the browser (for example, you bundle end-to-en ## Cheat Sheet -| Testing Library | Playwright | -|---------------------------------------------------------|-----------------------------------------------| -| [screen](https://testing-library.com/docs/queries/about#screen) | [page](./api/class-page) and [component](./api/class-locator) | -| [queries](https://testing-library.com/docs/queries/about) | [locators](./locators) | -| [async helpers](https://testing-library.com/docs/dom-testing-library/api-async) | [assertions](./test-assertions) | -| [user events](https://testing-library.com/docs/user-event/intro) | [actions](./api/class-locator) | -| `await user.click(screen.getByText('Click me'))` | `await component.getByText('Click me').click()` | -| `await user.click(await screen.findByText('Click me'))` | `await component.getByText('Click me').click()` | -| `await user.type(screen.getByLabel('Password'), 'secret')` | `await component.getByLabel('Password').fill('secret')` | -| `expect(screen.getByLabel('Password')).toHaveValue('secret')` | `await expect(component.getByLabel('Password')).toHaveValue('secret')` | -| `screen.findByText('...')` | `component.getByText('...')` | -| `screen.getByTestId('...')` | `component.getByTestId('...')` | -| `screen.queryByPlaceholderText('...')` | `component.getByPlaceholder('...')` | -| `screen.getByRole('button', { pressed: true })` | `component.getByRole('button', { pressed: true })`| +| Testing Library | Playwright | +| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [screen](https://testing-library.com/docs/queries/about#screen) | [page](./api/class-page) and [component](./api/class-locator) | +| [queries](https://testing-library.com/docs/queries/about) | [locators](./locators) | +| [async helpers](https://testing-library.com/docs/dom-testing-library/api-async) | [assertions](./test-assertions) | +| [user events](https://testing-library.com/docs/user-event/intro) | [actions](./api/class-locator) | +| `await user.click(screen.getByText('Click me'))` | `await component.getByText('Click me').click()` | +| `await user.click(await screen.findByText('Click me'))` | `await component.getByText('Click me').click()` | +| `await user.type(screen.getByLabelText('Password'), 'secret')` | `await component.getByLabel('Password').fill('secret')` | +| `expect(screen.getByLabelText('Password')).toHaveValue('secret')` | `await expect(component.getByLabel('Password')).toHaveValue('secret')` | +| `screen.getByRole('button', { pressed: true })` | `component.getByRole('button', { pressed: true })` | +| `screen.getByLabelText('...')` | `component.getByLabel('...')` | +| `screen.queryByPlaceholderText('...')` | `component.getByPlaceholder('...')` | +| `screen.findByText('...')` | `component.getByText('...')` | +| `screen.getByTestId('...')` | `component.getByTestId('...')` | +| `render();` | `mount();` | +| `const { unmount } = render();` | `const { unmount } = await mount();` | +| `const { rerender } = render();` | `const { update } = await mount();` | + ## Example @@ -37,18 +42,18 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -test('should sign in', async () => { +test('sign in', async () => { // Setup the page. const user = userEvent.setup(); render(); // Perform actions. - await user.type(screen.getByLabel('Username'), 'John'); - await user.type(screen.getByLabel('Password'), 'secret'); - await user.click(screen.getByText('Sign in')); + await user.type(screen.getByLabelText('Username'), 'John'); + await user.type(screen.getByLabelText('Password'), 'secret'); + await user.click(screen.getByRole('button', { name: 'Sign in' })); // Verify signed in state by waiting until "Welcome" message appears. - await screen.findByText('Welcome, John'); + expect(await screen.findByText('Welcome, John')).toBeInTheDocument(); }); ``` @@ -57,14 +62,14 @@ Line-by-line migration to Playwright Test: ```js const { test, expect } = require('@playwright/experimental-ct-react'); // 1 -test('should sign in', async ({ page, mount }) => { // 2 +test('sign in', async ({ mount }) => { // 2 // Setup the page. const component = await mount(); // 3 // Perform actions. - await component.getByText('Username').fill('John'); // 4 - await component.getByText('Password').fill('secret'); - await component.getByText('Sign in').click(); + await component.getByLabel('Username').fill('John'); // 4 + await component.getByLabel('Password').fill('secret'); + await component.getByRole('button', { name: 'Sign in' }).click(); // Verify signed in state by waiting until "Welcome" message appears. await expect(component.getByText('Welcome, John')).toBeVisible(); // 5 @@ -118,7 +123,7 @@ const messages = document.getElementById('messages'); const helloMessage = within(messages).getByText('hello'); // Playwright -const messages = component.locator('id=messages'); +const messages = component.getByTestId('messages'); const helloMessage = messages.getByText('hello'); ``` @@ -133,7 +138,9 @@ Once you're on Playwright Test, you get a lot! - Built-in test [artifact collection](./test-use-options.md#recording-options) You also get all these ✨ awesome tools ✨ that come bundled with Playwright Test: -- [Playwright Inspector](./debug.md) +- [Visual Studio Code integration](./getting-started-vscode.md) +- [UI mode](./test-ui-mode.md) for debugging tests with a time travel experience complete with watch mode. +- [Playwright Inspector](./debug.md#playwright-inspector) - [Playwright Test Code generation](./codegen-intro.md) - [Playwright Tracing](./trace-viewer.md) for post-mortem debugging From 664c4dd44297fc8147d10734b617801a6c464609 Mon Sep 17 00:00:00 2001 From: Sander Date: Tue, 11 Jun 2024 02:22:00 +0200 Subject: [PATCH 043/144] chore(ct): vue update vue-component-type-helpers (#31213) The `ComponentProps` copied from [`vue-component-type-helpers`](https://github.com/vuejs/language-tools/blob/5c65f102d01ade46975001e8509f26a2d90774e9/packages/component-type-helpers/index.ts#L6-L9) was outdated and had an issue accurately inferring generic/functional Vue components. --- packages/playwright-ct-vue/index.d.ts | 2 +- packages/playwright-ct-vue2/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-ct-vue/index.d.ts b/packages/playwright-ct-vue/index.d.ts index f755a6bcf3..fee68f3cd0 100644 --- a/packages/playwright-ct-vue/index.d.ts +++ b/packages/playwright-ct-vue/index.d.ts @@ -24,7 +24,7 @@ type ComponentEvents = Record; // Copied from: https://github.com/vuejs/language-tools/blob/master/packages/vue-component-type-helpers/index.d.ts#L10-L13 type ComponentProps = - T extends new () => { $props: infer P; } ? NonNullable

: + T extends new (...angs: any) => { $props: infer P; } ? NonNullable

: T extends (props: infer P, ...args: any) => any ? P : {}; diff --git a/packages/playwright-ct-vue2/index.d.ts b/packages/playwright-ct-vue2/index.d.ts index b4fd75e395..f1a2a0489f 100644 --- a/packages/playwright-ct-vue2/index.d.ts +++ b/packages/playwright-ct-vue2/index.d.ts @@ -24,7 +24,7 @@ type ComponentEvents = Record; // Copied from: https://github.com/vuejs/language-tools/blob/master/packages/vue-component-type-helpers/index.d.ts#L10-L13 type ComponentProps = - T extends new () => { $props: infer P; } ? NonNullable

: + T extends new (...angs: any) => { $props: infer P; } ? NonNullable

: T extends (props: infer P, ...args: any) => any ? P : {}; From f95b4e0ac83edae3882aab777c746cfa5149d96d Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 11 Jun 2024 15:06:03 +0200 Subject: [PATCH 044/144] docs(dotnet): recommend MSTest over NUnit (#31245) --- docs/src/api-testing-csharp.md | 256 +++++++++--------- docs/src/api/class-locatorassertions.md | 14 +- docs/src/api/class-pageassertions.md | 14 +- docs/src/api/class-playwrightassertions.md | 11 +- docs/src/intro-csharp.md | 18 +- docs/src/languages.md | 2 +- docs/src/library-csharp.md | 2 +- docs/src/release-notes-csharp.md | 2 +- docs/src/running-tests-csharp.md | 6 +- .../src/test-assertions-csharp-java-python.md | 4 +- docs/src/test-runners-csharp.md | 249 +++++++++-------- docs/src/trace-viewer-intro-csharp.md | 6 +- docs/src/trace-viewer.md | 8 +- docs/src/webview2.md | 31 ++- docs/src/writing-tests-csharp.md | 14 +- 15 files changed, 318 insertions(+), 319 deletions(-) diff --git a/docs/src/api-testing-csharp.md b/docs/src/api-testing-csharp.md index f68a733322..f68ff8a8f7 100644 --- a/docs/src/api-testing-csharp.md +++ b/docs/src/api-testing-csharp.md @@ -16,7 +16,7 @@ A few examples where it may come in handy: All of that could be achieved via [APIRequestContext] methods. -The following examples rely on the [`Microsoft.Playwright.NUnit`](./test-runners.md) package which creates a Playwright and Page instance for each test. +The following examples rely on the [`Microsoft.Playwright.MSTest`](./test-runners.md) package which creates a Playwright and Page instance for each test. @@ -34,22 +34,19 @@ The following example demonstrates how to use Playwright to test issues creation GitHub API requires authorization, so we'll configure the token once for all tests. While at it, we'll also set the `baseURL` to simplify the tests. ```csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Playwright.NUnit; using Microsoft.Playwright; -using NUnit.Framework; +using Microsoft.Playwright.MSTest; namespace PlaywrightTests; +[TestClass] public class TestGitHubAPI : PlaywrightTest { - static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); - private IAPIRequestContext Request = null; + private IAPIRequestContext Request = null!; - [SetUp] + [TestInitialize] public async Task SetUpAPITesting() { await CreateAPIRequestContext(); @@ -71,7 +68,7 @@ public class TestGitHubAPI : PlaywrightTest }); } - [TearDown] + [TestCleanup] public async Task TearDownAPITesting() { await Request.DisposeAsync(); @@ -83,36 +80,34 @@ public class TestGitHubAPI : PlaywrightTest Now that we initialized request object we can add a few tests that will create new issues in the repository. ```csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using System.Text.Json; -using Microsoft.Playwright.NUnit; using Microsoft.Playwright; -using NUnit.Framework; +using Microsoft.Playwright.MSTest; namespace PlaywrightTests; -[TestFixture] +[TestClass] public class TestGitHubAPI : PlaywrightTest { - static string REPO = "test-repo-2"; + static string REPO = "test"; static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); - static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); - private IAPIRequestContext Request = null; + private IAPIRequestContext Request = null!; - [Test] + [TestMethod] public async Task ShouldCreateBugReport() { - var data = new Dictionary(); - data.Add("title", "[Bug] report 1"); - data.Add("body", "Bug description"); + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); - Assert.True(newIssue.Ok); + await Expect(newIssue).ToBeOKAsync(); var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); - Assert.True(issues.Ok); + await Expect(newIssue).ToBeOKAsync(); var issuesJsonResponse = await issues.JsonAsync(); JsonElement? issue = null; foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) @@ -125,23 +120,24 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.NotNull(issue); + Assert.IsNotNull(issue); Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); } - [Test] + [TestMethod] public async Task ShouldCreateFeatureRequests() { - var data = new Dictionary(); - data.Add("title", "[Feature] request 1"); - data.Add("body", "Feature description"); + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); - Assert.True(newIssue.Ok); + await Expect(newIssue).ToBeOKAsync(); var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); - Assert.True(issues.Ok); + await Expect(newIssue).ToBeOKAsync(); var issuesJsonResponse = await issues.JsonAsync(); - var issuesJson = (await issues.JsonAsync())?.EnumerateArray(); JsonElement? issue = null; foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) @@ -154,7 +150,7 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.NotNull(issue); + Assert.IsNotNull(issue); Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); } @@ -167,41 +163,47 @@ public class TestGitHubAPI : PlaywrightTest These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `[SetUp]` and `[TearDown]` hooks for that. ```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; + +namespace PlaywrightTests; + +[TestClass] public class TestGitHubAPI : PlaywrightTest { - // ... + // ... + [TestInitialize] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } - [SetUp] - public async Task SetUpAPITesting() - { - await CreateAPIRequestContext(); - await CreateTestRepository(); - } + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } - private async Task CreateTestRepository() - { - var resp = await Request.PostAsync("/user/repos", new() - { - DataObject = new Dictionary() - { - ["name"] = REPO, - }, - }); - Assert.True(resp.Ok); - } + [TestCleanup] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } - [TearDown] - public async Task TearDownAPITesting() - { - await DeleteTestRepository(); - await Request.DisposeAsync(); - } - - private async Task DeleteTestRepository() - { - var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); - Assert.True(resp.Ok); - } + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } } ``` @@ -210,36 +212,34 @@ public class TestGitHubAPI : PlaywrightTest Here is the complete example of an API test: ```csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using System.Text.Json; -using Microsoft.Playwright.NUnit; using Microsoft.Playwright; -using NUnit.Framework; +using Microsoft.Playwright.MSTest; namespace PlaywrightTests; -[TestFixture] +[TestClass] public class TestGitHubAPI : PlaywrightTest { static string REPO = "test-repo-2"; static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); - static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); - private IAPIRequestContext Request = null; + private IAPIRequestContext Request = null!; - [Test] + [TestMethod] public async Task ShouldCreateBugReport() { - var data = new Dictionary(); - data.Add("title", "[Bug] report 1"); - data.Add("body", "Bug description"); + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); - Assert.True(newIssue.Ok); + await Expect(newIssue).ToBeOKAsync(); var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); - Assert.True(issues.Ok); + await Expect(newIssue).ToBeOKAsync(); var issuesJsonResponse = await issues.JsonAsync(); JsonElement? issue = null; foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) @@ -252,23 +252,24 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.NotNull(issue); + Assert.IsNotNull(issue); Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); } - [Test] + [TestMethod] public async Task ShouldCreateFeatureRequests() { - var data = new Dictionary(); - data.Add("title", "[Feature] request 1"); - data.Add("body", "Feature description"); + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); - Assert.True(newIssue.Ok); + await Expect(newIssue).ToBeOKAsync(); var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); - Assert.True(issues.Ok); + await Expect(newIssue).ToBeOKAsync(); var issuesJsonResponse = await issues.JsonAsync(); - var issuesJson = (await issues.JsonAsync())?.EnumerateArray(); JsonElement? issue = null; foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) @@ -281,11 +282,11 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.NotNull(issue); + Assert.IsNotNull(issue); Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); } - [SetUp] + [TestInitialize] public async Task SetUpAPITesting() { await CreateAPIRequestContext(); @@ -294,14 +295,16 @@ public class TestGitHubAPI : PlaywrightTest private async Task CreateAPIRequestContext() { - var headers = new Dictionary(); - // We set this header per GitHub guidelines. - headers.Add("Accept", "application/vnd.github.v3+json"); - // Add authorization token to all requests. - // Assuming personal access token available in the environment. - headers.Add("Authorization", "token " + API_TOKEN); + var headers = new Dictionary + { + // We set this header per GitHub guidelines. + { "Accept", "application/vnd.github.v3+json" }, + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + { "Authorization", "token " + API_TOKEN } + }; - Request = await this.Playwright.APIRequest.NewContextAsync(new() + Request = await Playwright.APIRequest.NewContextAsync(new() { // All requests we send go to this API endpoint. BaseURL = "https://api.github.com", @@ -318,10 +321,10 @@ public class TestGitHubAPI : PlaywrightTest ["name"] = REPO, }, }); - Assert.True(resp.Ok); + await Expect(resp).ToBeOKAsync(); } - [TearDown] + [TestCleanup] public async Task TearDownAPITesting() { await DeleteTestRepository(); @@ -331,7 +334,7 @@ public class TestGitHubAPI : PlaywrightTest private async Task DeleteTestRepository() { var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); - Assert.True(resp.Ok); + await Expect(resp).ToBeOKAsync(); } } ``` @@ -344,21 +347,23 @@ project to check that it appears at the top of the list. The check is performed ```csharp class TestGitHubAPI : PageTest { - [Test] - public async Task LastCreatedIssueShouldBeFirstInTheList() - { - var data = new Dictionary(); - data.Add("title", "[Feature] request 1"); - data.Add("body", "Feature description"); - var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); - Assert.True(newIssue.Ok); + [TestMethod] + public async Task LastCreatedIssueShouldBeFirstInTheList() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); - // When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start - // a browser, context, and page manually or inherit from 'PageTest' which will launch it for you. - await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); - var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First; - await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1"); - } + // When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start + // a browser, context, and page manually or inherit from 'PageTest' which will launch it for you. + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First; + await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1"); + } } ``` @@ -368,22 +373,23 @@ The following test creates a new issue via user interface in the browser and the it was created: ```csharp +// Make sure to extend from PageTest if you want to use the Page class. class GitHubTests : PageTest { - [Test] - public async Task LastCreatedIssueShouldBeOnTheServer() - { - await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); - await Page.Locator("text=New Issue").ClickAsync(); - await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1"); - await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description"); - await Page.Locator("text=Submit new issue").ClickAsync(); - String issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); + [TestMethod] + public async Task LastCreatedIssueShouldBeOnTheServer() + { + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + await Page.Locator("text=New Issue").ClickAsync(); + await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1"); + await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description"); + await Page.Locator("text=Submit new issue").ClickAsync(); + var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); - var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); - Assert.True(newIssue.Ok); - StringAssert.Contains(await newIssue.TextAsync(), "Bug report 1"); - } + var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + await Expect(newIssue).ToBeOKAsync(); + StringAssert.Contains(await newIssue.TextAsync(), "Bug report 1"); + } } ``` diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index c06f906e3d..1ff5e52119 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -47,21 +47,19 @@ def test_status_becomes_submitted(page: Page) -> None: ``` ```csharp -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.Playwright.NUnit; -using NUnit.Framework; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; namespace PlaywrightTests; -[TestFixture] +[TestClass] public class ExampleTests : PageTest { - [Test] + [TestMethod] public async Task StatusBecomesSubmitted() { - // .. - await Page.GetByRole(AriaRole.Button).ClickAsync(); + // ... + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); await Expect(Page.Locator(".status")).ToHaveTextAsync("Submitted"); } } diff --git a/docs/src/api/class-pageassertions.md b/docs/src/api/class-pageassertions.md index db9b0da236..51076e5e07 100644 --- a/docs/src/api/class-pageassertions.md +++ b/docs/src/api/class-pageassertions.md @@ -50,21 +50,19 @@ def test_navigates_to_login_page(page: Page) -> None: ```csharp using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.Playwright.NUnit; -using NUnit.Framework; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; namespace PlaywrightTests; -[TestFixture] +[TestClass] public class ExampleTests : PageTest { - [Test] + [TestMethod] public async Task NavigatetoLoginPage() { - // .. - await Page.GetByText("Sing in").ClickAsync(); - await Expect(Page.Locator("div#foobar")).ToHaveURL(new Regex(".*/login")); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Expect(Page).ToHaveURLAsync(new Regex(".*/login")); } } ``` diff --git a/docs/src/api/class-playwrightassertions.md b/docs/src/api/class-playwrightassertions.md index 0d8a2c9ce8..7a960e34e1 100644 --- a/docs/src/api/class-playwrightassertions.md +++ b/docs/src/api/class-playwrightassertions.md @@ -50,19 +50,18 @@ public class TestExample { ``` ```csharp -using System.Threading.Tasks; -using Microsoft.Playwright.NUnit; -using NUnit.Framework; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; namespace PlaywrightTests; -[TestFixture] +[TestClass] public class ExampleTests : PageTest { - [Test] + [TestMethod] public async Task StatusBecomesSubmitted() { - await Page.Locator("#submit-button").ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync(); await Expect(Page.Locator(".status")).ToHaveTextAsync("Submitted"); } } diff --git a/docs/src/intro-csharp.md b/docs/src/intro-csharp.md index 91f8a967b0..61136782ec 100644 --- a/docs/src/intro-csharp.md +++ b/docs/src/intro-csharp.md @@ -7,16 +7,16 @@ title: "Installation" Playwright was created specifically to accommodate the needs of end-to-end testing. Playwright supports all modern rendering engines including Chromium, WebKit, and Firefox. Test on Windows, Linux, and macOS, locally or on CI, headless or headed with native mobile emulation. -You can choose to use [NUnit base classes](./test-runners.md#nunit) or [MSTest base classes](./test-runners.md#mstest) that Playwright provides to write end-to-end tests. These classes support running tests on multiple browser engines, parallelizing tests, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. Alternatively you can use the [library](./library.md) to manually write the testing infrastructure. +You can choose to use [MSTest base classes](./test-runners.md#mstest) or [NUnit base classes](./test-runners.md#nunit) that Playwright provides to write end-to-end tests. These classes support running tests on multiple browser engines, parallelizing tests, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. Alternatively you can use the [library](./library.md) to manually write the testing infrastructure. 1. Start by creating a new project with `dotnet new`. This will create the `PlaywrightTests` directory which includes a `UnitTest1.cs` file: @@ -41,10 +41,10 @@ cd PlaywrightTests @@ -83,10 +83,10 @@ Edit the `UnitTest1.cs` file with the code below to create an example end-to-end @@ -132,10 +132,8 @@ public class ExampleTest : PageTest ```csharp title="UnitTest1.cs" using System.Text.RegularExpressions; -using System.Threading.Tasks; using Microsoft.Playwright; using Microsoft.Playwright.MSTest; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace PlaywrightTests; @@ -192,4 +190,4 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a - [Generate tests with Codegen](./codegen-intro.md) - [See a trace of your tests](./trace-viewer-intro.md) - [Run tests on CI](./ci-intro.md) -- [Learn more about the NUnit and MSTest base classes](./test-runners.md) +- [Learn more about the MSTest and NUnit base classes](./test-runners.md) diff --git a/docs/src/languages.md b/docs/src/languages.md index a84167fe97..050c1597b3 100644 --- a/docs/src/languages.md +++ b/docs/src/languages.md @@ -30,7 +30,7 @@ You can choose any testing framework such as JUnit or TestNG based on your proje ## .NET -Playwright for .NET comes with [NUnit base classes](https://playwright.dev/dotnet/docs/test-runners#nunit) and [MSTest base classes](https://playwright.dev/dotnet/docs/test-runners#mstest) for writing end-to-end tests. +Playwright for .NET comes with [MSTest base classes](https://playwright.dev/dotnet/docs/test-runners#mstest) and [NUnit base classes](https://playwright.dev/dotnet/docs/test-runners#nunit) for writing end-to-end tests. * [Documentation](https://playwright.dev/dotnet/docs/intro) * [GitHub repo](https://github.com/microsoft/playwright-dotnet) diff --git a/docs/src/library-csharp.md b/docs/src/library-csharp.md index 5dab997f7e..c659cbf1ee 100644 --- a/docs/src/library-csharp.md +++ b/docs/src/library-csharp.md @@ -5,7 +5,7 @@ title: "Getting started - Library" ## Introduction -Playwright can either be used with the [NUnit](./test-runners.md#nunit) or [MSTest](./test-runners.md#mstest), or as a Playwright Library (this guide). If you are working on an application that utilizes Playwright capabilities or you are using Playwright with another test runner, read on. +Playwright can either be used with the [MSTest](./test-runners.md#mstest) or [NUnit](./test-runners.md#nunit), or as a Playwright Library (this guide). If you are working on an application that utilizes Playwright capabilities or you are using Playwright with another test runner, read on. ## Usage diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index 54d91386cb..9a3301a764 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -637,7 +637,7 @@ This version was also tested against the following stable channels: ### Other highlights - New option `MaxRedirects` for [`method: APIRequestContext.get`] and others to limit redirect count. -- Codegen now supports NUnit and MSTest frameworks. +- Codegen now supports MSTest and NUnit frameworks. - ASP .NET is now supported. ### Behavior Change diff --git a/docs/src/running-tests-csharp.md b/docs/src/running-tests-csharp.md index 6bfbcb56bb..d366526f26 100644 --- a/docs/src/running-tests-csharp.md +++ b/docs/src/running-tests-csharp.md @@ -109,10 +109,10 @@ dotnet test --filter "Name~GetStartedLink" @@ -159,4 +159,4 @@ Check out our [debugging guide](./debug.md) to learn more about the [Playwright - [Generate tests with Codegen](./codegen-intro.md) - [See a trace of your tests](./trace-viewer-intro.md) - [Run tests on CI](./ci-intro.md) -- [Learn more about the NUnit and MSTest base classes](./test-runners.md) +- [Learn more about the MSTest and NUnit base classes](./test-runners.md) diff --git a/docs/src/test-assertions-csharp-java-python.md b/docs/src/test-assertions-csharp-java-python.md index 2ab57e40b2..3e8e634a07 100644 --- a/docs/src/test-assertions-csharp-java-python.md +++ b/docs/src/test-assertions-csharp-java-python.md @@ -77,10 +77,10 @@ expect.set_options(timeout=10_000) diff --git a/docs/src/test-runners-csharp.md b/docs/src/test-runners-csharp.md index c6824e2302..74c0f0ff08 100644 --- a/docs/src/test-runners-csharp.md +++ b/docs/src/test-runners-csharp.md @@ -5,135 +5,12 @@ title: "Test Runners" ## Introduction -While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience it works best with the built-in .NET test runner, and using NUnit as the test framework. NUnit is also what we use internally for [our tests](https://github.com/microsoft/playwright-dotnet/tree/main/src/Playwright.Tests). +While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience the easiest way of getting started is by using the base classes we provide for [MSTest](#mstest) and [NUnit](#nunit). These classes support running tests on multiple browser engines, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. -Playwright and Browser instances can be reused between tests for better performance. We +Playwright and Browser instances will be reused between tests for better performance. We recommend running each test case in a new BrowserContext, this way browser state will be isolated between the tests. - -## NUnit - -Playwright provides base classes to write tests with NUnit via the [`Microsoft.Playwright.NUnit`](https://www.nuget.org/packages/Microsoft.Playwright.NUnit) package. - -Check out the [installation guide](./intro.md) to get started. - -### Running NUnit tests in Parallel - -By default NUnit will run all test files in parallel, while running tests inside each file sequentially (`ParallelScope.Self`). It will create as many processes as there are cores on the host system. You can adjust this behavior using the NUnit.NumberOfTestWorkers parameter. -Only `ParallelScope.Self` is supported. - -For CPU-bound tests, we recommend using as many workers as there are cores on your system, divided by 2. For IO-bound tests you can use as many workers as you have cores. - -```bash -dotnet test -- NUnit.NumberOfTestWorkers=5 -``` - -### Customizing [BrowserContext] options - -To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example: - -```csharp -using Microsoft.Playwright.NUnit; - -namespace PlaywrightTests; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] -public class MyTest : PageTest -{ - [Test] - public async Task TestWithCustomContextOptions() - { - // The following Page (and BrowserContext) instance has the custom colorScheme, viewport and baseURL set: - await Page.GotoAsync("/login"); - } - - public override BrowserNewContextOptions ContextOptions() - { - return new BrowserNewContextOptions() - { - ColorScheme = ColorScheme.Light, - ViewportSize = new() - { - Width = 1920, - Height = 1080 - }, - BaseURL = "https://github.com", - }; - } -} -``` - -### Customizing [Browser]/launch options - -[Browser]/launch options can be overridden either using a run settings file or by setting the run settings options directly via the -CLI. See the following example: - -```xml - - - - chromium - - false - msedge - - - -``` - -```bash -dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Headless=false Playwright.LaunchOptions.Channel=msedge -``` - -### Using Verbose API Logs - -When you have enabled the [verbose API log](./debug.md#verbose-api-logs), via the `DEBUG` environment variable, you will see the messages in the standard error stream. In NUnit, within Visual Studio, that will be the `Tests` pane of the `Output` window. It will also be displayed in the `Test Log` for each test. - -### Using the .runsettings file - -When running tests from Visual Studio, you can take advantage of the `.runsettings` file. The following shows a reference of the supported values. - -For example, to specify the amount of workers you can use `NUnit.NumberOfTestWorkers` or to enable `DEBUG` logs `RunConfiguration.EnvironmentVariables`. - -```xml - - - - - 24 - - - - - - pw:api - - - - - chromium - 5000 - - false - msedge - - - -``` - -### Base NUnit classes for Playwright - -There are a few base classes available to you in `Microsoft.Playwright.NUnit` namespace: - -|Test |Description| -|--------------|-----------| -|PageTest |Each test gets a fresh copy of a web [Page] created in its own unique [BrowserContext]. Extending this class is the simplest way of writing a fully-functional Playwright test.



Note: You can override the `ContextOptions` method in each test file to control context options, the ones typically passed into the [`method: Browser.newContext`] method. That way you can specify all kinds of emulation options for your test file individually.| -|ContextTest |Each test will get a fresh copy of a [BrowserContext]. You can create as many pages in this context as you'd like. Using this test is the easiest way to test multi-page scenarios where you need more than one tab.



Note: You can override the `ContextOptions` method in each test file to control context options, the ones typically passed into the [`method: Browser.newContext`] method. That way you can specify all kinds of emulation options for your test file individually.| -|BrowserTest |Each test will get a browser and can create as many contexts as it likes. Each test is responsible for cleaning up all the contexts it created.| -|PlaywrightTest|This gives each test a Playwright object so that the test could start and stop as many browsers as it likes.| - ## MSTest Playwright provides base classes to write tests with MSTest via the [`Microsoft.Playwright.MSTest`](https://www.nuget.org/packages/Microsoft.Playwright.MSTest) package. @@ -259,6 +136,128 @@ There are a few base classes available to you in `Microsoft.Playwright.MSTest` n |BrowserTest |Each test will get a browser and can create as many contexts as it likes. Each test is responsible for cleaning up all the contexts it created.| |PlaywrightTest|This gives each test a Playwright object so that the test could start and stop as many browsers as it likes.| +## NUnit + +Playwright provides base classes to write tests with NUnit via the [`Microsoft.Playwright.NUnit`](https://www.nuget.org/packages/Microsoft.Playwright.NUnit) package. + +Check out the [installation guide](./intro.md) to get started. + +### Running NUnit tests in Parallel + +By default NUnit will run all test files in parallel, while running tests inside each file sequentially (`ParallelScope.Self`). It will create as many processes as there are cores on the host system. You can adjust this behavior using the NUnit.NumberOfTestWorkers parameter. +Only `ParallelScope.Self` is supported. + +For CPU-bound tests, we recommend using as many workers as there are cores on your system, divided by 2. For IO-bound tests you can use as many workers as you have cores. + +```bash +dotnet test -- NUnit.NumberOfTestWorkers=5 +``` + +### Customizing [BrowserContext] options + +To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example: + +```csharp +using Microsoft.Playwright.NUnit; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class MyTest : PageTest +{ + [Test] + public async Task TestWithCustomContextOptions() + { + // The following Page (and BrowserContext) instance has the custom colorScheme, viewport and baseURL set: + await Page.GotoAsync("/login"); + } + + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions() + { + ColorScheme = ColorScheme.Light, + ViewportSize = new() + { + Width = 1920, + Height = 1080 + }, + BaseURL = "https://github.com", + }; + } +} +``` + +### Customizing [Browser]/launch options + +[Browser]/launch options can be overridden either using a run settings file or by setting the run settings options directly via the +CLI. See the following example: + +```xml + + + + chromium + + false + msedge + + + +``` + +```bash +dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Headless=false Playwright.LaunchOptions.Channel=msedge +``` + +### Using Verbose API Logs + +When you have enabled the [verbose API log](./debug.md#verbose-api-logs), via the `DEBUG` environment variable, you will see the messages in the standard error stream. In NUnit, within Visual Studio, that will be the `Tests` pane of the `Output` window. It will also be displayed in the `Test Log` for each test. + +### Using the .runsettings file + +When running tests from Visual Studio, you can take advantage of the `.runsettings` file. The following shows a reference of the supported values. + +For example, to specify the amount of workers you can use `NUnit.NumberOfTestWorkers` or to enable `DEBUG` logs `RunConfiguration.EnvironmentVariables`. + +```xml + + + + + 24 + + + + + + pw:api + + + + + chromium + 5000 + + false + msedge + + + +``` + +### Base NUnit classes for Playwright + +There are a few base classes available to you in `Microsoft.Playwright.NUnit` namespace: + +|Test |Description| +|--------------|-----------| +|PageTest |Each test gets a fresh copy of a web [Page] created in its own unique [BrowserContext]. Extending this class is the simplest way of writing a fully-functional Playwright test.



Note: You can override the `ContextOptions` method in each test file to control context options, the ones typically passed into the [`method: Browser.newContext`] method. That way you can specify all kinds of emulation options for your test file individually.| +|ContextTest |Each test will get a fresh copy of a [BrowserContext]. You can create as many pages in this context as you'd like. Using this test is the easiest way to test multi-page scenarios where you need more than one tab.



Note: You can override the `ContextOptions` method in each test file to control context options, the ones typically passed into the [`method: Browser.newContext`] method. That way you can specify all kinds of emulation options for your test file individually.| +|BrowserTest |Each test will get a browser and can create as many contexts as it likes. Each test is responsible for cleaning up all the contexts it created.| +|PlaywrightTest|This gives each test a Playwright object so that the test could start and stop as many browsers as it likes.| + ## xUnit support While using xUnit is also supported, we do not support running parallel tests. This is a well known problem/design limitation diff --git a/docs/src/trace-viewer-intro-csharp.md b/docs/src/trace-viewer-intro-csharp.md index 3c64c91f5e..79e560862c 100644 --- a/docs/src/trace-viewer-intro-csharp.md +++ b/docs/src/trace-viewer-intro-csharp.md @@ -18,10 +18,10 @@ Traces can be recorded using the [`property: BrowserContext.tracing`] API as fol @@ -134,4 +134,4 @@ Check out our detailed guide on [Trace Viewer](/trace-viewer.md) to learn more a ## What's next - [Run tests on CI with GitHub Actions](/ci-intro.md) -- [Learn more about the NUnit and MSTest base classes](./test-runners.md) +- [Learn more about the MSTest and NUnit base classes](./test-runners.md) diff --git a/docs/src/trace-viewer.md b/docs/src/trace-viewer.md index 90ec4a5e7c..556b22729a 100644 --- a/docs/src/trace-viewer.md +++ b/docs/src/trace-viewer.md @@ -244,10 +244,10 @@ Traces can be recorded using the [`property: BrowserContext.tracing`] API as fol @@ -355,10 +355,10 @@ Setup your tests to record a trace only when the test fails: diff --git a/docs/src/webview2.md b/docs/src/webview2.md index 573f8369f4..7aef7216ca 100644 --- a/docs/src/webview2.md +++ b/docs/src/webview2.md @@ -347,14 +347,14 @@ def test_webview2(page: Page): ```csharp // WebView2Test.cs -using System.Text.RegularExpressions; -using Microsoft.Playwright.NUnit; -using Microsoft.Playwright; using System.Diagnostics; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; -namespace dotnet_nunit; +namespace PlaywrightTests; -public class WebView2Test : PlaywrightTest +[TestClass] +public class ExampleTest : PlaywrightTest { public IBrowser Browser { get; internal set; } = null!; public IBrowserContext Context { get; internal set; } = null!; @@ -363,12 +363,12 @@ public class WebView2Test : PlaywrightTest private string _userDataDir = null!; private string _executablePath = Path.Join(Directory.GetCurrentDirectory(), @"..\..\..\..\webview2-app\bin\Debug\net8.0-windows\webview2.exe"); - [SetUp] - public async Task BrowserSetUp() + [TestInitialize] + public async Task BrowserTestInitialize() { var cdpPort = 10000 + WorkerIndex; Assert.IsTrue(File.Exists(_executablePath), "Make sure that the executable exists"); - _userDataDir = Path.Join(Path.GetTempPath(), $"playwright-webview2-tests/user-data-dir-{TestContext.CurrentContext.WorkerId}"); + _userDataDir = Path.Join(Path.GetTempPath(), $"playwright-webview2-tests/user-data-dir-{WorkerIndex}"); // WebView2 does some lazy cleanups on shutdown so we can't clean it up after each test if (Directory.Exists(_userDataDir)) { @@ -401,8 +401,8 @@ public class WebView2Test : PlaywrightTest Page = Context.Pages[0]; } - [TearDown] - public async Task BrowserTearDown() + [TestCleanup] + public async Task BrowserTestCleanup() { _webView2Process!.Kill(true); await Browser.CloseAsync(); @@ -412,14 +412,15 @@ public class WebView2Test : PlaywrightTest ```csharp // UnitTest1.cs -using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; -namespace dotnet_nunit; +namespace PlaywrightTests; -[Parallelizable(ParallelScope.Self)] -public class Tests : WebView2Test +[TestClass] +public class ExampleTest : WebView2Test { - [Test] + [TestMethod] public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() { await Page.GotoAsync("https://playwright.dev"); diff --git a/docs/src/writing-tests-csharp.md b/docs/src/writing-tests-csharp.md index 4b8b760131..f6324be9f1 100644 --- a/docs/src/writing-tests-csharp.md +++ b/docs/src/writing-tests-csharp.md @@ -35,10 +35,10 @@ Take a look at the following example to see how to write a test. @@ -200,10 +200,10 @@ The Playwright NUnit and MSTest test framework base classes will isolate each te @@ -257,10 +257,10 @@ You can use `SetUp`/`TearDown` in NUnit or `TestInitialize`/`TestCleanup` in MST @@ -328,4 +328,4 @@ public class ExampleTest : PageTest - [Generate tests with Codegen](./codegen-intro.md) - [See a trace of your tests](./trace-viewer-intro.md) - [Run tests on CI](./ci-intro.md) -- [Learn more about the NUnit and MSTest base classes](./test-runners.md) +- [Learn more about the MSTest and NUnit base classes](./test-runners.md) From c08000b967c8040271c02a931b161639a0ddeb5a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 11 Jun 2024 09:18:45 -0700 Subject: [PATCH 045/144] feat(chromium): storage-access permission (#31239) Fixes https://github.com/microsoft/playwright/issues/31227 --- docs/src/api/class-browsercontext.md | 21 ++++++++------- .../src/server/chromium/crBrowser.ts | 1 + packages/playwright-core/types/types.d.ts | 21 ++++++++------- tests/library/permissions.spec.ts | 26 +++++++++++++++++++ 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index f9ab6694c3..3ea2180873 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1044,21 +1044,22 @@ specified. - `permissions` <[Array]<[string]>> A permission or an array of permissions to grant. Permissions can be one of the following values: -* `'geolocation'` -* `'midi'` -* `'midi-sysex'` (system-exclusive midi) -* `'notifications'` -* `'camera'` -* `'microphone'` -* `'background-sync'` -* `'ambient-light-sensor'` * `'accelerometer'` -* `'gyroscope'` -* `'magnetometer'` * `'accessibility-events'` +* `'ambient-light-sensor'` +* `'background-sync'` +* `'camera'` * `'clipboard-read'` * `'clipboard-write'` +* `'geolocation'` +* `'gyroscope'` +* `'magnetometer'` +* `'microphone'` +* `'midi-sysex'` (system-exclusive midi) +* `'midi'` +* `'notifications'` * `'payment-handler'` +* `'storage-access'` ### option: BrowserContext.grantPermissions.origin * since: v1.8 diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 530906c0b2..09a24c1d1b 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -433,6 +433,7 @@ export class CRBrowserContext extends BrowserContext { ['payment-handler', 'paymentHandler'], // chrome-specific permissions we have. ['midi-sysex', 'midiSysex'], + ['storage-access', 'storageAccess'], ]); const filtered = permissions.map(permission => { const protocolPermission = webPermissionToProtocol.get(permission); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 3753d59986..10dd00f198 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8516,21 +8516,22 @@ export interface BrowserContext { * Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if * specified. * @param permissions A permission or an array of permissions to grant. Permissions can be one of the following values: - * - `'geolocation'` - * - `'midi'` - * - `'midi-sysex'` (system-exclusive midi) - * - `'notifications'` - * - `'camera'` - * - `'microphone'` - * - `'background-sync'` - * - `'ambient-light-sensor'` * - `'accelerometer'` - * - `'gyroscope'` - * - `'magnetometer'` * - `'accessibility-events'` + * - `'ambient-light-sensor'` + * - `'background-sync'` + * - `'camera'` * - `'clipboard-read'` * - `'clipboard-write'` + * - `'geolocation'` + * - `'gyroscope'` + * - `'magnetometer'` + * - `'microphone'` + * - `'midi-sysex'` (system-exclusive midi) + * - `'midi'` + * - `'notifications'` * - `'payment-handler'` + * - `'storage-access'` * @param options */ grantPermissions(permissions: ReadonlyArray, options?: { diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index db0b92a53b..ac2ec7a0b5 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -172,3 +172,29 @@ it('should support clipboard read', async ({ page, context, server, browserName, await page.evaluate(() => navigator.clipboard.writeText('test content')); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('test content'); }); + +it('storage access', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31227' } +}, async ({ page, context, server, browserName }) => { + it.fixme(browserName !== 'chromium'); + await context.grantPermissions(['storage-access']); + expect(await getPermission(page, 'storage-access')).toBe('granted'); + server.setRoute('/set-cookie.html', (req, res) => { + res.setHeader('Set-Cookie', 'name=value; Path=/; SameSite=Strict; Secure'); + res.end(); + }); + server.setRoute('/my-frame.html', (req, res) => { + res.setHeader('Content-type', 'text/html'); + res.end(``); + }); + + // Navigate once to the domain as top level. + await page.goto(server.CROSS_PROCESS_PREFIX + '/set-cookie.html'); + await page.goto(server.PREFIX + '/my-frame.html'); + + const frame = page.frames()[1]; + expect(await getPermission(frame, 'storage-access')).toBe('granted'); + const access = await frame.evaluate(() => document.requestStorageAccess().then(() => true, () => false)); + expect(access).toBe(true); + expect(await frame.evaluate(() => document.hasStorageAccess())).toBe(true); +}); From 6399e8de4e35730a54421e420283ff58d4180963 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 11 Jun 2024 09:42:15 -0700 Subject: [PATCH 046/144] chore: clock api review (#31237) --- docs/src/api/class-clock.md | 232 ++-- docs/src/clock.md | 347 ++++-- .../playwright-core/src/client/browser.ts | 8 +- packages/playwright-core/src/client/clock.ts | 60 +- .../playwright-core/src/protocol/validator.ts | 51 +- packages/playwright-core/src/server/clock.ts | 146 +-- .../dispatchers/browserContextDispatcher.ts | 34 +- .../src/server/injected/clock.ts | 426 ++++--- packages/playwright-core/types/types.d.ts | 129 +- packages/protocol/src/channels.ts | 93 +- packages/protocol/src/protocol.yml | 49 +- tests/library/clock.spec.ts | 1068 +++-------------- tests/page/page-clock.frozen.spec.ts | 3 +- tests/page/page-clock.spec.ts | 627 ++++------ 14 files changed, 1254 insertions(+), 2019 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 31115db41b..bf8b9ac59a 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -6,11 +6,88 @@ Accurately simulating time-dependent behavior is essential for verifying the cor Note that clock is installed for the entire [BrowserContext], so the time in all the pages and iframes is controlled by the same clock. -## async method: Clock.installFakeTimers +## async method: Clock.fastForward +* since: v1.45 + +Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and +reopening it later, after given time. + +**Usage** + +```js +await page.clock.fastForward(1000); +await page.clock.fastForward('30:00'); +``` + +```python async +await page.clock.fast_forward(1000) +await page.clock.fast_forward("30:00") +``` + +```python sync +page.clock.fast_forward(1000) +page.clock.fast_forward("30:00") +``` + +```java +page.clock().fastForward(1000); +page.clock().fastForward("30:00"); +``` + +```csharp +await page.Clock.FastForwardAsync(1000); +await page.Clock.FastForwardAsync("30:00"); +``` + +### param: Clock.fastForward.ticks +* since: v1.45 +- `ticks` <[int]|[string]> + +Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + +## async method: Clock.fastForwardTo +* since: v1.45 + +Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and +reopening it at the specified time. + +**Usage** + +```js +await page.clock.fastForwardTo(new Date('2020-02-02')); +await page.clock.fastForwardTo('2020-02-02'); +``` + +```python async +await page.clock.fast_forward_to(datetime.datetime(2020, 2, 2)) +await page.clock.fast_forward_to("2020-02-02") +``` + +```python sync +page.clock.fast_forward_to(datetime.datetime(2020, 2, 2)) +page.clock.fast_forward_to("2020-02-02") +``` + +```java +page.clock().fastForwardTo(Instant.parse("2020-02-02")); +page.clock().fastForwardTo("2020-02-02"); +``` + +```csharp +await page.Clock.FastForwardToAsync(DateTime.Parse("2020-02-02")); +await page.Clock.FastForwardToAsync("2020-02-02"); +``` + +### param: Clock.fastForwardTo.time +* since: v1.45 +- `time` <[int]|[string]|[Date]> + +## async method: Clock.install * since: v1.45 Install fake implementations for the following time-related functions: +* `Date` * `setTimeout` * `clearTimeout` * `setInterval` @@ -21,41 +98,18 @@ Install fake implementations for the following time-related functions: * `cancelIdleCallback` * `performance` -Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.skipTime`] for more information. +Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.fastForward`] for more information. -### param: Clock.installFakeTimers.time +### option: Clock.install.time * since: v1.45 -- `time` <[int]|[Date]> - -Install fake timers with the specified base time. - -### option: Clock.installFakeTimers.loopLimit -* since: v1.45 -- `loopLimit` <[int]> - -The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`. - -## async method: Clock.runAllTimers -* since: v1.45 -- returns: <[int]> - -Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. -Fake timers must be installed. -Returns fake milliseconds since the unix epoch. - -**Details** - -This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or the delays in those timers. -It runs a maximum of [`option: loopLimit`] times after which it assumes there is an infinite loop of timers and throws an error. +- `time` <[int]|[string]|[Date]> +Time to initialize with, current system time by default. ## async method: Clock.runFor * since: v1.45 -- returns: <[int]> -Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. -Fake timers must be installed. -Returns fake milliseconds since the unix epoch. +Advance the clock, firing all the time-related callbacks. **Usage** @@ -66,12 +120,12 @@ await page.clock.runFor('30:00'); ```python async await page.clock.run_for(1000); -await page.clock.run_for('30:00') +await page.clock.run_for("30:00") ``` ```python sync page.clock.run_for(1000); -page.clock.run_for('30:00') +page.clock.run_for("30:00") ``` ```java @@ -84,84 +138,104 @@ await page.Clock.RunForAsync(1000); await page.Clock.RunForAsync("30:00"); ``` -### param: Clock.runFor.time +### param: Clock.runFor.ticks * since: v1.45 -- `time` <[int]|[string]> +- `ticks` <[int]|[string]> Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. -## async method: Clock.runToLastTimer -* since: v1.45 -- returns: <[int]> - -This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as necessary. -If new timers are added while it is executing they will be run only if they would occur before this time. -This is useful when you want to run a test to completion, but the test recursively sets timers that would cause runAll to trigger an infinite loop warning. -Fake timers must be installed. -Returns fake milliseconds since the unix epoch. - - -## async method: Clock.runToNextTimer -* since: v1.45 -- returns: <[int]> - -Advances the clock to the moment of the first scheduled timer, firing it. -Fake timers must be installed. -Returns fake milliseconds since the unix epoch. - - -## async method: Clock.setTime +## async method: Clock.pause * since: v1.45 -Set the clock to the specified time. +Pause timers. Once this method is called, no timers are fired unless [`method: Clock.runFor`], [`method: Clock.fastForward`], [`method: Clock.fastForwardTo`] or [`method: Clock.resume`] is called. -When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as a browser) -being put to sleep and resumed later, skipping intermediary timers. - -### param: Clock.setTime.time +## async method: Clock.resume * since: v1.45 -- `time` <[int]|[Date]> +Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual. -## async method: Clock.skipTime +## async method: Clock.setFixedTime * since: v1.45 -- returns: <[int]> -Advance the clock by jumping forward in time, equivalent to running [`method: Clock.setTime`] with the new target time. - -When fake timers are installed, [`method: Clock.skipTime`] only fires due timers at most once, while [`method: Clock.runFor`] fires all the timers up to the current time. -Returns fake milliseconds since the unix epoch. +Makes `Date.now` and `new Date()` return fixed fake time at all times, +keeps all the timers running. **Usage** ```js -await page.clock.skipTime(1000); -await page.clock.skipTime('30:00'); +await page.clock.setFixedTime(Date.now()); +await page.clock.setFixedTime(new Date('2020-02-02')); +await page.clock.setFixedTime('2020-02-02'); ``` ```python async -await page.clock.skipTime(1000); -await page.clock.skipTime('30:00') +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") ``` ```python sync -page.clock.skipTime(1000); -page.clock.skipTime('30:00') +page.clock.set_fixed_time(datetime.datetime.now()) +page.clock.set_fixed_time(datetime.datetime(2020, 2, 2)) +page.clock.set_fixed_time("2020-02-02") ``` ```java -page.clock().skipTime(1000); -page.clock().skipTime("30:00"); +page.clock().setFixedTime(Instant.now()); +page.clock().setFixedTime(Instant.parse("2020-02-02")); +page.clock().setFixedTime("2020-02-02"); ``` ```csharp -await page.Clock.SkipTimeAsync(1000); -await page.Clock.SkipTimeAsync("30:00"); +await page.Clock.SetFixedTimeAsync(DateTime.Now); +await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2)); +await page.Clock.SetFixedTimeAsync("2020-02-02"); ``` -### param: Clock.skipTime.time +### param: Clock.setFixedTime.time * since: v1.45 -- `time` <[int]|[string]> +- `time` <[int]|[string]|[Date]> -Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. +Time to be set. + +## async method: Clock.setSystemTime +* since: v1.45 + +Sets current system time but does not trigger any timers, unlike [`method: Clock.fastForwardTo`]. + +**Usage** + +```js +await page.clock.setSystemTime(Date.now()); +await page.clock.setSystemTime(new Date('2020-02-02')); +await page.clock.setSystemTime('2020-02-02'); +``` + +```python async +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") +``` + +```python sync +page.clock.set_system_time(datetime.datetime.now()) +page.clock.set_system_time(datetime.datetime(2020, 2, 2)) +page.clock.set_system_time("2020-02-02") +``` + +```java +page.clock().setSystemTime(Instant.now()); +page.clock().setSystemTime(Instant.parse("2020-02-02")); +page.clock().setSystemTime("2020-02-02"); +``` + +```csharp +await page.Clock.SetSystemTimeAsync(DateTime.Now); +await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2)); +await page.Clock.SetSystemTimeAsync("2020-02-02"); +``` + +### param: Clock.setSystemTime.time +* since: v1.45 +- `time` <[int]|[string]|[Date]> diff --git a/docs/src/clock.md b/docs/src/clock.md index 5299145916..82fba434ec 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -17,228 +17,324 @@ Accurately simulating time-dependent behavior is essential for verifying the cor - `cancelAnimationFrame` - `requestIdleCallback` - `cancelIdleCallback` + - `performance` -By default, the clock starts at the unix epoch (timestamp of 0). You can override it using the `now` option. +## Test with predefined time -```js -await page.clock.setTime(new Date('2020-02-02')); -await page.clock.installFakeTimers(new Date('2020-02-02')); -``` - -## Mock Date.now - -Most of the time, you only need to fake `Date.now` and no other time-related functions. -That way the time flows naturally, but `Date.now` returns a fixed value. +Often you only need to fake `Date.now` while keeping the timers going. +That way the time flows naturally, but `Date.now` always returns a fixed value. ```html

``` ```js -await page.clock.setTime(new Date('2024-02-02T10: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'); -await page.clock.setTime(new Date('2024-02-02T10:30:00')); +await page.clock.setFixedTime(new Date('2024-02-02T10:30:00')); +// We know that the page has a timer that updates the time every second. +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); +``` + +## Consistent time and timers + +Sometimes your timers depend on `Date.now` and are confused when the `Date.now` value does not change over time. +In this case, you can install the clock and fast forward to the time of interest when testing. + +```html +
+ +``` + +```js +// 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-02-02T08:00:00') }); +await page.goto('http://localhost:3333'); + +// Take control over time flow. +await page.clock.pause(); +// Pretend that the user closed the laptop lid and opened it again at 10am. +await page.clock.fastForwardTo(new Date('2024-02-02T10:00:00')); + +// Assert the page state. +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); + +// Close the laptop lid again and open it at 10:30am. +await page.clock.fastForward('30:00'); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); ``` ```python async -page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)) -await page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') -await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') +# 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=datetime.datetime(2024, 2, 2, 8, 0, 0)) +await page.goto("http://localhost:3333") -page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) -await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') +# Take control over time flow. +await page.clock.pause() +# Pretend that the user closed the laptop lid and opened it again at 10am. +await page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) + +# Assert the page state. +await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM") + +# Close the laptop lid again and open it at 10:30am. +await page.clock.fast_forward("30:00") +await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM") ``` ```python sync -page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)) -page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') -expect(locator).to_have_text('2/2/2024, 10:00:00 AM') +# Initialize clock with some time before the test time and let the page load +# naturally. `Date.now` will progress as the timers fire. +page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0)) +page.goto("http://localhost:3333") -page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) -expect(locator).to_have_text('2/2/2024, 10:30:00 AM') +# Take control over time flow. +page.clock.pause() +# Pretend that the user closed the laptop lid and opened it again at 10am. +page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) + +# Assert the page state. +expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM") + +# Close the laptop lid again and open it at 10:30am. +page.clock.fast_forward("30:00") +expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM") ``` ```java -page.clock().setTime(Instant.parse("2024-02-02T10:00:00")); +// Initialize clock with some time before the test time and let the page load +// naturally. `Date.now` will progress as the timers fire. +page.clock().install(new Clock.InstallOptions().setTime(Instant.parse("2024-02-02T08:00:00"))); page.navigate("http://localhost:3333"); Locator locator = page.getByTestId("current-time"); + +// Take control over time flow. +page.clock().pause(); +// Pretend that the user closed the laptop lid and opened it again at 10am. +page.clock().fastForwardTo(Instant.parse("2024-02-02T10:00:00")); + +// Assert the page state. assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); -page.clock().setTime(Instant.parse("2024-02-02T10:30:00")); +// Close the laptop lid again and open it at 10:30am. +page.clock().fastForward("30:00"); assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); ``` ```csharp -// Initialize clock with a specific time, only fake Date.now. -await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)); -await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("current-time"); -await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); +// 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.InstallAsync(new +{ + Time = new DateTime(2024, 2, 2, 8, 0, 0) +}); +await Page.GotoAsync("http://localhost:3333"); -await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 30, 0, DateTimeKind.Pst)); -await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); +// Take control over time flow. +await Page.Clock.PauseAsync(); +// Pretend that the user closed the laptop lid and opened it again at 10am. +await Page.Clock.FastForwardToAsync(new DateTime(2024, 2, 2, 10, 0, 0)); + +// Assert the page state. +await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM"); + +// Close the laptop lid again and open it at 10:30am. +await Page.Clock.FastForwardAsync("30:00"); +await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM"); ``` -## Mock Date.now consistent with the timers +## Test inactivity monitoring -Sometimes your timers depend on `Date.now` and are confused when the time stands still. -In cases like this you need to ensure that `Date.now` and timers are consistent. -You can achieve this by installing the fake timers. - -```html -
- -``` +Inactivity monitoring is a common feature in web applications that logs out users after a period of inactivity. +Testing this feature can be tricky because you need to wait for a long time to see the effect. +With the help of the clock, you can speed up time and test this feature quickly. ```js -// Initialize clock with a specific time, take full control over time. -await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); +// Initial time does not matter for the test, so we can pick current time. +await page.clock.install(); await page.goto('http://localhost:3333'); -await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); +// Interact with the page +await page.getByRole('button').click(); -// Fast forward time 30 minutes without firing intermediate timers, as if the user -// closed and opened the lid of the laptop. -await page.clock.skipTime('30:00'); -await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); +// 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('5:00'); + +// Check that the user was logged out automatically. +await expect(page.getByText('You have been logged out due to inactivity.')).toBeVisible(); ``` ```python async -# Initialize clock with a specific time, take full control over time. -await page.clock.install_fake_timers( - datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst) -) -await page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') -await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') +# 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.get_by_role("button").click() -# Fast forward time 30 minutes without firing intermediate timers, as if the user -# closed and opened the lid of the laptop. -await page.clock.skip_time('30:00') -await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') +# 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.fast_forward("5:00") + +# Check that the user was logged out automatically. +await expect(page.getByText("You have been logged out due to inactivity.")).toBeVisible() ``` ```python sync -# Initialize clock with a specific time, take full control over time. -page.clock.install_fake_timers( - datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst) -) -page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') -expect(locator).to_have_text('2/2/2024, 10:00:00 AM') +# Initial time does not matter for the test, so we can pick current time. +page.clock.install() +page.goto("http://localhost:3333") +# Interact with the page +page.get_by_role("button").click() -# Fast forward time 30 minutes without firing intermediate timers, as if the user -# closed and opened the lid of the laptop. -page.clock.skip_time('30:00') -expect(locator).to_have_text('2/2/2024, 10:30:00 AM') +# 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. +page.clock.fast_forward("5:00") + +# Check that the user was logged out automatically. +expect(page.get_by_text("You have been logged out due to inactivity.")).to_be_visible() ``` ```java -// Initialize clock with a specific time, take full control over time. -page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); +// Initial time does not matter for the test, so we can pick current time. +page.clock().install(); page.navigate("http://localhost:3333"); -Locator locator = page.getByTestId("current-time"); -assertThat(locator).hasText("2/2/2024, 10:00:00 AM") +Locator locator = page.getByRole("button"); -// Fast forward time 30 minutes without firing intermediate timers, as if the user -// closed and opened the lid of the laptop. -page.clock().skipTime("30:00"); -assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); +// Interact with the page +locator.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. +page.clock().fastForward("5:00"); + +// Check that the user was logged out automatically. +assertThat(page.getByText("You have been logged out due to inactivity.")).isVisible(); ``` ```csharp -// Initialize clock with a specific time, take full control over time. -await page.Clock.InstallFakeTimersAsync( - new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) -); +// Initial time does not matter for the test, so we can pick current time. +await Page.Clock.InstallAsync(); await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("current-time"); -await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); -// Fast forward time 30 minutes without firing intermediate timers, as if the user -// closed and opened the lid of the laptop. -await page.Clock.SkipTimeAsync("30:00"); -await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); +// Interact with the page +await page.GetByRole("button").ClickAsync(); + +// 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.FastForwardAsync("5:00"); + +// Check that the user was logged out automatically. +await Expect(Page.GetByText("You have been logged out due to inactivity.")).ToBeVisibleAsync(); ``` -## Tick through time manually +## Tick through time manually, firing all the timers consistently -In rare cases, you may want to tick through time manually, firing all timers and animation frames in the process to achieve a fine-grained -control over the passage of time. +In rare cases, you may want to tick through time manually, firing all timers and +animation frames in the process to achieve a fine-grained control over the passage of time. ```html
``` ```js -// Initialize clock with a specific time, take full control over time. -await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); +// Initialize clock with a specific time, let the page load naturally. +await page.clock.install({ time: new Date('2024-02-02T08:00:00') }); await page.goto('http://localhost:3333'); +// Pause the time flow, stop the timers, you now have manual control +// over the page time. +await page.clock.pause(); +await page.clock.fastForwardTo(new Date('2024-02-02T10:00:00')); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); + // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. await page.clock.runFor(2000); -await expect(locator).to_have_text('2/2/2024, 10:00:02 AM'); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:02 AM'); ``` ```python async -# Initialize clock with a specific time, take full control over time. -await page.clock.install_fake_timers( - datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst), +# Initialize clock with a specific time, let the page load naturally. +await page.clock.install(time= + datetime.datetime(2024, 2, 2, 8, 0, 0, tzinfo=datetime.timezone.pst), ) -await page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') +await page.goto("http://localhost:3333") +locator = page.get_by_test_id("current-time") + +# Pause the time flow, stop the timers, you now have manual control +# over the page time. +await page.clock.pause() +await page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) +await expect(locator).to_have_text("2/2/2024, 10:00:00 AM") # Tick through time manually, firing all timers in the process. # In this case, time will be updated in the screen 2 times. await page.clock.run_for(2000) -await expect(locator).to_have_text('2/2/2024, 10:00:02 AM') +await expect(locator).to_have_text("2/2/2024, 10:00:02 AM") ``` ```python sync -# Initialize clock with a specific time, take full control over time. -page.clock.install_fake_timers( - datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst), +# Initialize clock with a specific time, let the page load naturally. +page.clock.install( + time=datetime.datetime(2024, 2, 2, 8, 0, 0, tzinfo=datetime.timezone.pst), ) -page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') +page.goto("http://localhost:3333") +locator = page.get_by_test_id("current-time") + +# Pause the time flow, stop the timers, you now have manual control +# over the page time. +page.clock.pause() +page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) +expect(locator).to_have_text("2/2/2024, 10:00:00 AM") # Tick through time manually, firing all timers in the process. # In this case, time will be updated in the screen 2 times. page.clock.run_for(2000) -expect(locator).to_have_text('2/2/2024, 10:00:02 AM') +expect(locator).to_have_text("2/2/2024, 10:00:02 AM") ``` ```java -// Initialize clock with a specific time, take full control over time. -page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); +// Initialize clock with a specific time, let the page load naturally. +page.clock().install(new Clock.InstallOptions() + .setTime(Instant.parse("2024-02-02T08:00:00"))); page.navigate("http://localhost:3333"); Locator locator = page.getByTestId("current-time"); +// Pause the time flow, stop the timers, you now have manual control +// over the page time. +page.clock().pause(); +page.clock().fastForwardTo(Instant.parse("2024-02-02T10:00:00")); +assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); + // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. page.clock().runFor(2000); @@ -246,15 +342,22 @@ assertThat(locator).hasText("2/2/2024, 10:00:02 AM"); ``` ```csharp -// Initialize clock with a specific time, take full control over time. -await page.Clock.InstallFakeTimersAsync( - new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) -); +// Initialize clock with a specific time, let the page load naturally. +await Page.Clock.InstallAsync(new +{ + Time = new DateTime(2024, 2, 2, 8, 0, 0, DateTimeKind.Pst) +}); await page.GotoAsync("http://localhost:3333"); var locator = page.GetByTestId("current-time"); +// Pause the time flow, stop the timers, you now have manual control +// over the page time. +await Page.Clock.PauseAsync(); +await Page.Clock.FastForwardToAsync(new DateTime(2024, 2, 2, 10, 0, 0)); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); + // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. -await page.Clock.RunForAsync(2000); +await Page.Clock.RunForAsync(2000); await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM"); ``` diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index ed903b674e..c1a9b0259d 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -85,8 +85,12 @@ export class Browser extends ChannelOwner implements ap const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const context = BrowserContext.from(response.context); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); - if (!forReuse && !!process.env.PW_FREEZE_TIME) - await this._wrapApiCall(async () => { await context.clock.installFakeTimers(new Date(0)); }, true); + if (!forReuse && !!process.env.PW_FREEZE_TIME) { + await this._wrapApiCall(async () => { + await context.clock.install({ time: 0 }); + await context.clock.pause(); + }, true); + } return context; } diff --git a/packages/playwright-core/src/client/clock.ts b/packages/playwright-core/src/client/clock.ts index 792e36a135..f4d2133b40 100644 --- a/packages/playwright-core/src/client/clock.ts +++ b/packages/playwright-core/src/client/clock.ts @@ -24,44 +24,50 @@ export class Clock implements api.Clock { this._browserContext = browserContext; } - async installFakeTimers(time: number | Date, options: { loopLimit?: number } = {}) { - const timeMs = time instanceof Date ? time.getTime() : time; - await this._browserContext._channel.clockInstallFakeTimers({ time: timeMs, loopLimit: options.loopLimit }); + async install(options: { time?: number | string | Date } = { }) { + await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {}); } - async runAllTimers(): Promise { - const result = await this._browserContext._channel.clockRunAllTimers(); - return result.fakeTime; + async fastForward(ticks: number | string) { + await this._browserContext._channel.clockFastForward(parseTicks(ticks)); } - async runFor(time: number | string): Promise { - const result = await this._browserContext._channel.clockRunFor({ - timeNumber: typeof time === 'number' ? time : undefined, - timeString: typeof time === 'string' ? time : undefined - }); - return result.fakeTime; + async fastForwardTo(time: number | string | Date) { + await this._browserContext._channel.clockFastForwardTo(parseTime(time)); } - async runToLastTimer(): Promise { - const result = await this._browserContext._channel.clockRunToLastTimer(); - return result.fakeTime; + async pause() { + await this._browserContext._channel.clockPause({}); } - async runToNextTimer(): Promise { - const result = await this._browserContext._channel.clockRunToNextTimer(); - return result.fakeTime; + async resume() { + await this._browserContext._channel.clockResume({}); } - async setTime(time: number | Date) { - const timeMs = time instanceof Date ? time.getTime() : time; - await this._browserContext._channel.clockSetTime({ time: timeMs }); + async runFor(ticks: number | string) { + await this._browserContext._channel.clockRunFor(parseTicks(ticks)); } - async skipTime(time: number | string) { - const result = await this._browserContext._channel.clockSkipTime({ - timeNumber: typeof time === 'number' ? time : undefined, - timeString: typeof time === 'string' ? time : undefined - }); - return result.fakeTime; + async setFixedTime(time: string | number | Date) { + await this._browserContext._channel.clockSetFixedTime(parseTime(time)); + } + + async setSystemTime(time: string | number | Date) { + await this._browserContext._channel.clockSetSystemTime(parseTime(time)); } } + +function parseTime(time: string | number | Date): { timeNumber?: number, timeString?: string } { + if (typeof time === 'number') + return { timeNumber: time }; + if (typeof time === 'string') + return { timeString: time }; + return { timeNumber: time.getTime() }; +} + +function parseTicks(ticks: string | number): { ticksNumber?: number, ticksString?: string } { + return { + ticksNumber: typeof ticks === 'number' ? ticks : undefined, + ticksString: typeof ticks === 'string' ? ticks : undefined + }; +} diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 0a60a92ebb..169b7574df 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -963,41 +963,40 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({ enabled: tBoolean, }); scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({})); -scheme.BrowserContextClockInstallFakeTimersParams = tObject({ - time: tNumber, - loopLimit: tOptional(tNumber), +scheme.BrowserContextClockFastForwardParams = tObject({ + ticksNumber: tOptional(tNumber), + ticksString: tOptional(tString), }); -scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({})); -scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({})); -scheme.BrowserContextClockRunAllTimersResult = tObject({ - fakeTime: tNumber, +scheme.BrowserContextClockFastForwardResult = tOptional(tObject({})); +scheme.BrowserContextClockFastForwardToParams = tObject({ + timeNumber: tOptional(tNumber), + timeString: tOptional(tString), }); +scheme.BrowserContextClockFastForwardToResult = tOptional(tObject({})); +scheme.BrowserContextClockInstallParams = tObject({ + timeNumber: tOptional(tNumber), + timeString: tOptional(tString), +}); +scheme.BrowserContextClockInstallResult = tOptional(tObject({})); +scheme.BrowserContextClockPauseParams = tOptional(tObject({})); +scheme.BrowserContextClockPauseResult = tOptional(tObject({})); +scheme.BrowserContextClockResumeParams = tOptional(tObject({})); +scheme.BrowserContextClockResumeResult = tOptional(tObject({})); scheme.BrowserContextClockRunForParams = tObject({ + ticksNumber: tOptional(tNumber), + ticksString: tOptional(tString), +}); +scheme.BrowserContextClockRunForResult = tOptional(tObject({})); +scheme.BrowserContextClockSetFixedTimeParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); -scheme.BrowserContextClockRunForResult = tObject({ - fakeTime: tNumber, -}); -scheme.BrowserContextClockRunToLastTimerParams = tOptional(tObject({})); -scheme.BrowserContextClockRunToLastTimerResult = tObject({ - fakeTime: tNumber, -}); -scheme.BrowserContextClockRunToNextTimerParams = tOptional(tObject({})); -scheme.BrowserContextClockRunToNextTimerResult = tObject({ - fakeTime: tNumber, -}); -scheme.BrowserContextClockSetTimeParams = tObject({ - time: tNumber, -}); -scheme.BrowserContextClockSetTimeResult = tOptional(tObject({})); -scheme.BrowserContextClockSkipTimeParams = tObject({ +scheme.BrowserContextClockSetFixedTimeResult = tOptional(tObject({})); +scheme.BrowserContextClockSetSystemTimeParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); -scheme.BrowserContextClockSkipTimeResult = tObject({ - fakeTime: tNumber, -}); +scheme.BrowserContextClockSetSystemTimeResult = tOptional(tObject({})); scheme.PageInitializer = tObject({ mainFrame: tChannel(['Frame']), viewportSize: tOptional(tObject({ diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index dd7f194a1d..3cbb0b1b0a 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -16,81 +16,74 @@ import type { BrowserContext } from './browserContext'; import * as clockSource from '../generated/clockSource'; +import { isJavaScriptErrorInEvaluate } from './javascript'; export class Clock { private _browserContext: BrowserContext; - private _scriptInjected = false; - private _clockInstalled = false; - private _now = 0; + private _scriptInstalled = false; constructor(browserContext: BrowserContext) { this._browserContext = browserContext; } - async installFakeTimers(time: number, loopLimit: number | undefined) { - await this._injectScriptIfNeeded(); - await this._addAndEvaluate(`(() => { - globalThis.__pwClock.clock?.uninstall(); - globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit })}); - })();`); - this._now = time; - this._clockInstalled = true; + async fastForward(ticks: number | string) { + await this._installIfNeeded(); + const ticksMillis = parseTicks(ticks); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${ticksMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForward(${ticksMillis})`); } - async runToNextTimer(): Promise { - this._assertInstalled(); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.next()`); - return this._now; + async fastForwardTo(ticks: number | string) { + await this._installIfNeeded(); + const timeMillis = parseTime(ticks); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForwardTo', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForwardTo(${timeMillis})`); } - async runAllTimers(): Promise { - this._assertInstalled(); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAll()`); - return this._now; + async install(time: number | string | undefined) { + await this._installIfNeeded(); + const timeMillis = time !== undefined ? parseTime(time) : Date.now(); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('install', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`); } - async runToLastTimer(): Promise { - this._assertInstalled(); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLast()`); - return this._now; + async pause() { + await this._installIfNeeded(); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pause', ${Date.now()})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.pause()`); } - async setTime(time: number) { - if (this._clockInstalled) { - const jump = time - this._now; - if (jump < 0) - throw new Error('Unable to set time into the past when fake timers are installed'); - await this._addAndEvaluate(`globalThis.__pwClock.clock.jump(${jump})`); - this._now = time; - return this._now; - } - - await this._injectScriptIfNeeded(); - await this._addAndEvaluate(`(() => { - globalThis.__pwClock.clock?.uninstall(); - globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); - })();`); - this._now = time; - return this._now; + async resume() { + await this._installIfNeeded(); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('resume', ${Date.now()})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.resume()`); } - async skipTime(time: number | string) { - const delta = parseTime(time); - await this.setTime(this._now + delta); - return this._now; + async setFixedTime(time: string | number) { + await this._installIfNeeded(); + const timeMillis = parseTime(time); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setFixedTime', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.setFixedTime(${timeMillis})`); } - async runFor(time: number | string): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwClock.clock.recordTick(${JSON.stringify(time)})`); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`); - return this._now; + async setSystemTime(time: string | number) { + await this._installIfNeeded(); + const timeMillis = parseTime(time); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setSystemTime', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.setSystemTime(${timeMillis})`); } - private async _injectScriptIfNeeded() { - if (this._scriptInjected) + async runFor(ticks: number | string) { + await this._installIfNeeded(); + const ticksMillis = parseTicks(ticks); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('runFor', ${Date.now()}, ${ticksMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.runFor(${ticksMillis})`); + } + + private async _installIfNeeded() { + if (this._scriptInstalled) return; - this._scriptInjected = true; + this._scriptInstalled = true; const script = `(() => { const module = {}; ${clockSource.source} @@ -106,37 +99,56 @@ export class Clock { private async _evaluateInFrames(script: string) { const frames = this._browserContext.pages().map(page => page.frames()).flat(); - const results = await Promise.all(frames.map(frame => frame.evaluateExpression(script))); + const results = await Promise.all(frames.map(async frame => { + try { + await frame.nonStallingEvaluateInExistingContext(script, false, 'main'); + } catch (e) { + if (isJavaScriptErrorInEvaluate(e)) + throw e; + } + })); return results[0]; } - - private _assertInstalled() { - if (!this._clockInstalled) - throw new Error('Clock is not installed'); - } } -// Taken from sinonjs/fake-timerss-src. -function parseTime(time: string | number): number { - if (typeof time === 'number') - return time; - if (!time) +/** + * Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into + * number of milliseconds. This is used to support human-readable strings passed + * to clock.tick() + */ +function parseTicks(value: number | string): number { + if (typeof value === 'number') + return value; + if (!value) return 0; + const str = value; - const strings = time.split(':'); + const strings = str.split(':'); const l = strings.length; let i = l; let ms = 0; let parsed; - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(time)) - throw new Error(`tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits`); + if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { + throw new Error( + `Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`, + ); + } while (i--) { parsed = parseInt(strings[i], 10); if (parsed >= 60) - throw new Error(`Invalid time ${time}`); + throw new Error(`Invalid time ${str}`); ms += parsed * Math.pow(60, l - i - 1); } + return ms * 1000; } + +function parseTime(epoch: string | number | undefined): number { + if (!epoch) + return 0; + if (typeof epoch === 'number') + return epoch; + return new Date(epoch).getTime(); +} diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index dd1f61f57b..86b9a5576c 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -312,32 +312,36 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.clock.installFakeTimers(params.time, params.loopLimit); + async clockFastForward(params: channels.BrowserContextClockFastForwardParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.fastForward(params.ticksString ?? params.ticksNumber ?? 0); } - async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runAllTimers() }; + async clockFastForwardTo(params: channels.BrowserContextClockFastForwardToParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.fastForwardTo(params.timeString ?? params.timeNumber ?? 0); } - async clockRunToLastTimer(params: channels.BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runToLastTimer() }; + async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined); } - async clockRunToNextTimer(params: channels.BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runToNextTimer() }; + async clockPause(params: channels.BrowserContextClockPauseParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.pause(); } - async clockSetTime(params: channels.BrowserContextClockSetTimeParams, metadata?: CallMetadata | undefined): Promise { - await this._context.clock.setTime(params.time); - } - - async clockSkipTime(params: channels.BrowserContextClockSkipTimeParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.skipTime(params.timeString || params.timeNumber || 0) }; + async clockResume(params: channels.BrowserContextClockResumeParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.resume(); } async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runFor(params.timeString || params.timeNumber || 0) }; + await this._context.clock.runFor(params.ticksString ?? params.ticksNumber ?? 0); + } + + async clockSetFixedTime(params: channels.BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.setFixedTime(params.timeString ?? params.timeNumber ?? 0); + } + + async clockSetSystemTime(params: channels.BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.setSystemTime(params.timeString ?? params.timeNumber ?? 0); } async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise { diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index 508d7a59e5..5f6d4cf613 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -26,7 +26,6 @@ export type ClockMethods = { export type ClockConfig = { now?: number | Date; - loopLimit?: number; }; export type InstallConfig = ClockConfig & { @@ -53,26 +52,39 @@ type Timer = { }; interface Embedder { - postTask(task: () => void): void; - postTaskPeriodically(task: () => void, delay: number): () => void; + dateNow(): number; + performanceNow(): DOMHighResTimeStamp; + setTimeout(task: () => void, timeout?: number): () => void; + setInterval(task: () => void, delay: number): () => void; } +type Time = { + // ms since Epoch + time: number; + // Ticks since the session began (ala performance.now) + ticks: number; + // Whether fixed time was set. + isFixedTime: boolean; + // Origin time since Epoch when session started. + origin: number; +}; + +type LogEntryType = 'fastForward' | 'fastForwardTo' | 'install' | 'pause' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime'; + export class ClockController { - readonly timeOrigin: number; - private _now: { time: number, ticks: number, timeFrozen: boolean }; - private _loopLimit: number; + readonly _now: Time; private _duringTick = false; private _timers = new Map(); private _uniqueTimerId = idCounterStart; private _embedder: Embedder; readonly disposables: (() => void)[] = []; + private _log: { type: LogEntryType, time: number, param?: number }[] = []; + private _realTime: { startTicks: number, lastSyncTicks: number } | undefined; + private _currentRealTimeTimer: { callAt: number, dispose: () => void } | undefined; - constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) { - const start = Math.floor(getEpoch(startDate)); - this.timeOrigin = start; - this._now = { time: start, ticks: 0, timeFrozen: false }; + constructor(embedder: Embedder) { + this._now = { time: 0, isFixedTime: false, ticks: 0, origin: -1 }; this._embedder = embedder; - this._loopLimit = loopLimit; } uninstall() { @@ -81,109 +93,147 @@ export class ClockController { } now(): number { + this._replayLogOnce(); return this._now.time; } - setTime(now: Date | number, options: { freeze?: boolean } = {}) { - this._now.time = getEpoch(now); - this._now.timeFrozen = !!options.freeze; + install(time: number) { + this._replayLogOnce(); + this._innerSetTime(time); + } + + setSystemTime(time: number) { + this._replayLogOnce(); + this._innerSetTime(time); + } + + setFixedTime(time: number) { + this._replayLogOnce(); + this._innerSetFixedTime(time); } performanceNow(): DOMHighResTimeStamp { + this._replayLogOnce(); return this._now.ticks; } + private _innerSetTime(time: number) { + this._now.time = time; + this._now.isFixedTime = false; + if (this._now.origin < 0) + this._now.origin = this._now.time; + } + + private _innerSetFixedTime(time: number) { + this._innerSetTime(time); + this._now.isFixedTime = true; + } + private _advanceNow(toTicks: number) { - if (!this._now.timeFrozen) + if (!this._now.isFixedTime) this._now.time += toTicks - this._now.ticks; this._now.ticks = toTicks; } - private async _doTick(msFloat: number): Promise { - if (msFloat < 0) - throw new TypeError('Negative ticks are not supported'); + async log(type: LogEntryType, time: number, param?: number) { + this._log.push({ type, time, param }); + } + + async runFor(ticks: number) { + this._replayLogOnce(); + if (ticks < 0) + throw new TypeError('Negative ticks are not supported'); + await this._runTo(this._now.ticks + ticks); + } + + private async _runTo(tickTo: number) { + if (this._now.ticks > tickTo) + return; - const ms = Math.floor(msFloat); - const tickTo = this._now.ticks + ms; - let tickFrom = this._now.ticks; - let previous = this._now.ticks; let firstException: Error | undefined; - let timer = this._firstTimerInRange(tickFrom, tickTo); - while (timer && tickFrom <= tickTo) { - tickFrom = timer.callAt; - const error = await this._callTimer(timer).catch(e => e); - firstException = firstException || error; - timer = this._firstTimerInRange(previous, tickTo); - previous = tickFrom; + while (true) { + const result = await this._callFirstTimer(tickTo); + if (!result.timerFound) + break; + firstException = firstException || result.error; } this._advanceNow(tickTo); if (firstException) throw firstException; - - return this._now.ticks; } - async recordTick(tickValue: string | number) { - const msFloat = parseTime(tickValue); - this._advanceNow(this._now.ticks + msFloat); + pause() { + this._replayLogOnce(); + this._innerPause(); } - async tick(tickValue: string | number): Promise { - return await this._doTick(parseTime(tickValue)); + private _innerPause() { + this._realTime = undefined; + this._updateRealTimeTimer(); } - async next(): Promise { - const timer = this._firstTimer(); - if (!timer) - return this._now.ticks; - await this._callTimer(timer); - return this._now.ticks; + resume() { + this._replayLogOnce(); + this._innerResume(); } - async runToFrame(): Promise { - return this.tick(this.getTimeToNextFrame()); + private _innerResume() { + const now = this._embedder.performanceNow(); + this._realTime = { startTicks: now, lastSyncTicks: now }; + this._updateRealTimeTimer(); } - async runAll(): Promise { - for (let i = 0; i < this._loopLimit; i++) { - const numTimers = this._timers.size; - if (numTimers === 0) - return this._now.ticks; - - await this.next(); + private _updateRealTimeTimer() { + if (!this._realTime) { + this._currentRealTimeTimer?.dispose(); + this._currentRealTimeTimer = undefined; + return; } - const excessJob = this._firstTimer(); - if (!excessJob) - return this._now.ticks; - throw this._getInfiniteLoopError(excessJob); + const firstTimer = this._firstTimer(); + + // Either run the next timer or move time in 100ms chunks. + const callAt = Math.min(firstTimer ? firstTimer.callAt : this._now.ticks + maxTimeout, this._now.ticks + 100); + if (this._currentRealTimeTimer && this._currentRealTimeTimer.callAt < callAt) + return; + + if (this._currentRealTimeTimer) { + this._currentRealTimeTimer.dispose(); + this._currentRealTimeTimer = undefined; + } + + this._currentRealTimeTimer = { + callAt, + dispose: this._embedder.setTimeout(() => { + const now = Math.ceil(this._embedder.performanceNow()); + this._currentRealTimeTimer = undefined; + const sinceLastSync = now - this._realTime!.lastSyncTicks; + this._realTime!.lastSyncTicks = now; + // eslint-disable-next-line no-console + this._runTo(this._now.ticks + sinceLastSync).catch(e => console.error(e)).then(() => this._updateRealTimeTimer()); + }, callAt - this._now.ticks), + }; } - async runToLast(): Promise { - const timer = this._lastTimer(); - if (!timer) - return this._now.ticks; - return await this.tick(timer.callAt - this._now.ticks); - } - - reset() { - this._timers.clear(); - this._now = { time: this.timeOrigin, ticks: 0, timeFrozen: false }; - } - - async jump(tickValue: string | number): Promise { - const msFloat = parseTime(tickValue); - const ms = Math.floor(msFloat); - + async fastForward(ticks: number) { + this._replayLogOnce(); + const ms = ticks | 0; for (const timer of this._timers.values()) { if (this._now.ticks + ms > timer.callAt) timer.callAt = this._now.ticks + ms; } - return await this.tick(ms); + await this.runFor(ms); + } + + async fastForwardTo(time: number) { + this._replayLogOnce(); + const ticks = time - this._now.time; + await this.fastForward(ticks); } addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { + this._replayLogOnce(); if (options.func === undefined) throw new Error('Callback must be provided to timer calls'); @@ -204,56 +254,56 @@ export class ClockController { error: new Error(), }; this._timers.set(timer.id, timer); + if (this._realTime) + this._updateRealTimeTimer(); return timer.id; } - private _firstTimerInRange(from: number, to: number): Timer | null { - let firstTimer: Timer | null = null; - for (const timer of this._timers.values()) { - const isInRange = inRange(from, to, timer); - if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1)) - firstTimer = timer; - } - return firstTimer; - } - countTimers() { return this._timers.size; } - private _firstTimer(): Timer | null { + private _firstTimer(beforeTick?: number): Timer | null { let firstTimer: Timer | null = null; for (const timer of this._timers.values()) { - if (!firstTimer || compareTimers(firstTimer, timer) === 1) + const isInRange = beforeTick === undefined || timer.callAt <= beforeTick; + if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1)) firstTimer = timer; } return firstTimer; } - private _lastTimer(): Timer | null { - let lastTimer: Timer | null = null; + private _takeFirstTimer(beforeTick?: number): Timer | null { + const timer = this._firstTimer(beforeTick); + if (!timer) + return null; - for (const timer of this._timers.values()) { - if (!lastTimer || compareTimers(lastTimer, timer) === -1) - lastTimer = timer; - } - return lastTimer; - } - - private async _callTimer(timer: Timer) { this._advanceNow(timer.callAt); if (timer.type === TimerType.Interval) this._timers.get(timer.id)!.callAt += timer.delay; else this._timers.delete(timer.id); + return timer; + } + + private async _callFirstTimer(beforeTick: number): Promise<{ timerFound: boolean, error?: Error }> { + const timer = this._takeFirstTimer(beforeTick); + if (!timer) + return { timerFound: false }; this._duringTick = true; try { if (typeof timer.func !== 'function') { - (() => { eval(timer.func); })(); - return; + let error: Error | undefined; + try { + (() => { eval(timer.func); })(); + } catch (e) { + error = e; + } + await new Promise(f => this._embedder.setTimeout(f)); + return { timerFound: true, error }; } let args = timer.args; @@ -262,67 +312,26 @@ export class ClockController { else if (timer.type === TimerType.IdleCallback) args = [{ didTimeout: false, timeRemaining: () => 0 }]; - timer.func.apply(null, args); - await new Promise(f => this._embedder.postTask(f)); + let error: Error | undefined; + try { + timer.func.apply(null, args); + } catch (e) { + error = e; + } + await new Promise(f => this._embedder.setTimeout(f)); + return { timerFound: true, error }; } finally { this._duringTick = false; } } - private _getInfiniteLoopError(job: Timer) { - const infiniteLoopError = new Error( - `Aborting after running ${this._loopLimit} timers, assuming an infinite loop!`, - ); - - if (!job.error) - return infiniteLoopError; - - // pattern never matched in Node - const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/; - const clockMethodPattern = new RegExp( - String(Object.keys(this).join('|')), - ); - - let matchedLineIndex = -1; - job.error.stack!.split('\n').some((line, i) => { - // If we've matched a computed target line (e.g. setTimeout) then we - // don't need to look any further. Return true to stop iterating. - const matchedComputedTarget = line.match(computedTargetPattern); - /* istanbul ignore if */ - if (matchedComputedTarget) { - matchedLineIndex = i; - return true; - } - - // If we've matched a clock method line, then there may still be - // others further down the trace. Return false to keep iterating. - const matchedClockMethod = line.match(clockMethodPattern); - if (matchedClockMethod) { - matchedLineIndex = i; - return false; - } - - // If we haven't matched anything on this line, but we matched - // previously and set the matched line index, then we can stop. - // If we haven't matched previously, then we should keep iterating. - return matchedLineIndex >= 0; - }); - - const funcName = typeof job.func === 'function' ? job.func.name : 'anonymous'; - const stack = `${infiniteLoopError}\n${job.type || 'Microtask'} - ${funcName}\n${job.error.stack! - .split('\n') - .slice(matchedLineIndex + 1) - .join('\n')}`; - - infiniteLoopError.stack = stack; - return infiniteLoopError; - } - getTimeToNextFrame() { return 16 - this._now.ticks % 16; } clearTimer(timerId: number, type: TimerType) { + this._replayLogOnce(); + if (!timerId) { // null appears to be allowed in most browsers, and appears to be // relied upon by some libraries, like Bootstrap carousel @@ -356,64 +365,49 @@ export class ClockController { } } - advanceAutomatically(advanceTimeDelta: number = 20): () => void { - return this._embedder.postTaskPeriodically( - () => this._doTick(advanceTimeDelta!), - advanceTimeDelta, - ); + private _replayLogOnce() { + if (!this._log.length) + return; + + let lastLogTime = -1; + let isPaused = false; + + for (const { type, time, param } of this._log) { + if (!isPaused && lastLogTime !== -1) + this._advanceNow(this._now.ticks + time - lastLogTime); + lastLogTime = time; + + if (type === 'install') { + this._innerSetTime(param!); + } else if (type === 'fastForward' || type === 'runFor') { + this._advanceNow(this._now.ticks + param!); + } else if (type === 'fastForwardTo') { + this._innerSetTime(param!); + } else if (type === 'pause') { + this._innerPause(); + isPaused = true; + } else if (type === 'resume') { + this._innerResume(); + isPaused = false; + } else if (type === 'setFixedTime') { + this._innerSetFixedTime(param!); + } else if (type === 'setSystemTime') { + this._innerSetTime(param!); + } + } + + if (!isPaused && lastLogTime > 0) + this._advanceNow(this._now.ticks + this._embedder.dateNow() - lastLogTime); + + this._log.length = 0; } } -function getEpoch(epoch: Date | number | undefined): number { - if (!epoch) - return 0; - if (typeof epoch !== 'number') - return epoch.getTime(); - return epoch; -} - -function inRange(from: number, to: number, timer: Timer): boolean { - return timer && timer.callAt >= from && timer.callAt <= to; -} - -/** - * Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into - * number of milliseconds. This is used to support human-readable strings passed - * to clock.tick() - */ -function parseTime(value: number | string): number { - if (typeof value === 'number') - return value; - if (!value) - return 0; - const str = value; - - const strings = str.split(':'); - const l = strings.length; - let i = l; - let ms = 0; - let parsed; - - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { - throw new Error( - `Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`, - ); - } - - while (i--) { - parsed = parseInt(strings[i], 10); - if (parsed >= 60) - throw new Error(`Invalid time ${str}`); - ms += parsed * Math.pow(60, l - i - 1); - } - - return ms * 1000; -} - function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date { - let prop; - for (prop of Object.keys(source) as (keyof DateConstructor)[]) - target[prop] = source[prop]; + for (const prop in source) { + if (source.hasOwnProperty(prop)) + target[prop] = (source as any)[prop]; + } target.toString = () => source.toString(); target.prototype = source.prototype; target.parse = source.parse; @@ -485,7 +479,7 @@ function createIntl(clock: ClockController, NativeIntl: typeof Intl): typeof Int * All properties of Intl are non-enumerable, so we need * to do a bit of work to get them out. */ - for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[]) + for (const key of Object.getOwnPropertyNames(NativeIntl) as (keyof typeof Intl)[]) ClockIntl[key] = NativeIntl[key]; ClockIntl.DateTimeFormat = function(...args: any[]) { @@ -644,8 +638,8 @@ function getClearHandler(type: TimerType) { function fakePerformance(clock: ClockController, performance: Performance): Performance { const result: any = { now: () => clock.performanceNow(), - timeOrigin: clock.timeOrigin, }; + result.__defineGetter__('timeOrigin', () => clock._now.origin || 0); // eslint-disable-next-line no-proto for (const key of Object.keys((performance as any).__proto__)) { if (key === 'now' || key === 'timeOrigin') @@ -658,19 +652,22 @@ function fakePerformance(clock: ClockController, performance: Performance): Perf return result; } -export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { +export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { const originals = platformOriginals(globalObject); - const embedder = { - postTask: (task: () => void) => { - originals.bound.setTimeout(task, 0); + const embedder: Embedder = { + dateNow: () => originals.raw.Date.now(), + performanceNow: () => originals.raw.performance!.now(), + setTimeout: (task: () => void, timeout?: number) => { + const timerId = originals.bound.setTimeout(task, timeout); + return () => originals.bound.clearTimeout(timerId); }, - postTaskPeriodically: (task: () => void, delay: number) => { - const intervalId = globalObject.setInterval(task, delay); + setInterval: (task: () => void, delay: number) => { + const intervalId = originals.bound.setInterval(task, delay); return () => originals.bound.clearInterval(intervalId); }, }; - const clock = new ClockController(embedder, config.now, config.loopLimit); + const clock = new ClockController(embedder); const api = createApi(clock, originals.bound); return { clock, api, originals: originals.raw }; } @@ -682,7 +679,7 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install throw new TypeError(`Can't install fake timers twice on the same global object.`); } - const { clock, api, originals } = createClock(globalObject, config); + const { clock, api, originals } = createClock(globalObject); const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[]; for (const method of toFake) { @@ -706,11 +703,10 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install } export function inject(globalObject: WindowOrWorkerGlobalScope) { + const { clock: controller } = install(globalObject); + controller.resume(); return { - install: (config: InstallConfig) => { - const { clock } = install(globalObject, config); - return clock; - }, + controller, builtin: platformOriginals(globalObject).bound, }; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 10dd00f198..7fe30e2ef2 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17247,8 +17247,40 @@ export interface BrowserServer { * controlled by the same clock. */ export interface Clock { + /** + * Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user + * closing the laptop lid for a while and reopening it later, after given time. + * + * **Usage** + * + * ```js + * await page.clock.fastForward(1000); + * await page.clock.fastForward('30:00'); + * ``` + * + * @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are + * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + */ + fastForward(ticks: number|string): Promise; + + /** + * Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user + * closing the laptop lid for a while and reopening it at the specified time. + * + * **Usage** + * + * ```js + * await page.clock.fastForwardTo(new Date('2020-02-02')); + * await page.clock.fastForwardTo('2020-02-02'); + * ``` + * + * @param time + */ + fastForwardTo(time: number|string|Date): Promise; + /** * Install fake implementations for the following time-related functions: + * - `Date` * - `setTimeout` * - `clearTimeout` * - `setInterval` @@ -17261,34 +17293,33 @@ export interface Clock { * * Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, * and control the behavior of time-dependent functions. See - * [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) and - * [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) for more information. - * @param time Install fake timers with the specified base time. + * [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for) and + * [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward) for more information. * @param options */ - installFakeTimers(time: number|Date, options?: { + install(options?: { /** - * The maximum number of timers that will be run in - * [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`. + * Time to initialize with, current system time by default. */ - loopLimit?: number; + time?: number|string|Date; }): Promise; /** - * Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be - * run as well. Fake timers must be installed. Returns fake milliseconds since the unix epoch. - * - * **Details** - * - * This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, - * or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there is an infinite - * loop of timers and throws an error. + * Pause timers. Once this method is called, no timers are fired unless + * [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for), + * [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward), + * [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to) or + * [clock.resume()](https://playwright.dev/docs/api/class-clock#clock-resume) is called. */ - runAllTimers(): Promise; + pause(): Promise; /** - * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Fake timers must - * be installed. Returns fake milliseconds since the unix epoch. + * Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual. + */ + resume(): Promise; + + /** + * Advance the clock, firing all the time-related callbacks. * * **Usage** * @@ -17297,55 +17328,41 @@ export interface Clock { * await page.clock.runFor('30:00'); * ``` * - * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are + * @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. */ - runFor(time: number|string): Promise; + runFor(ticks: number|string): Promise; /** - * This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as - * necessary. If new timers are added while it is executing they will be run only if they would occur before this - * time. This is useful when you want to run a test to completion, but the test recursively sets timers that would - * cause runAll to trigger an infinite loop warning. Fake timers must be installed. Returns fake milliseconds since - * the unix epoch. - */ - runToLastTimer(): Promise; - - /** - * Advances the clock to the moment of the first scheduled timer, firing it. Fake timers must be installed. Returns - * fake milliseconds since the unix epoch. - */ - runToNextTimer(): Promise; - - /** - * Set the clock to the specified time. - * - * When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as - * a browser) being put to sleep and resumed later, skipping intermediary timers. - * @param time - */ - setTime(time: number|Date): Promise; - - /** - * Advance the clock by jumping forward in time, equivalent to running - * [clock.setTime(time)](https://playwright.dev/docs/api/class-clock#clock-set-time) with the new target time. - * - * When fake timers are installed, [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) - * only fires due timers at most once, while - * [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) fires all the timers up to the - * current time. Returns fake milliseconds since the unix epoch. + * Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running. * * **Usage** * * ```js - * await page.clock.skipTime(1000); - * await page.clock.skipTime('30:00'); + * await page.clock.setFixedTime(Date.now()); + * await page.clock.setFixedTime(new Date('2020-02-02')); + * await page.clock.setFixedTime('2020-02-02'); * ``` * - * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are - * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + * @param time Time to be set. */ - skipTime(time: number|string): Promise; + setFixedTime(time: number|string|Date): Promise; + + /** + * Sets current system time but does not trigger any timers, unlike + * [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to). + * + * **Usage** + * + * ```js + * await page.clock.setSystemTime(Date.now()); + * await page.clock.setSystemTime(new Date('2020-02-02')); + * await page.clock.setSystemTime('2020-02-02'); + * ``` + * + * @param time + */ + setSystemTime(time: number|string|Date): Promise; } /** diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index c8523e8de5..85bd3a8ffa 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1460,13 +1460,14 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise; createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise; updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise; - clockInstallFakeTimers(params: BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata): Promise; - clockRunAllTimers(params?: BrowserContextClockRunAllTimersParams, metadata?: CallMetadata): Promise; + clockFastForward(params: BrowserContextClockFastForwardParams, metadata?: CallMetadata): Promise; + clockFastForwardTo(params: BrowserContextClockFastForwardToParams, metadata?: CallMetadata): Promise; + clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise; + clockPause(params?: BrowserContextClockPauseParams, metadata?: CallMetadata): Promise; + clockResume(params?: BrowserContextClockResumeParams, metadata?: CallMetadata): Promise; clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise; - clockRunToLastTimer(params?: BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata): Promise; - clockRunToNextTimer(params?: BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata): Promise; - clockSetTime(params: BrowserContextClockSetTimeParams, metadata?: CallMetadata): Promise; - clockSkipTime(params: BrowserContextClockSkipTimeParams, metadata?: CallMetadata): Promise; + clockSetFixedTime(params: BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata): Promise; + clockSetSystemTime(params: BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata): Promise; } export type BrowserContextBindingCallEvent = { binding: BindingCallChannel, @@ -1755,58 +1756,66 @@ export type BrowserContextUpdateSubscriptionOptions = { }; export type BrowserContextUpdateSubscriptionResult = void; -export type BrowserContextClockInstallFakeTimersParams = { - time: number, - loopLimit?: number, +export type BrowserContextClockFastForwardParams = { + ticksNumber?: number, + ticksString?: string, }; -export type BrowserContextClockInstallFakeTimersOptions = { - loopLimit?: number, +export type BrowserContextClockFastForwardOptions = { + ticksNumber?: number, + ticksString?: string, }; -export type BrowserContextClockInstallFakeTimersResult = void; -export type BrowserContextClockRunAllTimersParams = {}; -export type BrowserContextClockRunAllTimersOptions = {}; -export type BrowserContextClockRunAllTimersResult = { - fakeTime: number, -}; -export type BrowserContextClockRunForParams = { +export type BrowserContextClockFastForwardResult = void; +export type BrowserContextClockFastForwardToParams = { timeNumber?: number, timeString?: string, }; +export type BrowserContextClockFastForwardToOptions = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockFastForwardToResult = void; +export type BrowserContextClockInstallParams = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockInstallOptions = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockInstallResult = void; +export type BrowserContextClockPauseParams = {}; +export type BrowserContextClockPauseOptions = {}; +export type BrowserContextClockPauseResult = void; +export type BrowserContextClockResumeParams = {}; +export type BrowserContextClockResumeOptions = {}; +export type BrowserContextClockResumeResult = void; +export type BrowserContextClockRunForParams = { + ticksNumber?: number, + ticksString?: string, +}; export type BrowserContextClockRunForOptions = { + ticksNumber?: number, + ticksString?: string, +}; +export type BrowserContextClockRunForResult = void; +export type BrowserContextClockSetFixedTimeParams = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockRunForResult = { - fakeTime: number, -}; -export type BrowserContextClockRunToLastTimerParams = {}; -export type BrowserContextClockRunToLastTimerOptions = {}; -export type BrowserContextClockRunToLastTimerResult = { - fakeTime: number, -}; -export type BrowserContextClockRunToNextTimerParams = {}; -export type BrowserContextClockRunToNextTimerOptions = {}; -export type BrowserContextClockRunToNextTimerResult = { - fakeTime: number, -}; -export type BrowserContextClockSetTimeParams = { - time: number, -}; -export type BrowserContextClockSetTimeOptions = { - -}; -export type BrowserContextClockSetTimeResult = void; -export type BrowserContextClockSkipTimeParams = { +export type BrowserContextClockSetFixedTimeOptions = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockSkipTimeOptions = { +export type BrowserContextClockSetFixedTimeResult = void; +export type BrowserContextClockSetSystemTimeParams = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockSkipTimeResult = { - fakeTime: number, +export type BrowserContextClockSetSystemTimeOptions = { + timeNumber?: number, + timeString?: string, }; +export type BrowserContextClockSetSystemTimeResult = void; export interface BrowserContextEvents { 'bindingCall': BrowserContextBindingCallEvent; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 7f6bd3bfe1..65fc2db4de 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1204,40 +1204,39 @@ BrowserContext: - requestFailed enabled: boolean - clockInstallFakeTimers: + clockFastForward: parameters: - time: number - loopLimit: number? + ticksNumber: number? + ticksString: string? - clockRunAllTimers: - returns: - fakeTime: number + clockFastForwardTo: + parameters: + timeNumber: number? + timeString: string? + + clockInstall: + parameters: + timeNumber: number? + timeString: string? + + clockPause: + + clockResume: clockRunFor: parameters: - timeNumber: number? - timeString: string? - returns: - fakeTime: number + ticksNumber: number? + ticksString: string? - clockRunToLastTimer: - returns: - fakeTime: number - - clockRunToNextTimer: - returns: - fakeTime: number - - clockSetTime: - parameters: - time: number - - clockSkipTime: + clockSetFixedTime: + parameters: + timeNumber: number? + timeString: string? + + clockSetSystemTime: parameters: timeNumber: number? timeString: string? - returns: - fakeTime: number events: diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index 554b49d1a7..a77356c172 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -18,8 +18,9 @@ import { test, expect } from '@playwright/test'; import { createClock as rawCreateClock, install as rawInstall } from '../../packages/playwright-core/src/server/injected/clock'; import type { InstallConfig, ClockController, ClockMethods } from '../../packages/playwright-core/src/server/injected/clock'; -const createClock = (now?: Date | number, loopLimit?: number): ClockController & ClockMethods => { - const { clock, api } = rawCreateClock(globalThis, { now, loopLimit }); +const createClock = (now?: number): ClockController & ClockMethods => { + const { clock, api } = rawCreateClock(globalThis); + clock.setSystemTime(now || 0); for (const key of Object.keys(api)) clock[key] = api[key]; return clock as ClockController & ClockMethods; @@ -27,26 +28,25 @@ const createClock = (now?: Date | number, loopLimit?: number): ClockController & type ClockFixtures = { clock: ClockController & ClockMethods; - now: Date | number | undefined; - loopLimit: number | undefined; - install: (config?: InstallConfig) => ClockController & ClockMethods; + now: number | undefined; + install: (now?: number) => ClockController & ClockMethods; installEx: (config?: InstallConfig) => { clock: ClockController, api: ClockMethods, originals: ClockMethods }; }; const it = test.extend({ - clock: async ({ now, loopLimit }, use) => { - const clock = createClock(now, loopLimit); + clock: async ({ now }, use) => { + const clock = createClock(now); await use(clock); }, now: undefined, - loopLimit: undefined, - install: async ({}, use) => { let clockObject: ClockController & ClockMethods; - const install = (config?: InstallConfig) => { - const { clock, api } = rawInstall(globalThis, config); + const install = (now?: number) => { + const { clock, api } = rawInstall(globalThis); + if (now) + clock.setSystemTime(now); for (const key of Object.keys(api)) clock[key] = api[key]; clockObject = clock as ClockController & ClockMethods; @@ -98,7 +98,7 @@ it.describe('setTimeout', () => { clock1.setTimeout(stubs[0], 100); clock2.setTimeout(stubs[1], 100); - await clock2.tick(200); + await clock2.runFor(200); expect(stubs[0].called).toBeFalsy(); expect(stubs[1].called).toBeTruthy(); @@ -110,7 +110,7 @@ it.describe('setTimeout', () => { evalCalled = true; // @ts-expect-error }, '10'); - await clock.tick(10); + await clock.runFor(10); expect(evalCalled).toBeTruthy(); }); @@ -120,7 +120,7 @@ it.describe('setTimeout', () => { evalCalled = true; // @ts-expect-error }, 'string'); - await clock.tick(10); + await clock.runFor(10); expect(evalCalled).toBeTruthy(); }); @@ -128,67 +128,52 @@ it.describe('setTimeout', () => { it('passes setTimeout parameters', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 2, 'the first', 'the second'); - await clock.tick(3); + await clock.runFor(3); expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); }); it('calls correct timeout on recursive tick', async ({ clock }) => { const stub = createStub(); const recurseCallback = () => { - void clock.tick(100); + void clock.runFor(100); }; clock.setTimeout(recurseCallback, 50); clock.setTimeout(stub, 100); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeTruthy(); }); it('does not depend on this', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - await clock.tick(100); + await clock.runFor(100); expect(stub.called).toBeTruthy(); }); it('is not influenced by forward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 5000); - await clock.tick(1000); - clock.setTime(new clock.Date().getTime() + 1000); - await clock.tick(3990); + await clock.runFor(1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); + await clock.runFor(3990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('is not influenced by backward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 5000); - await clock.tick(1000); - clock.setTime(new clock.Date().getTime() - 1000); - await clock.tick(3990); + await clock.runFor(1000); + clock.setSystemTime(new clock.Date().getTime() - 1000); + await clock.runFor(3990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); - it('handles Infinity and negative Infinity correctly', async ({ clock }) => { - const calls = []; - clock.setTimeout(() => { - calls.push('NaN'); - }, NaN); - clock.setTimeout(() => { - calls.push('Infinity'); - }, Number.POSITIVE_INFINITY); - clock.setTimeout(() => { - calls.push('-Infinity'); - }, Number.NEGATIVE_INFINITY); - await clock.runAll(); - expect(calls).toEqual(['NaN', 'Infinity', '-Infinity']); - }); - it.describe('use of eval when not in node', () => { it.beforeEach(() => { globalThis.evalCalled = false; @@ -200,7 +185,7 @@ it.describe('setTimeout', () => { it('evals non-function callbacks', async ({ clock }) => { clock.setTimeout('globalThis.evalCalled = true', 10); - await clock.tick(10); + await clock.runFor(10); expect(globalThis.evalCalled).toBeTruthy(); }); @@ -209,7 +194,7 @@ it.describe('setTimeout', () => { const x = 15; try { clock.setTimeout('x', x); - await clock.tick(x); + await clock.runFor(x); expect(true).toBeFalsy(); } catch (e) { expect(e).toBeInstanceOf(ReferenceError); @@ -218,25 +203,25 @@ it.describe('setTimeout', () => { }); }); -it.describe('tick', () => { +it.describe('runFor', () => { it('triggers immediately without specified delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub); - await clock.tick(0); + await clock.runFor(0); expect(stub.called).toBeTruthy(); }); it('does not trigger without sufficient delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - await clock.tick(10); + await clock.runFor(10); expect(stub.called).toBeFalsy(); }); it('triggers after sufficient delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - await clock.tick(100); + await clock.runFor(100); expect(stub.called).toBeTruthy(); }); @@ -244,7 +229,7 @@ it.describe('tick', () => { const spies = [createStub(), createStub()]; clock.setTimeout(spies[0], 100); clock.setTimeout(spies[1], 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); }); @@ -255,7 +240,7 @@ it.describe('tick', () => { clock.setTimeout(spies[1], 100); clock.setTimeout(spies[2], 99); clock.setTimeout(spies[3], 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); expect(spies[2].called).toBeTruthy(); @@ -274,19 +259,19 @@ it.describe('tick', () => { // First spy calls another setTimeout with delay=0 clock.setTimeout(spies[0], 0); clock.setTimeout(spies[2], 10); - await clock.tick(10); + await clock.runFor(10); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); expect(spies[2].called).toBeTruthy(); }); it('waits after setTimeout was called', async ({ clock }) => { - await clock.tick(100); + await clock.runFor(100); const stub = createStub(); clock.setTimeout(stub, 150); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); - await clock.tick(100); + await clock.runFor(100); expect(stub.called).toBeTruthy(); }); @@ -294,19 +279,19 @@ it.describe('tick', () => { const stubs = [createStub(), createStub(), createStub()]; clock.setTimeout(stubs[0], 100); clock.setTimeout(stubs[1], 120); - await clock.tick(10); - await clock.tick(89); + await clock.runFor(10); + await clock.runFor(89); expect(stubs[0].called).toBeFalsy(); expect(stubs[1].called).toBeFalsy(); clock.setTimeout(stubs[2], 20); - await clock.tick(1); + await clock.runFor(1); expect(stubs[0].called).toBeTruthy(); expect(stubs[1].called).toBeFalsy(); expect(stubs[2].called).toBeFalsy(); - await clock.tick(19); + await clock.runFor(19); expect(stubs[1].called).toBeFalsy(); expect(stubs[2].called).toBeTruthy(); - await clock.tick(1); + await clock.runFor(1); expect(stubs[1].called).toBeTruthy(); }); @@ -316,7 +301,7 @@ it.describe('tick', () => { clock.setTimeout(stubs[0], 100); clock.setTimeout(stubs[1], 120); - await expect(clock.tick(120)).rejects.toThrow(); + await expect(clock.runFor(120)).rejects.toThrow(); expect(stubs[0].called).toBeTruthy(); expect(stubs[1].called).toBeTruthy(); @@ -325,7 +310,7 @@ it.describe('tick', () => { it('calls function with global object or null (strict mode) as this', async ({ clock }) => { const stub = createStub().throws(); clock.setTimeout(stub, 100); - await expect(clock.tick(100)).rejects.toThrow(); + await expect(clock.runFor(100)).rejects.toThrow(); expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); }); @@ -334,7 +319,7 @@ it.describe('tick', () => { clock.setTimeout(spies[0], 13); clock.setTimeout(spies[1], 11); - await clock.tick(15); + await clock.runFor(15); expect(spies[1].calledBefore(spies[0])).toBeTruthy(); }); @@ -346,7 +331,7 @@ it.describe('tick', () => { spy(new clock.Date().getTime()); }, 10); - await clock.tick(100); + await clock.runFor(100); expect(spy.callCount).toBe(10); expect(spy.calledWith(10)).toBeTruthy(); @@ -364,7 +349,7 @@ it.describe('tick', () => { it('fires timer in intervals of 13', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 13); - await clock.tick(500); + await clock.runFor(500); expect(spy.callCount).toBe(38); }); @@ -372,7 +357,7 @@ it.describe('tick', () => { const spy = createStub(); // @ts-expect-error clock.setInterval(spy, '13'); - await clock.tick(500); + await clock.runFor(500); expect(spy.callCount).toBe(38); }); @@ -388,7 +373,7 @@ it.describe('tick', () => { spy10(new clock.Date().getTime()); }, 10); - await clock.tick(500); + await clock.runFor(500); expect(spy13.callCount).toBe(38); expect(spy10.callCount).toBe(50); @@ -402,7 +387,7 @@ it.describe('tick', () => { clock.setInterval(spies[0], 10); clock.setTimeout(spies[1], 50); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); expect(spies[0].callCount).toBe(10); @@ -417,62 +402,18 @@ it.describe('tick', () => { clock.clearInterval(id); }); id = clock.setInterval(callback, 10); - await clock.tick(100); + await clock.runFor(100); expect(callback.callCount).toBe(3); }); - it('passes 8 seconds', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 4000); - await clock.tick('08'); - expect(spy.callCount).toBe(2); - }); - - it('passes 1 minute', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 6000); - await clock.tick('01:00'); - expect(spy.callCount).toBe(10); - }); - - it('passes 2 hours, 34 minutes and 10 seconds', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - await clock.tick('02:34:10'); - expect(spy.callCount).toBe(925); - }); - - it('throws for invalid format', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - - await expect(clock.tick('12:02:34:10')).rejects.toThrow(); - - expect(spy.callCount).toBe(0); - }); - - it('throws for invalid minutes', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - await expect(clock.tick('67:10')).rejects.toThrow(); - expect(spy.callCount).toBe(0); - }); - it('throws for negative minutes', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 10000); - await expect(clock.tick('-7:10')).rejects.toThrow(); + await expect(clock.runFor(-7)).rejects.toThrow(); expect(spy.callCount).toBe(0); }); - it('treats missing argument as 0', async ({ clock }) => { - // @ts-expect-error - await clock.tick(); - - expect(clock.now()).toBe(0); - }); - it('fires nested setTimeout calls properly', async ({ clock }) => { let i = 0; const callback = () => { @@ -483,7 +424,7 @@ it.describe('tick', () => { }; callback(); - await clock.tick(1000); + await clock.runFor(1000); expect(i).toBe(11); }); @@ -492,74 +433,69 @@ it.describe('tick', () => { throw new Error('oh no!'); }; clock.setTimeout(callback, 1000); - await expect(clock.tick(1000)).rejects.toThrow(); - }); - - it('returns the current now value', async ({ clock }) => { - const value = await clock.tick(200); - expect(clock.now()).toBe(value); + await expect(clock.runFor(1000)).rejects.toThrow(); }); it('is not influenced by forward system clock changes', async ({ clock }) => { const callback = () => { - clock.setTime(new clock.Date().getTime() + 1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await clock.tick(1990); + await clock.runFor(1990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('is not influenced by forward system clock changes 2', async ({ clock }) => { const callback = () => { - clock.setTime(new clock.Date().getTime() - 1000); + clock.setSystemTime(new clock.Date().getTime() - 1000); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await clock.tick(1990); + await clock.runFor(1990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => { const callback = () => { - clock.setTime(new clock.Date().getTime() + 1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); throw new Error(); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await expect(clock.tick(1990)).rejects.toThrow(); + await expect(clock.runFor(1990)).rejects.toThrow(); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('is not influenced by forward system clock changes when an error is thrown 2', async ({ clock }) => { const callback = () => { - clock.setTime(new clock.Date().getTime() - 1000); + clock.setSystemTime(new clock.Date().getTime() - 1000); throw new Error(); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await expect(clock.tick(1990)).rejects.toThrow(); + await expect(clock.runFor(1990)).rejects.toThrow(); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('throws on negative ticks', async ({ clock }) => { - await expect(clock.tick(-500)).rejects.toThrow('Negative ticks are not supported'); + await expect(clock.runFor(-500)).rejects.toThrow('Negative ticks are not supported'); }); it('creates updated Date while ticking promises', async ({ clock }) => { @@ -571,7 +507,7 @@ it.describe('tick', () => { }); }, 10); - await clock.tick(100); + await clock.runFor(100); expect(spy.callCount).toBe(10); expect(spy.calledWith(10)).toBeTruthy(); @@ -602,7 +538,7 @@ it.describe('tick', () => { }); }, 10); - await clock.tick(500); + await clock.runFor(500); expect(spy13.callCount).toBe(38); expect(spy10.callCount).toBe(50); @@ -624,7 +560,7 @@ it.describe('tick', () => { }); id = clock.setInterval(callback, 10); - await clock.tick(100); + await clock.runFor(100); expect(callback.callCount).toBe(3); }); @@ -646,22 +582,22 @@ it.describe('tick', () => { // Clock API is async. await new Promise(setImmediate); - await clock.tick(1000); + await clock.runFor(1000); expect(i).toBe(11); }); it('is not influenced by forward system clock changes in promises', async ({ clock }) => { const callback = () => { void Promise.resolve().then(() => { - clock.setTime(new clock.Date().getTime() + 1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); }); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await clock.tick(1990); + await clock.runFor(1990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); @@ -672,7 +608,7 @@ it.describe('tick', () => { void Promise.resolve().then(spy); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spy.called).toBeTruthy(); }); @@ -687,7 +623,7 @@ it.describe('tick', () => { .then(spies[2]); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); @@ -703,7 +639,7 @@ it.describe('tick', () => { void Promise.resolve().then(spies[2]); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); @@ -721,7 +657,7 @@ it.describe('tick', () => { }); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spy.called).toBeTruthy(); }); @@ -734,7 +670,7 @@ it.describe('tick', () => { void Promise.resolve().then(spies[2]).catch(spies[3]); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].callCount).toBe(0); expect(spies[1].called).toBeTruthy(); @@ -751,7 +687,7 @@ it.describe('tick', () => { clock.setTimeout(spies[1], 200); - await clock.tick(200); + await clock.runFor(200); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); @@ -764,7 +700,7 @@ it.describe('tick', () => { // Clock API is async. await new Promise(setImmediate); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); @@ -781,516 +717,18 @@ it.describe('tick', () => { // Clock API is async. await new Promise(setImmediate); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); }); -it.describe('next', () => { - it('triggers the next timer', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub, 100); - - await clock.next(); - - expect(stub.called).toBeTruthy(); - }); - - it('does not trigger simultaneous timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 100); - clock.setTimeout(spies[1], 100); - - await clock.next(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - }); - - it('subsequent calls trigger simultaneous timers', async ({ clock }) => { - const spies = [createStub(), createStub(), createStub(), createStub()]; - clock.setTimeout(spies[0], 100); - clock.setTimeout(spies[1], 100); - clock.setTimeout(spies[2], 99); - clock.setTimeout(spies[3], 100); - - await clock.next(); - - expect(spies[2].called).toBeTruthy(); - expect(spies[0].called).toBeFalsy(); - expect(spies[1].called).toBeFalsy(); - expect(spies[3].called).toBeFalsy(); - - await clock.next(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - expect(spies[3].called).toBeFalsy(); - - await clock.next(); - - expect(spies[1].called).toBeTruthy(); - expect(spies[3].called).toBeFalsy(); - - await clock.next(); - - expect(spies[3].called).toBeTruthy(); - }); - - it('subsequent calls trigger simultaneous timers with zero callAt', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 0); - }), - createStub(), - createStub(), - ]; - - // First spy calls another setTimeout with delay=0 - clock.setTimeout(spies[0], 0); - clock.setTimeout(spies[2], 10); - - await clock.next(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - - await clock.next(); - - expect(spies[1].called).toBeTruthy(); - - await clock.next(); - - expect(spies[2].called).toBeTruthy(); - }); - - it('throws exception thrown by timer', async ({ clock }) => { - const stub = createStub().throws(); - clock.setTimeout(stub, 100); - await expect(clock.next()).rejects.toThrow(); - expect(stub.called).toBeTruthy(); - }); - - it('calls function with global object or null (strict mode) as this', async ({ clock }) => { - const stub = createStub().throws(); - clock.setTimeout(stub, 100); - await expect(clock.next()).rejects.toThrow(); - expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); - }); - - it('subsequent calls trigger in the order scheduled', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 13); - clock.setTimeout(spies[1], 11); - - await clock.next(); - await clock.next(); - - expect(spies[1].calledBefore(spies[0])).toBeTruthy(); - }); - - it('creates updated Date while ticking', async ({ clock }) => { - const spy = createStub(); - - clock.setInterval(() => { - spy(new clock.Date().getTime()); - }, 10); - - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - - expect(spy.callCount).toBe(10); - expect(spy.calledWith(10)).toBeTruthy(); - expect(spy.calledWith(20)).toBeTruthy(); - expect(spy.calledWith(30)).toBeTruthy(); - expect(spy.calledWith(40)).toBeTruthy(); - expect(spy.calledWith(50)).toBeTruthy(); - expect(spy.calledWith(60)).toBeTruthy(); - expect(spy.calledWith(70)).toBeTruthy(); - expect(spy.calledWith(80)).toBeTruthy(); - expect(spy.calledWith(90)).toBeTruthy(); - expect(spy.calledWith(100)).toBeTruthy(); - }); - - it('subsequent calls trigger timeouts and intervals in the order scheduled', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setInterval(spies[0], 10); - clock.setTimeout(spies[1], 50); - - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - expect(spies[0].callCount).toBe(5); - expect(spies[1].callCount).toBe(1); - }); - - it('subsequent calls do not fire canceled intervals', async ({ clock }) => { - // ESLint fails to detect this correctly - /* eslint-disable prefer-const */ - let id; - const callback = createStub(() => { - if (callback.callCount === 3) - clock.clearInterval(id); - }); - - id = clock.setInterval(callback, 10); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - - expect(callback.callCount).toBe(3); - }); - - it('advances the clock based on when the timer was supposed to be called', async ({ clock }) => { - clock.setTimeout(createStub(), 55); - await clock.next(); - - expect(clock.now()).toBe(55); - }); - - it('returns the current now value', async ({ clock }) => { - clock.setTimeout(createStub(), 55); - const value = await clock.next(); - - expect(clock.now()).toBe(value); - }); - - it('does not fire intervals canceled in promises', async ({ clock }) => { - // ESLint fails to detect this correctly - /* eslint-disable prefer-const */ - let id; - const callback = createStub(() => { - if (callback.callCount === 3) { - void Promise.resolve().then(() => { - clock.clearInterval(id); - }); - } - }); - - id = clock.setInterval(callback, 10); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - - expect(callback.callCount).toBe(3); - }); - - it('should settle user-created promises', async ({ clock }) => { - const spy = createStub(); - - clock.setTimeout(() => { - void Promise.resolve().then(spy); - }, 55); - - await clock.next(); - - expect(spy.called).toBeTruthy(); - }); - - it('should settle nested user-created promises', async ({ clock }) => { - const spy = createStub(); - - clock.setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(spy); - }); - }); - }, 55); - - await clock.next(); - - expect(spy.called).toBeTruthy(); - }); - - it('should settle local promises before firing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - void Promise.resolve().then(spies[0]); - clock.setTimeout(spies[1], 55); - - // Clock API is async. - await new Promise(setImmediate); - await clock.next(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); -}); - -it.describe('runAll', () => { - it('if there are no timers just return', async ({ clock }) => { - await clock.runAll(); - }); - - it('runs all timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - - await clock.runAll(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('new timers added while running are also run', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 50); - }), - createStub(), - ]; - - // Spy calls another setTimeout - clock.setTimeout(spies[0], 10); - - await clock.runAll(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('throws before allowing infinite recursion', async ({ clock }) => { - const recursiveCallback = () => { - clock.setTimeout(recursiveCallback, 10); - }; - recursiveCallback(); - await expect(clock.runAll()).rejects.toThrow(); - }); - - it('the loop limit can be set when creating a clock', async ({}) => { - const clock = createClock(0, 1); - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - await expect(clock.runAll()).rejects.toThrow(); - }); - - it('the loop limit can be set when installing a clock', async ({ install }) => { - const clock = install({ loopLimit: 1 }); - const spies = [createStub(), createStub()]; - setTimeout(spies[0], 10); - setTimeout(spies[1], 50); - - await expect(clock.runAll()).rejects.toThrow(); - }); - - it('throws before allowing infinite recursion from promises', async ({ clock }) => { - const recursiveCallback = () => { - void Promise.resolve().then(() => { - clock.setTimeout(recursiveCallback, 10); - }); - }; - recursiveCallback(); - - // Clock API is async. - await new Promise(setImmediate); - await expect(clock.runAll()).rejects.toThrow(); - }); - - it('should settle user-created promises', async ({ clock }) => { - const spy = createStub(); - clock.setTimeout(() => { - void Promise.resolve().then(spy); - }, 55); - await clock.runAll(); - expect(spy.called).toBeTruthy(); - }); - - it('should settle nested user-created promises', async ({ clock }) => { - const spy = createStub(); - - clock.setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(spy); - }); - }); - }, 55); - - await clock.runAll(); - - expect(spy.called).toBeTruthy(); - }); - - it('should settle local promises before firing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - void Promise.resolve().then(spies[0]); - clock.setTimeout(spies[1], 55); - - // Clock API is async. - await new Promise(setImmediate); - await clock.runAll(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); - - it('should settle user-created promises before firing more timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(() => { - void Promise.resolve().then(spies[0]); - }, 55); - clock.setTimeout(spies[1], 75); - await clock.runAll(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); -}); - -it.describe('runToLast', () => { - it('returns current time when there are no timers', async ({ clock }) => { - const time = await clock.runToLast(); - expect(time).toBe(0); - }); - - it('runs all existing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - await clock.runToLast(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('returns time of the last timer', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - const time = await clock.runToLast(); - expect(time).toBe(50); - }); - - it('runs all existing timers when two timers are matched for being last', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 10); - await clock.runToLast(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('new timers added with a call time later than the last existing timer are NOT run', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 50); - }), - createStub(), - ]; - - // Spy calls another setTimeout - clock.setTimeout(spies[0], 10); - await clock.runToLast(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - }); - - it('new timers added with a call time earlier than the last existing timer are run', async ({ clock }) => { - const spies = [ - createStub(), - createStub(() => { - clock.setTimeout(spies[2], 50); - }), - createStub(), - ]; - - clock.setTimeout(spies[0], 100); - // Spy calls another setTimeout - clock.setTimeout(spies[1], 10); - await clock.runToLast(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - expect(spies[2].called).toBeTruthy(); - }); - - it('new timers cannot cause an infinite loop', async ({ clock }) => { - const spy = createStub(); - const recursiveCallback = () => { - clock.setTimeout(recursiveCallback, 0); - }; - - clock.setTimeout(recursiveCallback, 0); - clock.setTimeout(spy, 100); - await clock.runToLast(); - expect(spy.called).toBeTruthy(); - }); - - it('should support clocks with start time', async ({ clock }) => { - let invocations = 0; - - clock.setTimeout(function cb() { - invocations++; - clock.setTimeout(cb, 50); - }, 50); - - await clock.runToLast(); - - expect(invocations).toBe(1); - }); - - it('should settle user-created promises', async ({ clock }) => { - const spy = createStub(); - clock.setTimeout(() => { - void Promise.resolve().then(spy); - }, 55); - await clock.runToLast(); - expect(spy.called).toBeTruthy(); - }); - - it('should settle nested user-created promises', async ({ clock }) => { - const spy = createStub(); - - clock.setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(spy); - }); - }); - }, 55); - - await clock.runToLast(); - expect(spy.called).toBeTruthy(); - }); - - it('should settle local promises before firing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - void Promise.resolve().then(spies[0]); - clock.setTimeout(spies[1], 55); - - // Clock API is async. - await new Promise(setImmediate); - await clock.runToLast(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); - - it('should settle user-created promises before firing more timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(() => { - void Promise.resolve().then(spies[0]); - }, 55); - clock.setTimeout(spies[1], 75); - await clock.runToLast(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); -}); - it.describe('clearTimeout', () => { it('removes timeout', async ({ clock }) => { const stub = createStub(); const id = clock.setTimeout(stub, 50); clock.clearTimeout(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1298,7 +736,7 @@ it.describe('clearTimeout', () => { const stub = createStub(); const id = clock.setInterval(stub, 50); clock.clearTimeout(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1306,7 +744,7 @@ it.describe('clearTimeout', () => { const stub = createStub(); const id = clock.setInterval(stub); clock.clearTimeout(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1315,21 +753,6 @@ it.describe('clearTimeout', () => { }); }); -it.describe('reset', () => { - it('resets to the time install with - issue #183', async ({ clock }) => { - await clock.tick(100); - clock.reset(); - expect(clock.now()).toBe(0); - }); - - it('resets hrTime - issue #206', async ({ clock }) => { - await clock.tick(100); - expect(clock.performance.now()).toEqual(100); - clock.reset(); - expect(clock.performance.now()).toEqual(0); - }); -}); - it.describe('setInterval', () => { it('throws if no arguments', async ({ clock }) => { expect(() => { @@ -1353,7 +776,7 @@ it.describe('setInterval', () => { it('schedules recurring timeout', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - await clock.tick(99); + await clock.runFor(99); expect(stub.callCount).toBe(9); }); @@ -1361,23 +784,23 @@ it.describe('setInterval', () => { it('is not influenced by forward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - await clock.tick(11); + await clock.runFor(11); expect(stub.callCount).toBe(1); - clock.setTime(new clock.Date().getTime() + 1000); - await clock.tick(8); + clock.setSystemTime(new clock.Date().getTime() + 1000); + await clock.runFor(8); expect(stub.callCount).toBe(1); - await clock.tick(3); + await clock.runFor(3); expect(stub.callCount).toBe(2); }); it('is not influenced by backward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - await clock.tick(5); - clock.setTime(new clock.Date().getTime() - 1000); - await clock.tick(6); + await clock.runFor(5); + clock.setSystemTime(new clock.Date().getTime() - 1000); + await clock.runFor(6); expect(stub.callCount).toBe(1); - await clock.tick(10); + await clock.runFor(10); expect(stub.callCount).toBe(2); }); @@ -1388,7 +811,7 @@ it.describe('setInterval', () => { }); const id = clock.setInterval(stub, 10); - await clock.tick(100); + await clock.runFor(100); expect(stub.callCount).toBe(3); }); @@ -1396,7 +819,7 @@ it.describe('setInterval', () => { it('passes setTimeout parameters', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 2, 'the first', 'the second'); - await clock.tick(3); + await clock.runFor(3); expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); }); }); @@ -1406,7 +829,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setInterval(stub, 50); clock.clearInterval(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1414,7 +837,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setInterval(stub); clock.clearInterval(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1422,7 +845,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setTimeout(stub, 50); clock.clearInterval(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1458,14 +881,14 @@ it.describe('date', () => { it('listens to ticking clock', async ({ clock }) => { const date1 = new clock.Date(); - await clock.tick(3); + await clock.runFor(3); const date2 = new clock.Date(); expect(date2.getTime() - date1.getTime()).toBe(3); }); it('listens to system clock changes', async ({ clock }) => { const date1 = new clock.Date(); - clock.setTime(date1.getTime() + 1000); + clock.setSystemTime(date1.getTime() + 1000); const date2 = new clock.Date(); expect(date2.getTime() - date1.getTime()).toBe(1000); }); @@ -1568,16 +991,16 @@ it.describe('stubTimers', () => { it('returns clock object', async ({ install }) => { const clock = install(); expect(clock).toEqual(expect.any(Object)); - expect(clock.tick).toEqual(expect.any(Function)); + expect(clock.runFor).toEqual(expect.any(Function)); }); it('takes an object parameter', async ({ install }) => { - const clock = install({}); + const clock = install(); expect(clock).toEqual(expect.any(Object)); }); it('sets initial timestamp', async ({ install }) => { - const clock = install({ now: 1400 }); + const clock = install(1400); expect(clock.now()).toBe(1400); }); @@ -1586,7 +1009,7 @@ it.describe('stubTimers', () => { const stub = createStub(); setTimeout(stub, 1000); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.called).toBeTruthy(); }); @@ -1603,7 +1026,7 @@ it.describe('stubTimers', () => { const stub = createStub(); clearTimeout(setTimeout(stub, 1000)); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.called).toBeFalsy(); }); @@ -1613,7 +1036,7 @@ it.describe('stubTimers', () => { const stub = createStub(); setInterval(stub, 500); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.callCount).toBe(2); }); @@ -1623,7 +1046,7 @@ it.describe('stubTimers', () => { const stub = createStub(); clearInterval(setInterval(stub, 500)); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.called).toBeFalsy(); }); @@ -1631,7 +1054,7 @@ it.describe('stubTimers', () => { it('replaces global performance.now', async ({ install }) => { const clock = install(); const prev = performance.now(); - await clock.tick(1000); + await clock.runFor(1000); const next = performance.now(); expect(next).toBe(1000); expect(prev).toBe(0); @@ -1733,12 +1156,6 @@ it.describe('stubTimers', () => { expect(Date.prototype).toEqual(clock.Date.prototype); }); - it('decide on Date.now support at call-time when supported', async ({ install }) => { - (Date.now as any) = () => {}; - install({ now: 0 }); - expect(Date.now).toEqual(expect.any(Function)); - }); - it('mirrors custom Date properties', async ({ install }) => { const f = () => { return ''; @@ -1761,7 +1178,7 @@ it.describe('stubTimers', () => { expect(Date).not.toBe(originals.Date); }); - it('resets faked methods', async ({ install }) => { + it('resets faked methods', async ({ }) => { const { clock, originals } = rawInstall(globalThis, { now: 0, toFake: ['setTimeout', 'Date'], @@ -1784,60 +1201,6 @@ it.describe('stubTimers', () => { }); }); -it.describe('shouldAdvanceTime', () => { - it('should create an auto advancing timer', async () => { - const testDelay = 29; - const date = new Date('2015-09-25'); - const clock = createClock(date); - clock.advanceAutomatically(); - expect(clock.Date.now()).toBe(1443139200000); - const timeoutStarted = clock.Date.now(); - - let callback: (r: number) => void; - const promise = new Promise(r => callback = r); - - clock.setTimeout(() => { - const timeDifference = clock.Date.now() - timeoutStarted; - callback(timeDifference); - }, testDelay); - expect(await promise).toBe(testDelay); - - }); - - it('should test setInterval', async () => { - const interval = 20; - let intervalsTriggered = 0; - const cyclesToTrigger = 3; - const date = new Date('2015-09-25'); - const clock = createClock(date); - clock.advanceAutomatically(); - expect(clock.Date.now()).toBe(1443139200000); - const timeoutStarted = clock.Date.now(); - - let callback: (r: number) => void; - const promise = new Promise(r => callback = r); - - const intervalId = clock.setInterval(() => { - if (++intervalsTriggered === cyclesToTrigger) { - clock.clearInterval(intervalId); - const timeDifference = clock.Date.now() - timeoutStarted; - callback(timeDifference); - } - }, interval); - - expect(await promise).toBe(interval * cyclesToTrigger); - }); - - it('should not depend on having to stub setInterval or clearInterval to work', async ({ install }) => { - const origSetInterval = globalThis.setInterval; - const origClearInterval = globalThis.clearInterval; - - install({ toFake: ['setTimeout'] }); - expect(globalThis.setInterval).toBe(origSetInterval); - expect(globalThis.clearInterval).toBe(origClearInterval); - }); -}); - it.describe('requestAnimationFrame', () => { it('throws if no arguments', async ({ clock }) => { expect(() => { @@ -1860,30 +1223,30 @@ it.describe('requestAnimationFrame', () => { it('should run every 16ms', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - await clock.tick(15); + await clock.runFor(15); expect(stub.callCount).toBe(0); - await clock.tick(1); + await clock.runFor(1); expect(stub.callCount).toBe(1); }); it('should be called with performance.now() when available', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - await clock.tick(20); + await clock.runFor(20); expect(stub.calledWith(16)).toBeTruthy(); }); it('should be called with performance.now() even when performance unavailable', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - await clock.tick(20); + await clock.runFor(20); expect(stub.calledWith(16)).toBeTruthy(); }); it('should call callback once', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - await clock.tick(32); + await clock.runFor(32); expect(stub.callCount).toBe(1); }); @@ -1891,9 +1254,9 @@ it.describe('requestAnimationFrame', () => { const stub1 = createStub(); const stub2 = createStub(); clock.requestAnimationFrame(stub1); - await clock.tick(5); + await clock.runFor(5); clock.requestAnimationFrame(stub2); - await clock.tick(11); + await clock.runFor(11); expect(stub1.calledWith(16)).toBeTruthy(); expect(stub2.calledWith(16)).toBeTruthy(); }); @@ -1902,18 +1265,18 @@ it.describe('requestAnimationFrame', () => { const stub1 = createStub(); const stub2 = createStub(); clock.requestAnimationFrame(stub1); - await clock.tick(57); + await clock.runFor(57); clock.requestAnimationFrame(stub2); - await clock.tick(10); + await clock.runFor(10); expect(stub1.calledWith(16)).toBeTruthy(); expect(stub2.calledWith(64)).toBeTruthy(); }); it('should schedule for next frame if on current frame', async ({ clock }) => { const stub = createStub(); - await clock.tick(16); + await clock.runFor(16); clock.requestAnimationFrame(stub); - await clock.tick(16); + await clock.runFor(16); expect(stub.calledWith(32)).toBeTruthy(); }); }); @@ -1923,7 +1286,7 @@ it.describe('cancelAnimationFrame', () => { const stub = createStub(); const id = clock.requestAnimationFrame(stub); clock.cancelAnimationFrame(id); - await clock.tick(16); + await clock.runFor(16); expect(stub.called).toBeFalsy(); }); @@ -1933,7 +1296,7 @@ it.describe('cancelAnimationFrame', () => { expect(() => { clock.cancelAnimationFrame(id); }).toThrow(); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeTruthy(); }); @@ -1943,7 +1306,7 @@ it.describe('cancelAnimationFrame', () => { expect(() => { clock.cancelAnimationFrame(id); }).toThrow(); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeTruthy(); }); @@ -1952,21 +1315,11 @@ it.describe('cancelAnimationFrame', () => { }); }); -it.describe('runToFrame', () => { - it('should tick next frame', async ({ clock }) => { - await clock.runToFrame(); - expect(clock.now()).toBe(16); - await clock.tick(3); - await clock.runToFrame(); - expect(clock.now()).toBe(32); - }); -}); - -it.describe('jump', () => { +it.describe('fastForward', () => { it('ignores timers which wouldn\'t be run', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 1000); - await clock.jump(500); + await clock.fastForward(500); expect(stub.called).toBeFalsy(); }); @@ -1975,7 +1328,7 @@ it.describe('jump', () => { clock.setTimeout(() => { stub(clock.Date.now()); }, 1000); - await clock.jump(2000); + await clock.fastForward(2000); expect(stub.callCount).toBe(1); expect(stub.calledWith(2000)).toBeTruthy(); }); @@ -1988,19 +1341,12 @@ it.describe('jump', () => { clock.setTimeout(shortTimers[0], 250); clock.setInterval(shortTimers[1], 100); clock.requestAnimationFrame(shortTimers[2]); - await clock.jump(1500); + await clock.fastForward(1500); for (const stub of longTimers) expect(stub.called).toBeFalsy(); for (const stub of shortTimers) expect(stub.callCount).toBe(1); }); - - it('supports string time arguments', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub, 100000); // 100000 = 1:40 - await clock.jump('01:50'); - expect(stub.callCount).toBe(1); - }); }); it.describe('performance.now()', () => { @@ -2010,7 +1356,7 @@ it.describe('performance.now()', () => { }); it('should run along with clock.tick', async ({ clock }) => { - await clock.tick(5000); + await clock.runFor(5000); const result = clock.performance.now(); expect(result).toBe(5000); }); @@ -2019,7 +1365,7 @@ it.describe('performance.now()', () => { for (let i = 0; i < 10; i++) { const next = clock.performance.now(); expect(next).toBe(1000 * i); - await clock.tick(1000); + await clock.runFor(1000); } }); @@ -2028,7 +1374,7 @@ it.describe('performance.now()', () => { const result = clock.performance.now(); expect(result).toBe(2500); }, 2500); - await clock.tick(5000); + await clock.runFor(5000); }); }); @@ -2054,7 +1400,7 @@ it.describe('requestIdleCallback', () => { it('runs after all timers', async ({ clock }) => { const stub = createStub(); clock.requestIdleCallback(stub); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.called).toBeTruthy(); const idleCallbackArg = stub.firstCall.args[0]; expect(idleCallbackArg.didTimeout).toBeFalsy(); @@ -2066,7 +1412,7 @@ it.describe('requestIdleCallback', () => { clock.setTimeout(() => {}, 10); clock.setTimeout(() => {}, 30); clock.requestIdleCallback(stub, { timeout: 20 }); - await clock.tick(20); + await clock.runFor(20); expect(stub.called).toBeTruthy(); }); @@ -2074,7 +1420,7 @@ it.describe('requestIdleCallback', () => { const stub = createStub(); clock.setTimeout(() => {}, 30); clock.requestIdleCallback(stub); - await clock.tick(35); + await clock.runFor(35); expect(stub.called).toBeFalsy(); }); }); @@ -2084,149 +1430,11 @@ it.describe('cancelIdleCallback', () => { const stub = createStub(); const callbackId = clock.requestIdleCallback(stub, { timeout: 0 }); clock.cancelIdleCallback(callbackId); - await clock.tick(0); + await clock.runFor(0); expect(stub.called).toBeFalsy(); }); }); -it.describe('loop limit stack trace', () => { - const expectedMessage = - 'Aborting after running 5 timers, assuming an infinite loop!'; - it.use({ loopLimit: 5 }); - - it.describe('setTimeout', () => { - it('provides a stack trace for running all async', async ({ clock }) => { - const catchSpy = createStub(); - const recursiveCreateTimer = () => { - clock.setTimeout(recursiveCreateTimer, 10); - }; - - recursiveCreateTimer(); - await clock.runAll().catch(catchSpy); - expect(catchSpy.callCount).toBe(1); - const err = catchSpy.firstCall.args[0]; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); - }); - - it('provides a stack trace for running all sync', async ({ clock }) => { - let caughtError = false; - const recursiveCreateTimer = () => { - clock.setTimeout(recursiveCreateTimer, 10); - }; - - recursiveCreateTimer(); - try { - await clock.runAll(); - } catch (err) { - caughtError = true; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); - } - expect(caughtError).toBeTruthy(); - }); - }); - - it.describe('requestIdleCallback', () => { - it('provides a stack trace for running all async', async ({ clock }) => { - const catchSpy = createStub(); - const recursiveCreateTimer = () => { - clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); - }; - - recursiveCreateTimer(); - await clock.runAll().catch(catchSpy); - expect(catchSpy.callCount).toBe(1); - const err = catchSpy.firstCall.args[0]; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); - }); - - it('provides a stack trace for running all sync', async ({ clock }) => { - let caughtError = false; - const recursiveCreateTimer = () => { - clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); - }; - - recursiveCreateTimer(); - try { - await clock.runAll(); - } catch (err) { - caughtError = true; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); - } - expect(caughtError).toBeTruthy(); - }); - }); - - it.describe('setInterval', () => { - it('provides a stack trace for running all async', async ({ clock }) => { - const catchSpy = createStub(); - const recursiveCreateTimer = () => { - clock.setInterval(recursiveCreateTimer, 10); - }; - - recursiveCreateTimer(); - await clock.runAll().catch(catchSpy); - expect(catchSpy.callCount).toBe(1); - const err = catchSpy.firstCall.args[0]; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); - }); - - it('provides a stack trace for running all sync', async ({ clock }) => { - let caughtError = false; - const recursiveCreateTimer = () => { - clock.setInterval(recursiveCreateTimer, 10); - }; - - recursiveCreateTimer(); - try { - await clock.runAll(); - } catch (err) { - caughtError = true; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); - } - expect(caughtError).toBeTruthy(); - }); - }); - - it.describe('requestAnimationFrame', () => { - it('provides a stack trace for running all async', async ({ clock }) => { - const catchSpy = createStub(); - const recursiveCreateTimer = () => { - clock.requestAnimationFrame(recursiveCreateTimer); - }; - - recursiveCreateTimer(); - await clock.runAll().catch(catchSpy); - expect(catchSpy.callCount).toBe(1); - const err = catchSpy.firstCall.args[0]; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); - }); - - it('provides a stack trace for running all sync', async ({ clock }) => { - let caughtError = false; - const recursiveCreateTimer = () => { - clock.requestAnimationFrame(recursiveCreateTimer); - }; - - recursiveCreateTimer(); - try { - await clock.runAll(); - } catch (err) { - caughtError = true; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); - } - expect(caughtError).toBeTruthy(); - }); - }); -}); - it.describe('Intl API', () => { function isFirstOfMonth(ianaTimeZone, timestamp?: number) { return ( @@ -2301,13 +1509,13 @@ it.describe('Intl API', () => { it('formatToParts via isFirstOfMonth -> Returns true when passed no timestamp and system time is first of the month', async ({ install }) => { // June 1 04:00 UTC - Toronto is June 1 00:00 - install({ now: Date.UTC(2022, 5, 1, 4) }); + install(Date.UTC(2022, 5, 1, 4)); expect(isFirstOfMonth('America/Toronto')).toBeTruthy(); }); it('formatToParts via isFirstOfMonth -> Returns false when passed no timestamp and system time is not first of the month', async ({ install }) => { // June 1 00:00 UTC - Toronto is May 31 20:00 - install({ now: Date.UTC(2022, 5, 1) }); + install(Date.UTC(2022, 5, 1)); expect(isFirstOfMonth('America/Toronto')).toBeFalsy(); }); diff --git a/tests/page/page-clock.frozen.spec.ts b/tests/page/page-clock.frozen.spec.ts index 3d763a2cc3..bddb794da1 100644 --- a/tests/page/page-clock.frozen.spec.ts +++ b/tests/page/page-clock.frozen.spec.ts @@ -18,6 +18,7 @@ import { test as it, expect } from './pageTest'; it.skip(!process.env.PW_FREEZE_TIME); -it('cock should be frozen', async ({ page }) => { +it('clock should be frozen', async ({ page }) => { + await page.clock.setSystemTime(0); expect(await page.evaluate('Date.now()')).toBe(0); }); diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index 4a6ca00896..fdc01c5570 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -35,8 +35,12 @@ const it = test.extend<{ calls: { params: any[] }[] }>({ }); it.describe('runFor', () => { + it.beforeEach(async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); + }); + it('triggers immediately without specified delay', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub); }); @@ -46,7 +50,6 @@ it.describe('runFor', () => { }); it('does not trigger without sufficient delay', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); @@ -55,7 +58,6 @@ it.describe('runFor', () => { }); it('triggers after sufficient delay', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); @@ -64,7 +66,6 @@ it.describe('runFor', () => { }); it('triggers simultaneous timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); @@ -74,7 +75,6 @@ it.describe('runFor', () => { }); it('triggers multiple simultaneous timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); @@ -86,7 +86,6 @@ it.describe('runFor', () => { }); it('waits after setTimeout was called', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 150); }); @@ -97,18 +96,16 @@ it.describe('runFor', () => { }); it('triggers event when some throw', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { throw new Error(); }, 100); setTimeout(window.stub, 120); }); - await expect(page.clock.runFor(120)).rejects.toThrow(); expect(calls).toHaveLength(1); }); it('creates updated Date while ticking', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); + await page.clock.setSystemTime(0); await page.evaluate(async () => { setInterval(() => { window.stub(new Date().getTime()); @@ -130,7 +127,6 @@ it.describe('runFor', () => { }); it('passes 8 seconds', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 4000); }); @@ -140,7 +136,6 @@ it.describe('runFor', () => { }); it('passes 1 minute', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 6000); }); @@ -150,7 +145,6 @@ it.describe('runFor', () => { }); it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 10000); }); @@ -160,7 +154,6 @@ it.describe('runFor', () => { }); it('throws for invalid format', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 10000); }); @@ -169,332 +162,93 @@ it.describe('runFor', () => { }); it('returns the current now value', async ({ page }) => { - await page.clock.installFakeTimers(0); + await page.clock.setSystemTime(0); const value = 200; await page.clock.runFor(value); expect(await page.evaluate(() => Date.now())).toBe(value); }); }); -it.describe('skipTime', () => { +it.describe('fastForward', () => { + it.beforeEach(async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); + await page.clock.setSystemTime(0); + }); + it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub('should not be logged'); }, 1000); }); - await page.clock.skipTime(500); + await page.clock.fastForward(500); expect(calls).toEqual([]); }); it('pushes back execution time for skipped timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(Date.now()); }, 1000); }); - await page.clock.skipTime(2000); + await page.clock.fastForward(2000); expect(calls).toEqual([{ params: [2000] }]); }); it('supports string time arguments', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(Date.now()); }, 100000); // 100000 = 1:40 }); - await page.clock.skipTime('01:50'); + await page.clock.fastForward('01:50'); expect(calls).toEqual([{ params: [110000] }]); }); }); -it.describe('runAllTimers', () => { - it('if there are no timers just return', async ({ page }) => { - await page.clock.installFakeTimers(0); - await page.clock.runAllTimers(); +it.describe('fastForwardTo', () => { + it.beforeEach(async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); + await page.clock.setSystemTime(0); }); - it('runs all timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 50); - }); - await page.clock.runAllTimers(); - expect(calls.length).toBe(2); - }); - - it('new timers added while running are also run', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); + it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { await page.evaluate(async () => { setTimeout(() => { - setTimeout(window.stub, 50); - }, 10); + window.stub('should not be logged'); + }, 1000); }); - await page.clock.runAllTimers(); - expect(calls.length).toBe(1); + await page.clock.fastForwardTo(500); + expect(calls).toEqual([]); }); - it('new timers added in promises while running are also run', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); + it('pushes back execution time for skipped timers', async ({ page, calls }) => { await page.evaluate(async () => { setTimeout(() => { - void Promise.resolve().then(() => { - setTimeout(window.stub, 50); - }); - }, 10); + window.stub(Date.now()); + }, 1000); }); - await page.clock.runAllTimers(); - expect(calls.length).toBe(1); - }); - it('throws before allowing infinite recursion', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - const recursiveCallback = () => { - window.stub(); - setTimeout(recursiveCallback, 10); - }; - setTimeout(recursiveCallback, 10); - }); - await expect(page.clock.runAllTimers()).rejects.toThrow(); - expect(calls).toHaveLength(1000); - }); - - it('throws before allowing infinite recursion from promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - const recursiveCallback = () => { - window.stub(); - void Promise.resolve().then(() => { - setTimeout(recursiveCallback, 10); - }); - }; - setTimeout(recursiveCallback, 10); - }); - await expect(page.clock.runAllTimers()).rejects.toThrow(); - expect(calls).toHaveLength(1000); - }); - - it('the loop limit can be set when creating a clock', async ({ page, calls }) => { - await page.clock.installFakeTimers(0, { loopLimit: 1 }); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 50); - }); - await expect(page.clock.runAllTimers()).rejects.toThrow(); - expect(calls).toHaveLength(1); - }); - - it('should settle user-created promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => window.stub()); - }, 55); - }); - await page.clock.runAllTimers(); - expect(calls).toHaveLength(1); - }); - - it('should settle nested user-created promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => window.stub()); - }); - }); - }, 55); - }); - await page.clock.runAllTimers(); - expect(calls).toHaveLength(1); - }); - - it('should settle local promises before firing timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - void Promise.resolve().then(() => window.stub(1)); - setTimeout(() => window.stub(2), 55); - }); - await page.clock.runAllTimers(); - expect(calls).toEqual([ - { params: [1] }, - { params: [2] }, - ]); - }); -}); - -it.describe('runToLastTimer', () => { - it('returns current time when there are no timers', async ({ page }) => { - await page.clock.installFakeTimers(0); - const time = await page.clock.runToLastTimer(); - expect(time).toBe(0); - }); - - it('runs all existing timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 50); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(2); - }); - - it('returns time of the last timer', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 50); - }); - const time = await page.clock.runToLastTimer(); - expect(time).toBe(50); - }); - - it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 10); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(2); - }); - - it('new timers added with a call time later than the last existing timer are NOT run', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - window.stub(); - setTimeout(window.stub, 50); - }, 10); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 100); - setTimeout(() => { - setTimeout(window.stub, 50); - }, 10); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(2); - }); - - it('new timers cannot cause an infinite loop', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - const recursiveCallback = () => { - window.stub(); - setTimeout(recursiveCallback, 0); - }; - setTimeout(recursiveCallback, 0); - setTimeout(window.stub, 100); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(102); - }); - - it('should support clocks with start time', async ({ page, calls }) => { - await page.clock.installFakeTimers(200); - await page.evaluate(async () => { - setTimeout(function cb() { - window.stub(); - setTimeout(cb, 50); - }, 50); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - const recursiveCallback = () => { - void Promise.resolve().then(() => { - setTimeout(recursiveCallback, 0); - }); - }; - setTimeout(recursiveCallback, 0); - setTimeout(window.stub, 100); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('should settle user-created promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => window.stub()); - }, 55); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('should settle nested user-created promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => window.stub()); - }); - }); - }, 55); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('should settle local promises before firing timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - void Promise.resolve().then(() => window.stub(1)); - setTimeout(() => window.stub(2), 55); - }); - await page.clock.runToLastTimer(); - expect(calls).toEqual([ - { params: [1] }, - { params: [2] }, - ]); - }); - - it('should settle user-created promises before firing more timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => window.stub(1)); - }, 55); - setTimeout(() => window.stub(2), 75); - }); - await page.clock.runToLastTimer(); - expect(calls).toEqual([ - { params: [1] }, - { params: [2] }, - ]); + await page.clock.fastForwardTo(2000); + expect(calls).toEqual([{ params: [2000] }]); }); }); it.describe('stubTimers', () => { + it.beforeEach(async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); + await page.clock.setSystemTime(0); + }); it('sets initial timestamp', async ({ page, calls }) => { - await page.clock.installFakeTimers(1400); + await page.clock.setSystemTime(1400); expect(await page.evaluate(() => Date.now())).toBe(1400); }); it('replaces global setTimeout', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 1000); }); @@ -503,13 +257,11 @@ it.describe('stubTimers', () => { }); it('global fake setTimeout should return id', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); const to = await page.evaluate(() => setTimeout(window.stub, 1000)); expect(typeof to).toBe('number'); }); it('replaces global clearTimeout', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { const to = setTimeout(window.stub, 1000); clearTimeout(to); @@ -519,7 +271,6 @@ it.describe('stubTimers', () => { }); it('replaces global setInterval', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 500); }); @@ -528,7 +279,6 @@ it.describe('stubTimers', () => { }); it('replaces global clearInterval', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { const to = setInterval(window.stub, 500); clearInterval(to); @@ -538,7 +288,6 @@ it.describe('stubTimers', () => { }); it('replaces global performance.now', async ({ page }) => { - await page.clock.installFakeTimers(0); const promise = page.evaluate(async () => { const prev = performance.now(); await new Promise(f => setTimeout(f, 1000)); @@ -549,30 +298,35 @@ it.describe('stubTimers', () => { expect(await promise).toEqual({ prev: 0, next: 1000 }); }); - it('replaces global performance.timeOrigin', async ({ page }) => { - await page.clock.installFakeTimers(1000); - const promise = page.evaluate(async () => { - const prev = performance.now(); - await new Promise(f => setTimeout(f, 1000)); - const next = performance.now(); - return { prev, next }; - }); - expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000); - await page.clock.runFor(1000); - expect(await promise).toEqual({ prev: 0, next: 1000 }); - }); - it('fakes Date constructor', async ({ page }) => { - await page.clock.installFakeTimers(0); const now = await page.evaluate(() => new Date().getTime()); expect(now).toBe(0); }); }); +it.describe('stubTimers', () => { + it('replaces global performance.timeOrigin', async ({ page }) => { + await page.clock.install({ time: 1000 }); + await page.clock.pause(); + await page.clock.setSystemTime(1000); + const promise = page.evaluate(async () => { + const prev = performance.now(); + await new Promise(f => setTimeout(f, 1000)); + const next = performance.now(); + return { prev, next }; + }); + await page.clock.runFor(1000); + expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000); + expect(await promise).toEqual({ prev: 0, next: 1000 }); + }); +}); + it.describe('popup', () => { it('should tick after popup', async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); const now = new Date('2015-09-25'); - await page.clock.installFakeTimers(now); + await page.clock.setSystemTime(now); const [popup] = await Promise.all([ page.waitForEvent('popup'), page.evaluate(() => window.open('about:blank')), @@ -584,11 +338,12 @@ it.describe('popup', () => { expect(popupTimeAfter).toBe(now.getTime() + 1000); }); - it('should tick before popup', async ({ page, browserName }) => { + it('should tick before popup', async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); const now = new Date('2015-09-25'); - await page.clock.installFakeTimers(now); - const ticks = await page.clock.runFor(1000); - expect(ticks).toBe(1000); + await page.clock.setSystemTime(now); + await page.clock.runFor(1000); const [popup] = await Promise.all([ page.waitForEvent('popup'), @@ -597,90 +352,47 @@ it.describe('popup', () => { const popupTime = await popup.evaluate(() => Date.now()); expect(popupTime).toBe(now.getTime() + 1000); }); -}); -it.describe('runToNextTimer', () => { - it('triggers the next timer', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 100); + it('should run time before popup', async ({ page, server }) => { + server.setRoute('/popup.html', async (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); }); - expect(await page.clock.runToNextTimer()).toBe(100); - expect(calls).toHaveLength(1); + await page.clock.setSystemTime(0); + await page.goto(server.EMPTY_PAGE); + // Wait for 2 second in real life to check that it is past in popup. + await page.waitForTimeout(2000); + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'), + ]); + const popupTime = await popup.evaluate('time'); + expect(popupTime).toBeGreaterThanOrEqual(2000); }); - it('does not trigger simultaneous timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(() => { - setTimeout(() => { - window.stub(); - }, 100); - setTimeout(() => { - window.stub(); - }, 100); + it('should not run time before popup on pause', async ({ page, server }) => { + server.setRoute('/popup.html', async (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); }); - - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(1); - }); - - it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - window.stub(); - }, 100); - setTimeout(() => { - window.stub(); - }, 100); - setTimeout(() => { - window.stub(); - }, 99); - setTimeout(() => { - window.stub(); - }, 100); - }); - - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(1); - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(2); - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(3); - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(4); - }); - - it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - window.stub(1); - setTimeout(() => { - setTimeout(() => window.stub(2), 0); - }, 0); - }); - - await page.clock.runToNextTimer(); - expect(calls).toEqual([{ params: [1] }]); - await page.clock.runToNextTimer(); - expect(calls).toEqual([{ params: [1] }, { params: [2] }]); - }); - - it('throws exception thrown by timer', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - throw new Error(); - }, 100); - }); - - await expect(page.clock.runToNextTimer()).rejects.toThrow(); + await page.clock.install(); + await page.clock.pause(); + await page.clock.setSystemTime(0); + await page.goto(server.EMPTY_PAGE); + // Wait for 2 second in real life to check that it is past in popup. + await page.waitForTimeout(2000); + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'), + ]); + const popupTime = await popup.evaluate('time'); + expect(popupTime).toBe(0); }); }); -it.describe('setTime', () => { +it.describe('setFixedTime', () => { it('does not fake methods', async ({ page }) => { - await page.clock.setTime(0); + await page.clock.setFixedTime(0); // Should not stall. await page.evaluate(() => { @@ -688,54 +400,145 @@ it.describe('setTime', () => { }); }); - it('allows setting time multiple times', async ({ page, calls }) => { - await page.clock.setTime(100); + it('allows setting time multiple times', async ({ page }) => { + await page.clock.setFixedTime(100); expect(await page.evaluate(() => Date.now())).toBe(100); - await page.clock.setTime(200); + await page.clock.setFixedTime(200); expect(await page.evaluate(() => Date.now())).toBe(200); }); - it('supports skipTime w/o fake timers', async ({ page }) => { - await page.clock.setTime(100); + it('fixed time is not affected by clock manipulation', async ({ page }) => { + await page.clock.setFixedTime(100); + expect(await page.evaluate(() => Date.now())).toBe(100); + await page.clock.fastForward(20); expect(await page.evaluate(() => Date.now())).toBe(100); - await page.clock.skipTime(20); - expect(await page.evaluate(() => Date.now())).toBe(120); }); it('allows installing fake timers after settings time', async ({ page, calls }) => { - await page.clock.setTime(100); + await page.clock.setFixedTime(100); expect(await page.evaluate(() => Date.now())).toBe(100); - await page.clock.installFakeTimers(200); + await page.clock.setFixedTime(200); await page.evaluate(async () => { setTimeout(() => window.stub(Date.now())); }); await page.clock.runFor(0); expect(calls).toEqual([{ params: [200] }]); }); +}); - it('allows setting time after installing fake timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(200); - await page.evaluate(async () => { - setTimeout(() => window.stub(Date.now())); - }); - await page.clock.setTime(220); - expect(calls).toEqual([{ params: [220] }]); +it.describe('while running', () => { + it('should progress time', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.waitForTimeout(1000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(1000); + expect(now).toBeLessThanOrEqual(2000); }); - it('does not allow flowing time backwards', async ({ page, calls }) => { - await page.clock.installFakeTimers(200); - await expect(page.clock.setTime(180)).rejects.toThrow(); + it('should runFor', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.runFor(10000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(10000); + expect(now).toBeLessThanOrEqual(11000); }); - it('should turn setTime into jump', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 100); - setTimeout(window.stub, 200); - }); - await page.clock.setTime(100); - expect(calls).toHaveLength(1); - await page.clock.setTime(200); - expect(calls).toHaveLength(2); + it('should fastForward', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.fastForward(10000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(10000); + expect(now).toBeLessThanOrEqual(11000); + }); + + it('should fastForwardTo', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.fastForwardTo(10000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(10000); + expect(now).toBeLessThanOrEqual(11000); + }); + + it('should pause', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.waitForTimeout(1000); + await page.clock.resume(); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(0); + expect(now).toBeLessThanOrEqual(1000); + }); + + it('should pause and fastForwardTo', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.clock.fastForwardTo(1000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBe(1000); + }); + + it('should set system time on pause', async ({ page }) => { + await page.clock.install(); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.clock.setSystemTime(1000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBe(1000); + }); +}); + +it.describe('while on pause', () => { + it('fastForward should not run nested immediate', async ({ page, calls }) => { + await page.clock.install(); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.evaluate(() => { + setTimeout(() => { + window.stub('outer'); + setTimeout(() => window.stub('inner'), 0); + }, 1000); + }); + await page.clock.fastForward(1000); + expect(calls).toEqual([{ params: ['outer'] }]); + await page.clock.fastForward(1); + expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]); + }); + + it('runFor should not run nested immediate', async ({ page, calls }) => { + await page.clock.install(); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.evaluate(() => { + setTimeout(() => { + window.stub('outer'); + setTimeout(() => window.stub('inner'), 0); + }, 1000); + }); + await page.clock.runFor(1000); + expect(calls).toEqual([{ params: ['outer'] }]); + await page.clock.runFor(1); + expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]); + }); + + it('runFor should not run nested immediate from microtask', async ({ page, calls }) => { + await page.clock.install(); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.evaluate(() => { + setTimeout(() => { + window.stub('outer'); + void Promise.resolve().then(() => setTimeout(() => window.stub('inner'), 0)); + }, 1000); + }); + await page.clock.runFor(1000); + expect(calls).toEqual([{ params: ['outer'] }]); + await page.clock.runFor(1); + expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]); }); }); From 8fd0a56427dc6caa88cf5f8f1b8dc95e18202228 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:52:38 -0700 Subject: [PATCH 047/144] feat(chromium-tip-of-tree): roll to r1230 (#31251) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index fc053c9b7a..385b408892 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1229", + "revision": "1230", "installByDefault": false, - "browserVersion": "127.0.6523.0" + "browserVersion": "127.0.6533.0" }, { "name": "firefox", From 2b257ea963eb4dc918659fc6c688e45a7fb8e007 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 11 Jun 2024 12:51:00 -0700 Subject: [PATCH 048/144] chore(clock): introduce pauseAt (#31255) --- docs/src/api/class-clock.md | 80 +++++++++---------- docs/src/clock.md | 42 ++++------ .../playwright-core/src/client/browser.ts | 2 +- packages/playwright-core/src/client/clock.ts | 8 +- .../playwright-core/src/protocol/validator.ts | 12 ++- packages/playwright-core/src/server/clock.ts | 14 +--- .../dispatchers/browserContextDispatcher.ts | 8 +- .../src/server/injected/clock.ts | 32 ++++---- packages/playwright-core/types/types.d.ts | 38 ++++----- packages/protocol/src/channels.ts | 24 +++--- packages/protocol/src/protocol.yml | 10 +-- tests/library/clock.spec.ts | 9 ++- tests/page/page-clock.spec.ts | 79 ++++++++---------- 13 files changed, 157 insertions(+), 201 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index bf8b9ac59a..9099d33a4f 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -45,43 +45,6 @@ await page.Clock.FastForwardAsync("30:00"); Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. -## async method: Clock.fastForwardTo -* since: v1.45 - -Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and -reopening it at the specified time. - -**Usage** - -```js -await page.clock.fastForwardTo(new Date('2020-02-02')); -await page.clock.fastForwardTo('2020-02-02'); -``` - -```python async -await page.clock.fast_forward_to(datetime.datetime(2020, 2, 2)) -await page.clock.fast_forward_to("2020-02-02") -``` - -```python sync -page.clock.fast_forward_to(datetime.datetime(2020, 2, 2)) -page.clock.fast_forward_to("2020-02-02") -``` - -```java -page.clock().fastForwardTo(Instant.parse("2020-02-02")); -page.clock().fastForwardTo("2020-02-02"); -``` - -```csharp -await page.Clock.FastForwardToAsync(DateTime.Parse("2020-02-02")); -await page.Clock.FastForwardToAsync("2020-02-02"); -``` - -### param: Clock.fastForwardTo.time -* since: v1.45 -- `time` <[int]|[string]|[Date]> - ## async method: Clock.install * since: v1.45 @@ -145,10 +108,47 @@ await page.Clock.RunForAsync("30:00"); Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. -## async method: Clock.pause +## async method: Clock.pauseAt * since: v1.45 -Pause timers. Once this method is called, no timers are fired unless [`method: Clock.runFor`], [`method: Clock.fastForward`], [`method: Clock.fastForwardTo`] or [`method: Clock.resume`] is called. +Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers +are fired unless [`method: Clock.runFor`], [`method: Clock.fastForward`], [`method: Clock.pauseAt`] or [`method: Clock.resume`] is called. + +Only fires due timers at most once. +This is equivalent to user closing the laptop lid for a while and reopening it at the specified time and +pausing. + +**Usage** + +```js +await page.clock.pauseAt(new Date('2020-02-02')); +await page.clock.pauseAt('2020-02-02'); +``` + +```python async +await page.clock.pause_at(datetime.datetime(2020, 2, 2)) +await page.clock.pause_at("2020-02-02") +``` + +```python sync +page.clock.pause_at(datetime.datetime(2020, 2, 2)) +page.clock.pause_at("2020-02-02") +``` + +```java +page.clock().pauseAt(Instant.parse("2020-02-02")); +page.clock().pauseAt("2020-02-02"); +``` + +```csharp +await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02")); +await page.Clock.PauseAtAsync("2020-02-02"); +``` + +### param: Clock.pauseAt.time +* since: v1.45 +- `time` <[int]|[string]|[Date]> + ## async method: Clock.resume * since: v1.45 @@ -202,7 +202,7 @@ Time to be set. ## async method: Clock.setSystemTime * since: v1.45 -Sets current system time but does not trigger any timers, unlike [`method: Clock.fastForwardTo`]. +Sets current system time but does not trigger any timers. **Usage** diff --git a/docs/src/clock.md b/docs/src/clock.md index 82fba434ec..b89dfd93d6 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -67,10 +67,9 @@ In this case, you can install the clock and fast forward to the time of interest await page.clock.install({ time: new Date('2024-02-02T08:00:00') }); await page.goto('http://localhost:3333'); -// Take control over time flow. -await page.clock.pause(); -// Pretend that the user closed the laptop lid and opened it again at 10am. -await page.clock.fastForwardTo(new Date('2024-02-02T10:00:00')); +// Pretend that the user closed the laptop lid and opened it again at 10am, +// Pause the time once reached that point. +await page.clock.pauseAt(new Date('2024-02-02T10:00:00')); // Assert the page state. await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); @@ -86,10 +85,9 @@ await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM await page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0)) await page.goto("http://localhost:3333") -# Take control over time flow. -await page.clock.pause() # Pretend that the user closed the laptop lid and opened it again at 10am. -await page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) +# Pause the time once reached that point. +await page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0)) # Assert the page state. await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM") @@ -105,10 +103,9 @@ await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30: page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0)) page.goto("http://localhost:3333") -# Take control over time flow. -page.clock.pause() # Pretend that the user closed the laptop lid and opened it again at 10am. -page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) +# Pause the time once reached that point. +page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0)) # Assert the page state. expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM") @@ -125,10 +122,9 @@ page.clock().install(new Clock.InstallOptions().setTime(Instant.parse("2024-02-0 page.navigate("http://localhost:3333"); Locator locator = page.getByTestId("current-time"); -// Take control over time flow. -page.clock().pause(); // Pretend that the user closed the laptop lid and opened it again at 10am. -page.clock().fastForwardTo(Instant.parse("2024-02-02T10:00:00")); +// Pause the time once reached that point. +page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00")); // Assert the page state. assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); @@ -147,10 +143,9 @@ await Page.Clock.InstallAsync(new }); await Page.GotoAsync("http://localhost:3333"); -// Take control over time flow. -await Page.Clock.PauseAsync(); // Pretend that the user closed the laptop lid and opened it again at 10am. -await Page.Clock.FastForwardToAsync(new DateTime(2024, 2, 2, 10, 0, 0)); +// Pause the time once reached that point. +await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0)); // Assert the page state. await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM"); @@ -272,8 +267,7 @@ await page.goto('http://localhost:3333'); // Pause the time flow, stop the timers, you now have manual control // over the page time. -await page.clock.pause(); -await page.clock.fastForwardTo(new Date('2024-02-02T10: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'); // Tick through time manually, firing all timers in the process. @@ -292,8 +286,7 @@ locator = page.get_by_test_id("current-time") # Pause the time flow, stop the timers, you now have manual control # over the page time. -await page.clock.pause() -await page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) +await page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0)) await expect(locator).to_have_text("2/2/2024, 10:00:00 AM") # Tick through time manually, firing all timers in the process. @@ -312,8 +305,7 @@ locator = page.get_by_test_id("current-time") # Pause the time flow, stop the timers, you now have manual control # over the page time. -page.clock.pause() -page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) +page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0)) expect(locator).to_have_text("2/2/2024, 10:00:00 AM") # Tick through time manually, firing all timers in the process. @@ -331,8 +323,7 @@ Locator locator = page.getByTestId("current-time"); // Pause the time flow, stop the timers, you now have manual control // over the page time. -page.clock().pause(); -page.clock().fastForwardTo(Instant.parse("2024-02-02T10:00:00")); +page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00")); assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); // Tick through time manually, firing all timers in the process. @@ -352,8 +343,7 @@ var locator = page.GetByTestId("current-time"); // Pause the time flow, stop the timers, you now have manual control // over the page time. -await Page.Clock.PauseAsync(); -await Page.Clock.FastForwardToAsync(new DateTime(2024, 2, 2, 10, 0, 0)); +await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0)); await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); // Tick through time manually, firing all timers in the process. diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index c1a9b0259d..b6f55e3b29 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -88,7 +88,7 @@ export class Browser extends ChannelOwner implements ap if (!forReuse && !!process.env.PW_FREEZE_TIME) { await this._wrapApiCall(async () => { await context.clock.install({ time: 0 }); - await context.clock.pause(); + await context.clock.pauseAt(1000); }, true); } return context; diff --git a/packages/playwright-core/src/client/clock.ts b/packages/playwright-core/src/client/clock.ts index f4d2133b40..32faa7a4d4 100644 --- a/packages/playwright-core/src/client/clock.ts +++ b/packages/playwright-core/src/client/clock.ts @@ -32,12 +32,8 @@ export class Clock implements api.Clock { await this._browserContext._channel.clockFastForward(parseTicks(ticks)); } - async fastForwardTo(time: number | string | Date) { - await this._browserContext._channel.clockFastForwardTo(parseTime(time)); - } - - async pause() { - await this._browserContext._channel.clockPause({}); + async pauseAt(time: number | string | Date) { + await this._browserContext._channel.clockPauseAt(parseTime(time)); } async resume() { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 169b7574df..9f5af4ed72 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -968,18 +968,16 @@ scheme.BrowserContextClockFastForwardParams = tObject({ ticksString: tOptional(tString), }); scheme.BrowserContextClockFastForwardResult = tOptional(tObject({})); -scheme.BrowserContextClockFastForwardToParams = tObject({ - timeNumber: tOptional(tNumber), - timeString: tOptional(tString), -}); -scheme.BrowserContextClockFastForwardToResult = tOptional(tObject({})); scheme.BrowserContextClockInstallParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); scheme.BrowserContextClockInstallResult = tOptional(tObject({})); -scheme.BrowserContextClockPauseParams = tOptional(tObject({})); -scheme.BrowserContextClockPauseResult = tOptional(tObject({})); +scheme.BrowserContextClockPauseAtParams = tObject({ + timeNumber: tOptional(tNumber), + timeString: tOptional(tString), +}); +scheme.BrowserContextClockPauseAtResult = tOptional(tObject({})); scheme.BrowserContextClockResumeParams = tOptional(tObject({})); scheme.BrowserContextClockResumeResult = tOptional(tObject({})); scheme.BrowserContextClockRunForParams = tObject({ diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index 3cbb0b1b0a..58a1f72720 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -33,13 +33,6 @@ export class Clock { await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForward(${ticksMillis})`); } - async fastForwardTo(ticks: number | string) { - await this._installIfNeeded(); - const timeMillis = parseTime(ticks); - await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForwardTo', ${Date.now()}, ${timeMillis})`); - await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForwardTo(${timeMillis})`); - } - async install(time: number | string | undefined) { await this._installIfNeeded(); const timeMillis = time !== undefined ? parseTime(time) : Date.now(); @@ -47,10 +40,11 @@ export class Clock { await this._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`); } - async pause() { + async pauseAt(ticks: number | string) { await this._installIfNeeded(); - await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pause', ${Date.now()})`); - await this._evaluateInFrames(`globalThis.__pwClock.controller.pause()`); + const timeMillis = parseTime(ticks); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pauseAt', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.pauseAt(${timeMillis})`); } async resume() { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 86b9a5576c..585cf1d20b 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -316,16 +316,12 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.clock.fastForwardTo(params.timeString ?? params.timeNumber ?? 0); - } - async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise { await this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined); } - async clockPause(params: channels.BrowserContextClockPauseParams, metadata?: CallMetadata | undefined): Promise { - await this._context.clock.pause(); + async clockPauseAt(params: channels.BrowserContextClockPauseAtParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.pauseAt(params.timeString ?? params.timeNumber ?? 0); } async clockResume(params: channels.BrowserContextClockResumeParams, metadata?: CallMetadata | undefined): Promise { diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index 5f6d4cf613..5b8abd3271 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -69,7 +69,7 @@ type Time = { origin: number; }; -type LogEntryType = 'fastForward' | 'fastForwardTo' | 'install' | 'pause' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime'; +type LogEntryType = 'fastForward' |'install' | 'pauseAt' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime'; export class ClockController { readonly _now: Time; @@ -163,9 +163,10 @@ export class ClockController { throw firstException; } - pause() { + async pauseAt(time: number) { this._replayLogOnce(); this._innerPause(); + await this._innerFastForwardTo(time); } private _innerPause() { @@ -218,18 +219,18 @@ export class ClockController { async fastForward(ticks: number) { this._replayLogOnce(); - const ms = ticks | 0; - for (const timer of this._timers.values()) { - if (this._now.ticks + ms > timer.callAt) - timer.callAt = this._now.ticks + ms; - } - await this.runFor(ms); + await this._innerFastForwardTo(this._now.ticks + ticks | 0); } - async fastForwardTo(time: number) { - this._replayLogOnce(); - const ticks = time - this._now.time; - await this.fastForward(ticks); + + private async _innerFastForwardTo(toTicks: number) { + if (toTicks < this._now.ticks) + throw new Error('Cannot fast-forward to the past'); + for (const timer of this._timers.values()) { + if (toTicks > timer.callAt) + timer.callAt = toTicks; + } + await this._runTo(toTicks); } addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { @@ -381,11 +382,10 @@ export class ClockController { this._innerSetTime(param!); } else if (type === 'fastForward' || type === 'runFor') { this._advanceNow(this._now.ticks + param!); - } else if (type === 'fastForwardTo') { - this._innerSetTime(param!); - } else if (type === 'pause') { - this._innerPause(); + } else if (type === 'pauseAt') { isPaused = true; + this._innerPause(); + this._innerSetTime(param!); } else if (type === 'resume') { this._innerResume(); isPaused = false; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 7fe30e2ef2..76d12b2d9a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17263,21 +17263,6 @@ export interface Clock { */ fastForward(ticks: number|string): Promise; - /** - * Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user - * closing the laptop lid for a while and reopening it at the specified time. - * - * **Usage** - * - * ```js - * await page.clock.fastForwardTo(new Date('2020-02-02')); - * await page.clock.fastForwardTo('2020-02-02'); - * ``` - * - * @param time - */ - fastForwardTo(time: number|string|Date): Promise; - /** * Install fake implementations for the following time-related functions: * - `Date` @@ -17305,13 +17290,25 @@ export interface Clock { }): Promise; /** - * Pause timers. Once this method is called, no timers are fired unless - * [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for), + * Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers are fired + * unless [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for), * [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward), - * [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to) or + * [clock.pauseAt(time)](https://playwright.dev/docs/api/class-clock#clock-pause-at) or * [clock.resume()](https://playwright.dev/docs/api/class-clock#clock-resume) is called. + * + * Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it + * at the specified time and pausing. + * + * **Usage** + * + * ```js + * await page.clock.pauseAt(new Date('2020-02-02')); + * await page.clock.pauseAt('2020-02-02'); + * ``` + * + * @param time */ - pause(): Promise; + pauseAt(time: number|string|Date): Promise; /** * Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual. @@ -17349,8 +17346,7 @@ export interface Clock { setFixedTime(time: number|string|Date): Promise; /** - * Sets current system time but does not trigger any timers, unlike - * [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to). + * Sets current system time but does not trigger any timers. * * **Usage** * diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 85bd3a8ffa..2ddeeee837 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1461,9 +1461,8 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise; updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise; clockFastForward(params: BrowserContextClockFastForwardParams, metadata?: CallMetadata): Promise; - clockFastForwardTo(params: BrowserContextClockFastForwardToParams, metadata?: CallMetadata): Promise; clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise; - clockPause(params?: BrowserContextClockPauseParams, metadata?: CallMetadata): Promise; + clockPauseAt(params: BrowserContextClockPauseAtParams, metadata?: CallMetadata): Promise; clockResume(params?: BrowserContextClockResumeParams, metadata?: CallMetadata): Promise; clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise; clockSetFixedTime(params: BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata): Promise; @@ -1765,15 +1764,6 @@ export type BrowserContextClockFastForwardOptions = { ticksString?: string, }; export type BrowserContextClockFastForwardResult = void; -export type BrowserContextClockFastForwardToParams = { - timeNumber?: number, - timeString?: string, -}; -export type BrowserContextClockFastForwardToOptions = { - timeNumber?: number, - timeString?: string, -}; -export type BrowserContextClockFastForwardToResult = void; export type BrowserContextClockInstallParams = { timeNumber?: number, timeString?: string, @@ -1783,9 +1773,15 @@ export type BrowserContextClockInstallOptions = { timeString?: string, }; export type BrowserContextClockInstallResult = void; -export type BrowserContextClockPauseParams = {}; -export type BrowserContextClockPauseOptions = {}; -export type BrowserContextClockPauseResult = void; +export type BrowserContextClockPauseAtParams = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockPauseAtOptions = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockPauseAtResult = void; export type BrowserContextClockResumeParams = {}; export type BrowserContextClockResumeOptions = {}; export type BrowserContextClockResumeResult = void; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 65fc2db4de..f02045d51e 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1209,17 +1209,15 @@ BrowserContext: ticksNumber: number? ticksString: string? - clockFastForwardTo: - parameters: - timeNumber: number? - timeString: string? - clockInstall: parameters: timeNumber: number? timeString: string? - clockPause: + clockPauseAt: + parameters: + timeNumber: number? + timeString: string? clockResume: diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index a77356c172..da82f2d960 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -1342,10 +1342,11 @@ it.describe('fastForward', () => { clock.setInterval(shortTimers[1], 100); clock.requestAnimationFrame(shortTimers[2]); await clock.fastForward(1500); - for (const stub of longTimers) - expect(stub.called).toBeFalsy(); - for (const stub of shortTimers) - expect(stub.callCount).toBe(1); + expect(longTimers[0].called).toBeFalsy(); + expect(longTimers[1].called).toBeFalsy(); + expect(shortTimers[0].callCount).toBe(1); + expect(shortTimers[1].callCount).toBe(1); + expect(shortTimers[2].callCount).toBe(1); }); }); diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index fdc01c5570..58a0d130e2 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -36,8 +36,8 @@ const it = test.extend<{ calls: { params: any[] }[] }>({ it.describe('runFor', () => { it.beforeEach(async ({ page }) => { - await page.clock.install(); - await page.clock.pause(); + await page.clock.install({ time: 0 }); + await page.clock.pauseAt(1000); }); it('triggers immediately without specified delay', async ({ page, calls }) => { @@ -171,9 +171,8 @@ it.describe('runFor', () => { it.describe('fastForward', () => { it.beforeEach(async ({ page }) => { - await page.clock.install(); - await page.clock.pause(); - await page.clock.setSystemTime(0); + await page.clock.install({ time: 0 }); + await page.clock.pauseAt(1000); }); it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { @@ -194,7 +193,7 @@ it.describe('fastForward', () => { }); await page.clock.fastForward(2000); - expect(calls).toEqual([{ params: [2000] }]); + expect(calls).toEqual([{ params: [1000 + 2000] }]); }); it('supports string time arguments', async ({ page, calls }) => { @@ -204,15 +203,14 @@ it.describe('fastForward', () => { }, 100000); // 100000 = 1:40 }); await page.clock.fastForward('01:50'); - expect(calls).toEqual([{ params: [110000] }]); + expect(calls).toEqual([{ params: [1000 + 110000] }]); }); }); it.describe('fastForwardTo', () => { it.beforeEach(async ({ page }) => { - await page.clock.install(); - await page.clock.pause(); - await page.clock.setSystemTime(0); + await page.clock.install({ time: 0 }); + await page.clock.pauseAt(1000); }); it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { @@ -221,7 +219,7 @@ it.describe('fastForwardTo', () => { window.stub('should not be logged'); }, 1000); }); - await page.clock.fastForwardTo(500); + await page.clock.fastForward(500); expect(calls).toEqual([]); }); @@ -232,16 +230,15 @@ it.describe('fastForwardTo', () => { }, 1000); }); - await page.clock.fastForwardTo(2000); - expect(calls).toEqual([{ params: [2000] }]); + await page.clock.fastForward(2000); + expect(calls).toEqual([{ params: [1000 + 2000] }]); }); }); it.describe('stubTimers', () => { it.beforeEach(async ({ page }) => { - await page.clock.install(); - await page.clock.pause(); - await page.clock.setSystemTime(0); + await page.clock.install({ time: 0 }); + await page.clock.pauseAt(1000); }); it('sets initial timestamp', async ({ page, calls }) => { await page.clock.setSystemTime(1400); @@ -295,20 +292,19 @@ it.describe('stubTimers', () => { return { prev, next }; }); await page.clock.runFor(1000); - expect(await promise).toEqual({ prev: 0, next: 1000 }); + expect(await promise).toEqual({ prev: 1000, next: 2000 }); }); it('fakes Date constructor', async ({ page }) => { const now = await page.evaluate(() => new Date().getTime()); - expect(now).toBe(0); + expect(now).toBe(1000); }); }); it.describe('stubTimers', () => { it('replaces global performance.timeOrigin', async ({ page }) => { await page.clock.install({ time: 1000 }); - await page.clock.pause(); - await page.clock.setSystemTime(1000); + await page.clock.pauseAt(2000); const promise = page.evaluate(async () => { const prev = performance.now(); await new Promise(f => setTimeout(f, 1000)); @@ -317,16 +313,15 @@ it.describe('stubTimers', () => { }); await page.clock.runFor(1000); expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000); - expect(await promise).toEqual({ prev: 0, next: 1000 }); + expect(await promise).toEqual({ prev: 2000, next: 3000 }); }); }); it.describe('popup', () => { it('should tick after popup', async ({ page }) => { await page.clock.install(); - await page.clock.pause(); const now = new Date('2015-09-25'); - await page.clock.setSystemTime(now); + await page.clock.pauseAt(now); const [popup] = await Promise.all([ page.waitForEvent('popup'), page.evaluate(() => window.open('about:blank')), @@ -340,9 +335,8 @@ it.describe('popup', () => { it('should tick before popup', async ({ page }) => { await page.clock.install(); - await page.clock.pause(); const now = new Date('2015-09-25'); - await page.clock.setSystemTime(now); + await page.clock.pauseAt(now); await page.clock.runFor(1000); const [popup] = await Promise.all([ @@ -358,7 +352,6 @@ it.describe('popup', () => { res.setHeader('Content-Type', 'text/html'); res.end(``); }); - await page.clock.setSystemTime(0); await page.goto(server.EMPTY_PAGE); // Wait for 2 second in real life to check that it is past in popup. await page.waitForTimeout(2000); @@ -376,8 +369,7 @@ it.describe('popup', () => { res.end(``); }); await page.clock.install(); - await page.clock.pause(); - await page.clock.setSystemTime(0); + await page.clock.pauseAt(1000); await page.goto(server.EMPTY_PAGE); // Wait for 2 second in real life to check that it is past in popup. await page.waitForTimeout(2000); @@ -386,7 +378,7 @@ it.describe('popup', () => { page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'), ]); const popupTime = await popup.evaluate('time'); - expect(popupTime).toBe(0); + expect(popupTime).toBe(1000); }); }); @@ -457,7 +449,7 @@ it.describe('while running', () => { it('should fastForwardTo', async ({ page }) => { await page.clock.install({ time: 0 }); await page.goto('data:text/html,'); - await page.clock.fastForwardTo(10000); + await page.clock.fastForward(10000); const now = await page.evaluate(() => Date.now()); expect(now).toBeGreaterThanOrEqual(10000); expect(now).toBeLessThanOrEqual(11000); @@ -466,7 +458,7 @@ it.describe('while running', () => { it('should pause', async ({ page }) => { await page.clock.install({ time: 0 }); await page.goto('data:text/html,'); - await page.clock.pause(); + await page.clock.pauseAt(1000); await page.waitForTimeout(1000); await page.clock.resume(); const now = await page.evaluate(() => Date.now()); @@ -474,20 +466,19 @@ it.describe('while running', () => { expect(now).toBeLessThanOrEqual(1000); }); - it('should pause and fastForwardTo', async ({ page }) => { + it('should pause and fastForward', async ({ page }) => { await page.clock.install({ time: 0 }); await page.goto('data:text/html,'); - await page.clock.pause(); - await page.clock.fastForwardTo(1000); + await page.clock.pauseAt(1000); + await page.clock.fastForward(1000); const now = await page.evaluate(() => Date.now()); - expect(now).toBe(1000); + expect(now).toBe(2000); }); it('should set system time on pause', async ({ page }) => { - await page.clock.install(); + await page.clock.install({ time: 0 }); await page.goto('data:text/html,'); - await page.clock.pause(); - await page.clock.setSystemTime(1000); + await page.clock.pauseAt(1000); const now = await page.evaluate(() => Date.now()); expect(now).toBe(1000); }); @@ -495,9 +486,9 @@ it.describe('while running', () => { it.describe('while on pause', () => { it('fastForward should not run nested immediate', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.install({ time: 0 }); await page.goto('data:text/html,'); - await page.clock.pause(); + await page.clock.pauseAt(1000); await page.evaluate(() => { setTimeout(() => { window.stub('outer'); @@ -511,9 +502,9 @@ it.describe('while on pause', () => { }); it('runFor should not run nested immediate', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.install({ time: 0 }); await page.goto('data:text/html,'); - await page.clock.pause(); + await page.clock.pauseAt(1000); await page.evaluate(() => { setTimeout(() => { window.stub('outer'); @@ -527,9 +518,9 @@ it.describe('while on pause', () => { }); it('runFor should not run nested immediate from microtask', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.install({ time: 0 }); await page.goto('data:text/html,'); - await page.clock.pause(); + await page.clock.pauseAt(1000); await page.evaluate(() => { setTimeout(() => { window.stub('outer'); From e07b46883d4dc1326957ec8a8d0ede78357902c4 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:14:30 -0700 Subject: [PATCH 049/144] feat(webkit): roll to r2029 (#31257) --- packages/playwright-core/browsers.json | 2 +- .../src/server/webkit/protocol.d.ts | 24 +++++++++++-------- .../src/server/webkit/wkPage.ts | 5 +++- tests/library/browsercontext-viewport.spec.ts | 11 +++++++++ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 385b408892..5234f96102 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2022", + "revision": "2029", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index 0143edbf7f..ba9ba9e5d3 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -2122,6 +2122,10 @@ export module Protocol { * Array of DOMNode ids of any children marked as selected. */ selectedChildNodeIds?: NodeId[]; + /** + * On / off state of switch form controls. + */ + switchState?: "off"|"on"; } /** * A structure holding an RGBA color. @@ -4584,6 +4588,14 @@ might return multiple quads for inline nodes. } export type resetPermissionsReturnValue = { } + /** + * Overrides window.orientation with provided value. + */ + export type setOrientationOverrideParameters = { + angle?: number; + } + export type setOrientationOverrideReturnValue = { + } } /** @@ -7351,14 +7363,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the } export type crashReturnValue = { } - /** - * Overrides window.orientation with provided value. - */ - export type setOrientationOverrideParameters = { - angle?: number; - } - export type setOrientationOverrideReturnValue = { - } /** * Ensures that the scroll regions are up to date. */ @@ -9509,6 +9513,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "Emulation.setActiveAndFocused": Emulation.setActiveAndFocusedParameters; "Emulation.grantPermissions": Emulation.grantPermissionsParameters; "Emulation.resetPermissions": Emulation.resetPermissionsParameters; + "Emulation.setOrientationOverride": Emulation.setOrientationOverrideParameters; "Heap.enable": Heap.enableParameters; "Heap.disable": Heap.disableParameters; "Heap.gc": Heap.gcParameters; @@ -9591,7 +9596,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "Page.createUserWorld": Page.createUserWorldParameters; "Page.setBypassCSP": Page.setBypassCSPParameters; "Page.crash": Page.crashParameters; - "Page.setOrientationOverride": Page.setOrientationOverrideParameters; "Page.updateScrollingState": Page.updateScrollingStateParameters; "Playwright.enable": Playwright.enableParameters; "Playwright.disable": Playwright.disableParameters; @@ -9820,6 +9824,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "Emulation.setActiveAndFocused": Emulation.setActiveAndFocusedReturnValue; "Emulation.grantPermissions": Emulation.grantPermissionsReturnValue; "Emulation.resetPermissions": Emulation.resetPermissionsReturnValue; + "Emulation.setOrientationOverride": Emulation.setOrientationOverrideReturnValue; "Heap.enable": Heap.enableReturnValue; "Heap.disable": Heap.disableReturnValue; "Heap.gc": Heap.gcReturnValue; @@ -9902,7 +9907,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "Page.createUserWorld": Page.createUserWorldReturnValue; "Page.setBypassCSP": Page.setBypassCSPReturnValue; "Page.crash": Page.crashReturnValue; - "Page.setOrientationOverride": Page.setOrientationOverrideReturnValue; "Page.updateScrollingState": Page.updateScrollingStateReturnValue; "Playwright.enable": Playwright.enableReturnValue; "Playwright.disable": Playwright.disableReturnValue; diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index e0326072f7..8d441240bc 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -16,6 +16,7 @@ */ import path from 'path'; +import os from 'os'; import { PNG, jpegjs } from '../../utilsBundle'; import { splitErrorMessage } from '../../utils/stackTrace'; import { assert, createGuid, debugAssert, headersArrayToObject } from '../../utils'; @@ -713,7 +714,9 @@ export class WKPage implements PageDelegate { ]; if (options.isMobile) { const angle = viewportSize.width > viewportSize.height ? 90 : 0; - promises.push(this._session.send('Page.setOrientationOverride', { angle })); + // Special handling for macOS 12. + const useLegacySetOrientationOverrideMethod = os.platform() === 'darwin' && parseInt(os.release().split('.')[0], 10) <= 21; + promises.push(this._pageProxySession.send(useLegacySetOrientationOverrideMethod ? 'Page.setOrientationOverride' as any : 'Emulation.setOrientationOverride', { angle })); } await Promise.all(promises); } diff --git a/tests/library/browsercontext-viewport.spec.ts b/tests/library/browsercontext-viewport.spec.ts index 6c37ee3ac9..19dd03d335 100644 --- a/tests/library/browsercontext-viewport.spec.ts +++ b/tests/library/browsercontext-viewport.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { devices } from '@playwright/test'; import { contextTest as it, expect } from '../config/browserTest'; import { browserTest } from '../config/browserTest'; import { verifyViewport } from '../config/utils'; @@ -175,3 +176,13 @@ browserTest('should be able to get correct orientation angle on non-mobile devic expect(await page.evaluate(() => window.screen.orientation.angle)).toBe(0); await context.close(); }); + +it('should set window.screen.orientation.type for mobile devices', async ({ contextFactory, browserName, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31151' }); + it.skip(browserName === 'firefox', 'Firefox does not support mobile emulation'); + const context = await contextFactory(devices['iPhone 14']); + const page = await context.newPage(); + await page.goto(server.PREFIX + '/index.html'); + expect(await page.evaluate(() => window.screen.orientation.type)).toBe('portrait-primary'); + await context.close(); +}); From b24fd0dd27135285bf6d7c73bd16ed847605a5d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:37:43 -0700 Subject: [PATCH 050/144] chore(deps): bump @azure/identity from 4.1.0 to 4.2.1 in /utils/flakiness-dashboard (#31261) --- utils/flakiness-dashboard/package-lock.json | 96 ++++++++------------- utils/flakiness-dashboard/package.json | 2 +- 2 files changed, 39 insertions(+), 59 deletions(-) diff --git a/utils/flakiness-dashboard/package-lock.json b/utils/flakiness-dashboard/package-lock.json index 43efb92951..abb1e23ae0 100644 --- a/utils/flakiness-dashboard/package-lock.json +++ b/utils/flakiness-dashboard/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "flakiness-dashboard", "dependencies": { - "@azure/identity": "^4.1.0", + "@azure/identity": "^4.2.1", "@azure/storage-blob": "^12.17.0" } }, @@ -214,9 +214,9 @@ } }, "node_modules/@azure/identity": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.1.0.tgz", - "integrity": "sha512-BhYkF8Xr2gXjyDxocm0pc9RI5J5a1jw8iW0dw6Bx95OGdYbuMyFZrrwNw4eYSqQ2BB6FZOqpJP3vjsAqRcvDhw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.5.0", @@ -226,7 +226,7 @@ "@azure/core-util": "^1.3.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^3.11.1", - "@azure/msal-node": "^2.6.6", + "@azure/msal-node": "^2.9.2", "events": "^3.0.0", "jws": "^4.0.0", "open": "^8.0.0", @@ -279,11 +279,11 @@ } }, "node_modules/@azure/msal-node": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.7.0.tgz", - "integrity": "sha512-wXD8LkUvHICeSWZydqg6o8Yvv+grlBEcmLGu+QEI4FcwFendbTEZrlSygnAXXSOCVaGAirWLchca35qrgpO6Jw==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", "dependencies": { - "@azure/msal-common": "14.9.0", + "@azure/msal-common": "14.12.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -291,6 +291,14 @@ "node": ">=16" } }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@azure/storage-blob": { "version": "12.17.0", "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz", @@ -588,17 +596,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -691,12 +688,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -772,11 +766,6 @@ "engines": { "node": ">=4.0" } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } }, "dependencies": { @@ -947,9 +936,9 @@ } }, "@azure/identity": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.1.0.tgz", - "integrity": "sha512-BhYkF8Xr2gXjyDxocm0pc9RI5J5a1jw8iW0dw6Bx95OGdYbuMyFZrrwNw4eYSqQ2BB6FZOqpJP3vjsAqRcvDhw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.5.0", @@ -959,7 +948,7 @@ "@azure/core-util": "^1.3.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^3.11.1", - "@azure/msal-node": "^2.6.6", + "@azure/msal-node": "^2.9.2", "events": "^3.0.0", "jws": "^4.0.0", "open": "^8.0.0", @@ -999,13 +988,20 @@ "integrity": "sha512-yzBPRlWPnTBeixxLNI3BBIgF5/bHpbhoRVuuDBnYjCyWRavaPUsKAHUDYLqpGkBLDciA6TCc6GOxN4/S3WiSxg==" }, "@azure/msal-node": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.7.0.tgz", - "integrity": "sha512-wXD8LkUvHICeSWZydqg6o8Yvv+grlBEcmLGu+QEI4FcwFendbTEZrlSygnAXXSOCVaGAirWLchca35qrgpO6Jw==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", "requires": { - "@azure/msal-common": "14.9.0", + "@azure/msal-common": "14.12.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" + }, + "dependencies": { + "@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==" + } } }, "@azure/storage-blob": { @@ -1252,14 +1248,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1312,12 +1300,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" }, "stoppable": { "version": "1.1.0", @@ -1371,11 +1356,6 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/utils/flakiness-dashboard/package.json b/utils/flakiness-dashboard/package.json index d8844674b9..29f60a6b70 100644 --- a/utils/flakiness-dashboard/package.json +++ b/utils/flakiness-dashboard/package.json @@ -5,7 +5,7 @@ "main": "processing/index.js", "author": "", "dependencies": { - "@azure/identity": "^4.1.0", + "@azure/identity": "^4.2.1", "@azure/storage-blob": "^12.17.0" } } From 732e7393d39078ef252b29d8d291aa1b78abc3e8 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 11 Jun 2024 16:05:21 -0700 Subject: [PATCH 051/144] =?UTF-8?q?Revert=20"feat(test):=20add=20`URL`=20f?= =?UTF-8?q?ield=20to=20annotations=20for=20hyperlink=20disp=E2=80=A6=20(#3?= =?UTF-8?q?1259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …lay (#30665)" --- docs/src/test-api/class-test.md | 18 ++---------------- .../html-reporter/src/testCaseView.spec.tsx | 4 ++-- packages/html-reporter/src/types.ts | 2 +- packages/playwright/src/common/config.ts | 2 +- packages/playwright/types/test.d.ts | 6 ++---- 5 files changed, 8 insertions(+), 24 deletions(-) diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 7b03353361..9b56fbf2e6 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -71,8 +71,7 @@ import { test, expect } from '@playwright/test'; test('basic test', { annotation: { type: 'issue', - description: 'feature tags API', - url: 'https://github.com/microsoft/playwright/issues/23180' + description: 'https://github.com/microsoft/playwright/issues/23180', }, }, async ({ page }) => { await page.goto('https://playwright.dev/'); @@ -98,8 +97,7 @@ Test title. - `tag` ?<[string]|[Array]<[string]>> - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> Annotation type, for example `'issue'`. - - `description` ?<[string]> Optional annotation description. - - `url` ?<[string]> Optional for example an issue url. + - `description` ?<[string]> Optional annotation description, for example an issue url. Additional test details. @@ -442,7 +440,6 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> Additional details for all tests in the group. @@ -571,7 +568,6 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -627,7 +623,6 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -681,7 +676,6 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -733,7 +727,6 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -789,7 +782,6 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -847,7 +839,6 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -900,7 +891,6 @@ Group title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.describe`] for details description. @@ -1119,7 +1109,6 @@ Test title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.(call)`] for test details description. @@ -1225,7 +1214,6 @@ Test title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.(call)`] for test details description. @@ -1303,7 +1291,6 @@ Test title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.(call)`] for test details description. @@ -1449,7 +1436,6 @@ Test title. - `annotation` ?<[Object]|[Array]<[Object]>> - `type` <[string]> - `description` ?<[string]> - - `url` ?<[string]> See [`method: Test.(call)`] for test details description. diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index 0a4da91b6e..28fe8247f5 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -52,8 +52,8 @@ const testCase: TestCase = { projectName: 'chromium', location: { file: 'test.spec.ts', line: 42, column: 0 }, annotations: [ - { type: 'annotation', description: 'Annotation text', url: 'example url' }, - { type: 'annotation', description: 'Another annotation text', url: 'Another example url' }, + { type: 'annotation', description: 'Annotation text' }, + { type: 'annotation', description: 'Another annotation text' }, ], tags: [], outcome: 'expected', diff --git a/packages/html-reporter/src/types.ts b/packages/html-reporter/src/types.ts index 79dc2f3812..733e88e8b9 100644 --- a/packages/html-reporter/src/types.ts +++ b/packages/html-reporter/src/types.ts @@ -59,7 +59,7 @@ export type TestFileSummary = { stats: Stats; }; -export type TestCaseAnnotation = { type: string, description?: string, url?: string}; +export type TestCaseAnnotation = { type: string, description?: string }; export type TestCaseSummary = { testId: string, diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index ac7b313820..025b7e3838 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -35,7 +35,7 @@ export type FixturesWithLocation = { fixtures: Fixtures; location: Location; }; -export type Annotation = { type: string, description?: string, url?: string }; +export type Annotation = { type: string, description?: string }; export const defaultTimeout = 30000; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 3d68c341e8..2b1f105d7e 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -2198,8 +2198,7 @@ interface TestFunction { * test('basic test', { * annotation: { * type: 'issue', - * description: 'feature tags API', - * url: 'https://github.com/microsoft/playwright/issues/23180' + * description: 'https://github.com/microsoft/playwright/issues/23180', * }, * }, async ({ page }) => { * await page.goto('https://playwright.dev/'); @@ -2275,8 +2274,7 @@ interface TestFunction { * test('basic test', { * annotation: { * type: 'issue', - * description: 'feature tags API', - * url: 'https://github.com/microsoft/playwright/issues/23180' + * description: 'https://github.com/microsoft/playwright/issues/23180', * }, * }, async ({ page }) => { * await page.goto('https://playwright.dev/'); From cf400a6080ff568347f363be3745c09caa9a1baf Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 11 Jun 2024 16:05:35 -0700 Subject: [PATCH 052/144] =?UTF-8?q?Revert=20"feat(test=20runner):=20shuffl?= =?UTF-8?q?e=20order=20of=20tests=20with=20sharding=20seed=20=E2=80=A6=20(?= =?UTF-8?q?#31260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …(#30817)" --- docs/src/test-api/class-testconfig.md | 21 -------- docs/src/test-sharding-js.md | 6 --- packages/playwright/src/common/config.ts | 2 - packages/playwright/src/common/ipc.ts | 1 - packages/playwright/src/program.ts | 2 - packages/playwright/src/runner/loadUtils.ts | 4 -- packages/playwright/src/runner/shuffle.ts | 59 --------------------- packages/playwright/types/test.d.ts | 23 -------- 8 files changed, 118 deletions(-) delete mode 100644 packages/playwright/src/runner/shuffle.ts diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index 93a3c25cab..4130bdd66d 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -482,27 +482,6 @@ export default defineConfig({ ``` -## property: TestConfig.shardingSeed - -* since: v1.45 -- type: ?<[string]> - -Shuffle the order of test groups with a seed. By default tests are run in the order they are discovered, which is mostly alphabetical. This could lead to an uneven distribution of slow and fast tests. Shuffling the order of tests in a deterministic way can help to distribute the load more evenly. - -The sharding seed is a string that is used to initialize a random number generator. - -Learn more about [parallelism and sharding](../test-parallel.md) with Playwright Test. - -**Usage** - -```js title="playwright.config.ts" -import { defineConfig } from '@playwright/test'; - -export default defineConfig({ - shardingSeed: 'string value' -}); -``` - ## property: TestConfig.testDir * since: v1.10 - type: ?<[string]> diff --git a/docs/src/test-sharding-js.md b/docs/src/test-sharding-js.md index f5fe176a87..068b7172d0 100644 --- a/docs/src/test-sharding-js.md +++ b/docs/src/test-sharding-js.md @@ -22,12 +22,6 @@ Now, if you run these shards in parallel on different computers, your test suite Note that Playwright can only shard tests that can be run in parallel. By default, this means Playwright will shard test files. Learn about other options in the [parallelism guide](./test-parallel.md). -## Randomizing test order in a deterministic way - -By default tests are run in the order they are discovered, which is mostly alphabetical. This could lead to an uneven distribution of slow and fast tests. For example, if the first half of your tests are slower than the rest of your tests and you are using 4 shards it means that shard 1 and 2 will take significantly more time then shard 3 and 4. - -To aid with this problem you can pass `--sharding-seed=string-value` to randomize the order of tests in a deterministic way, which could yield better distribution of slow and fast tests across all shards. - ## Merging reports from multiple shards In the previous example, each test shard has its own test report. If you want to have a combined report showing all the test results from all the shards, you can merge them. diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 025b7e3838..32d00d2751 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -55,7 +55,6 @@ export class FullConfigInternal { cliFailOnFlakyTests?: boolean; testIdMatcher?: Matcher; defineConfigWasUsed = false; - shardingSeed: string | null; constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) { if (configCLIOverrides.projects && userConfig.projects) @@ -93,7 +92,6 @@ export class FullConfigInternal { workers: 0, webServer: null, }; - this.shardingSeed = takeFirst(configCLIOverrides.shardingSeed, userConfig.shardingSeed, null); for (const key in userConfig) { if (key.startsWith('@')) (this.config as any)[key] = (userConfig as any)[key]; diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index f5e3ec0858..c1c7b2da25 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -32,7 +32,6 @@ export type ConfigCLIOverrides = { reporter?: ReporterDescription[]; additionalReporters?: ReporterDescription[]; shard?: { current: number, total: number }; - shardingSeed?: string; timeout?: number; ignoreSnapshots?: boolean; updateSnapshots?: 'all'|'none'|'missing'; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index e85f3c4911..f462b81d43 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -281,7 +281,6 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid retries: options.retries ? parseInt(options.retries, 10) : undefined, reporter: resolveReporterOption(options.reporter), shard: shardPair ? { current: shardPair[0], total: shardPair[1] } : undefined, - shardingSeed: options.shardingSeed ? options.shardingSeed : undefined, timeout: options.timeout ? parseInt(options.timeout, 10) : undefined, ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined, updateSnapshots: options.updateSnapshots ? 'all' as const : undefined, @@ -359,7 +358,6 @@ const testOptions: [string, string][] = [ ['--reporter ', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`], ['--retries ', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`], ['--shard ', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`], - ['--sharding-seed ', `Seed string for randomizing the test order before sharding. Defaults to not randomizing the order.`], ['--timeout ', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${defaultTimeout})`], ['--trace ', `Force tracing mode, can be ${kTraceModes.map(mode => `"${mode}"`).join(', ')}`], ['--ui', `Run tests in interactive UI mode`], diff --git a/packages/playwright/src/runner/loadUtils.ts b/packages/playwright/src/runner/loadUtils.ts index 304ee15dc9..d7d1c7c6c8 100644 --- a/packages/playwright/src/runner/loadUtils.ts +++ b/packages/playwright/src/runner/loadUtils.ts @@ -31,7 +31,6 @@ import { createTestGroups, filterForShard, type TestGroup } from './testGroups'; import { dependenciesForTestFile } from '../transform/compilationCache'; import { sourceMapSupport } from '../utilsBundle'; import type { RawSourceMap } from 'source-map'; -import { shuffleWithSeed } from './shuffle'; export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean, additionalFileMatcher?: Matcher) { const config = testRun.config; @@ -180,9 +179,6 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho for (const projectSuite of rootSuite.suites) testGroups.push(...createTestGroups(projectSuite, config.config.workers)); - if (config.shardingSeed) - shuffleWithSeed(testGroups, config.shardingSeed); - // Shard test groups. const testGroupsInThisShard = filterForShard(config.config.shard, testGroups); const testsInThisShard = new Set(); diff --git a/packages/playwright/src/runner/shuffle.ts b/packages/playwright/src/runner/shuffle.ts deleted file mode 100644 index 32f8498a1d..0000000000 --- a/packages/playwright/src/runner/shuffle.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Shuffles the given array of items using the given seed. - * - * @param items The array of items to shuffle. - * @param seed The seed to use for shuffling. - */ -export function shuffleWithSeed(items: any[], seed: string): void { - const random = rng(cyrb32(seed)); - for (let i = items.length - 1; i > 0; i--) { - const j = Math.floor(random() * (i + 1)); - [items[i], items[j]] = [items[j], items[i]]; - } -} - -/** - * Returns a random number generator seeded with the given seed. - * - * @param seed The seed for the random number generator. - * @returns The random number generator. - */ -function rng(seed: number) { - const m = 2 ** 35 - 31; - const a = 185852; - let s = seed % m; - return function() { - return (s = s * a % m) / m; - }; -} - -/** - * Return a 32-bit hash from a string. - * - * @param str The string to hash. - * @returns The 32-bit hash. - */ -function cyrb32(str: string) { - let h = 0x2323; - for (let i = 0; i < str.length; i++) { - h = h ^ str.charCodeAt(i); - h = Math.imul(h, 2654435761); - } - return h >>> 0; -} diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 2b1f105d7e..c1ecc81fc1 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1426,29 +1426,6 @@ interface TestConfig { total: number; }; - /** - * Shuffle the order of test groups with a seed. By default tests are run in the order they are discovered, which is - * mostly alphabetical. This could lead to an uneven distribution of slow and fast tests. Shuffling the order of tests - * in a deterministic way can help to distribute the load more evenly. - * - * The sharding seed is a string that is used to initialize a random number generator. - * - * Learn more about [parallelism and sharding](https://playwright.dev/docs/test-parallel) with Playwright Test. - * - * **Usage** - * - * ```js - * // playwright.config.ts - * import { defineConfig } from '@playwright/test'; - * - * export default defineConfig({ - * shardingSeed: 'string value' - * }); - * ``` - * - */ - shardingSeed?: string; - /** * **NOTE** Use * [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template) From f115ba85d98affbc05f8ffb30421f81bde0d8e41 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 11 Jun 2024 16:43:44 -0700 Subject: [PATCH 053/144] test: add more edge-case tests for clock (#31256) --- tests/library/clock.spec.ts | 91 +++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index da82f2d960..ba3a17335e 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -1535,6 +1535,97 @@ it.describe('Intl API', () => { }); }); +it('works with concurrent runFor calls', async ({ clock }) => { + clock.setSystemTime(0); + + const log: string[] = []; + for (let t = 500; t > 0; t -= 100) { + clock.setTimeout(() => { + log.push(`${t}: ${clock.Date.now()}`); + clock.setTimeout(() => { + log.push(`${t}+0: ${clock.Date.now()}`); + }, 0); + }, t); + } + + await Promise.all([ + clock.runFor(500), + clock.runFor(600), + ]); + expect(log).toEqual([ + `100: 100`, + `100+0: 101`, + `200: 200`, + `200+0: 201`, + `300: 300`, + `300+0: 301`, + `400: 400`, + `400+0: 401`, + `500: 500`, + `500+0: 501`, + ]); +}); + +it('works with slow setTimeout in busy embedder', async ({ installEx }) => { + const { originals, api, clock } = installEx({ now: 0 }); + await clock.pauseAt(0); + + const log: string[] = []; + api.setTimeout(() => { + log.push(`100: ${api.Date.now()}`); + api.setTimeout(() => { + log.push(`100+10: ${api.Date.now()}`); + }, 10); + }, 100); + api.setTimeout(() => log.push(`200: ${api.Date.now()}`), 200); + api.setTimeout(() => log.push(`300: ${api.Date.now()}`), 300); + api.setTimeout(() => log.push(`400: ${api.Date.now()}`), 400); + + (clock as any)._embedder.setTimeout = (task, timeout) => { + const timerId = originals.setTimeout(task, (timeout || 0) + 200); + return () => originals.clearTimeout(timerId); + }; + + await clock.runFor(500); + expect(log).toEqual([ + `100: 100`, + `100+10: 110`, + `200: 200`, + `300: 300`, + `400: 400`, + ]); +}); + +it('works with slow setTimeout in busy embedder when not paused', async ({ installEx }) => { + const { originals, api, clock } = installEx({ now: 0 }); + clock.setSystemTime(0); + + const log: string[] = []; + api.setTimeout(() => { + log.push(`200: ${api.Date.now()}`); + api.setTimeout(() => { + log.push(`200+10: ${api.Date.now()}`); + }, 10); + }, 200); + api.setTimeout(() => log.push(`400: ${api.Date.now()}`), 400); + api.setTimeout(() => log.push(`600: ${api.Date.now()}`), 600); + api.setTimeout(() => log.push(`800: ${api.Date.now()}`), 800); + + (clock as any)._embedder.setTimeout = (task, timeout) => { + const timerId = originals.setTimeout(task, timeout === undefined ? 300 : timeout); + return () => originals.clearTimeout(timerId); + }; + + await clock.runFor(5000); + expect(log).toEqual([ + `200: 200`, + `200+10: 210`, + `400: 400`, + `600: 600`, + `800: 800`, + ]); +}); + interface Stub { called: boolean; callCount: number; From 6a7bfe63a1d3df5352e3ac5f3808a2f35b898bcd Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 12 Jun 2024 07:06:47 -0700 Subject: [PATCH 054/144] feat(webkit): roll to r2031 (#31272) --- packages/playwright-core/browsers.json | 2 +- packages/playwright-core/src/server/webkit/wkPage.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 5234f96102..a45f862d15 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2029", + "revision": "2031", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 8d441240bc..a495244b6b 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -716,7 +716,10 @@ export class WKPage implements PageDelegate { const angle = viewportSize.width > viewportSize.height ? 90 : 0; // Special handling for macOS 12. const useLegacySetOrientationOverrideMethod = os.platform() === 'darwin' && parseInt(os.release().split('.')[0], 10) <= 21; - promises.push(this._pageProxySession.send(useLegacySetOrientationOverrideMethod ? 'Page.setOrientationOverride' as any : 'Emulation.setOrientationOverride', { angle })); + if (useLegacySetOrientationOverrideMethod) + promises.push(this._session.send('Page.setOrientationOverride' as any, { angle })); + else + promises.push(this._pageProxySession.send('Emulation.setOrientationOverride', { angle })); } await Promise.all(promises); } From f1475fa6442dd645441acbd57dd3b544ff99116a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 12 Jun 2024 08:24:12 -0700 Subject: [PATCH 055/144] chore: trim multiline step titles to first line (#31269) Fixes https://github.com/microsoft/playwright/issues/31266 --- packages/playwright/src/reporters/base.ts | 2 +- tests/playwright-test/reporter-line.spec.ts | 22 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index f418baeea4..e4575a8627 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -414,7 +414,7 @@ function relativeTestPath(config: FullConfig, test: TestCase): string { export function stepSuffix(step: TestStep | undefined) { const stepTitles = step ? step.titlePath() : []; - return stepTitles.map(t => ' β€Ί ' + t).join(''); + return stepTitles.map(t => t.split('\n')[0]).map(t => ' β€Ί ' + t).join(''); } export function formatTestTitle(config: FullConfig, test: TestCase, step?: TestStep, omitLocation: boolean = false): string { diff --git a/tests/playwright-test/reporter-line.spec.ts b/tests/playwright-test/reporter-line.spec.ts index 5e591b191d..14959877b5 100644 --- a/tests/playwright-test/reporter-line.spec.ts +++ b/tests/playwright-test/reporter-line.spec.ts @@ -109,6 +109,28 @@ for (const useIntermediateMergeReport of [false, true] as const) { ].join('\n')); }); + test('should trim multiline step titles to first line', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31266' } + }, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', async ({}) => { + await test.step(\`outer + 1.0\`, async () => { + await test.step(\`inner + 1.1\`, async () => { + expect(1).toBe(1); + }); + }); + }); + `, + }, { reporter: 'line' }); + const text = result.output; + expect(text).toContain('[1/1] a.test.ts:6:26 β€Ί passes β€Ί outer β€Ί inner'); + expect(result.exitCode).toBe(0); + }); + test('should render failed test steps', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` From 751a41f9ee50e24661eeb0dc743c30424ddc9bd2 Mon Sep 17 00:00:00 2001 From: Debbie O'Brien Date: Wed, 12 Jun 2024 18:12:05 +0200 Subject: [PATCH 056/144] docs: update how network and console work (#31278) --- docs/src/trace-viewer.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/src/trace-viewer.md b/docs/src/trace-viewer.md index 556b22729a..8a59c3e35f 100644 --- a/docs/src/trace-viewer.md +++ b/docs/src/trace-viewer.md @@ -31,7 +31,7 @@ In the Actions tab you can see what locator was used for every action and how lo ### Screenshots -When tracing with the [`option: screenshots`] option turned on, each trace records a screencast and renders it as a film strip. You can hover over the film strip to see a magnified image of for each action and state which helps you easily find the action you want to inspect. +When tracing with the [`option: screenshots`] option turned on (default), each trace records a screencast and renders it as a film strip. You can hover over the film strip to see a magnified image of for each action and state which helps you easily find the action you want to inspect. Double click on an action to see the time range for that action. You can use the slider in the timeline to increase the actions selected and these will be shown in the Actions tab and all console logs and network logs will be filtered to only show the logs for the actions selected. @@ -58,7 +58,7 @@ Notice how it highlights both, the DOM Node as well as the exact click position. ### Source -As you hover over each action of your test the line of code for that action is highlighted in the source panel. +When you click on an action in the sidebar, the line of code for that action is highlighted in the source panel. ![showing source code tab in trace viewer](https://github.com/microsoft/playwright/assets/13063165/daa8845d-c250-4923-aa7a-5d040da9adc5) @@ -86,6 +86,10 @@ See console logs from the browser as well as from your test. Different icons are ![showing log of tests in trace viewer](https://github.com/microsoft/playwright/assets/13063165/4107c08d-1eaf-421c-bdd4-9dd2aa641d4a) +Double click on an action from your test in the actions sidebar. This will filter the console to only show the logs that were made during that action. Click the *Show all* button to see all console logs again. + +Use the timeline to filter actions, by clicking a start point and dragging to an ending point. The console tab will also be filtered to only show the logs that were made during the actions selected. + ### Network @@ -93,6 +97,10 @@ The Network tab shows you all the network requests that were made during your te ![network requests tab in trace viewer](https://github.com/microsoft/playwright/assets/13063165/0a3d1671-8ccd-4f7a-a844-35f5eb37f236) +Double click on an action from your test in the actions sidebar. This will filter the network requests to only show the requests that were made during that action. Click the *Show all* button to see all network requests again. + +Use the timeline to filter actions, by clicking a start point and dragging to an ending point. The network tab will also be filtered to only show the network requests that were made during the actions selected. + ### Metadata Next to the Actions tab you will find the Metadata tab which will show you more information on your test such as the Browser, viewport size, test duration and more. From dcf4e4e0542bd94d33d5ff8ac75d2101c220206f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 12 Jun 2024 22:20:18 +0200 Subject: [PATCH 057/144] feat: allow folder uploads (#31165) --- docs/src/api/class-elementhandle.md | 1 + docs/src/api/class-locator.md | 1 + docs/src/api/class-page.md | 1 + .../src/client/elementHandle.ts | 55 +++++++++++--- .../playwright-core/src/protocol/validator.ts | 18 +++-- .../dispatchers/browserContextDispatcher.ts | 15 +++- .../dispatchers/writableStreamDispatcher.ts | 18 +++-- packages/playwright-core/src/server/dom.ts | 26 +++++-- .../src/server/fileUploadUtils.ts | 11 ++- .../src/server/webkit/wkPage.ts | 12 +-- packages/playwright-core/types/types.d.ts | 9 ++- packages/protocol/src/channels.ts | 28 +++++-- packages/protocol/src/protocol.yml | 21 +++++- tests/assets/input/folderupload.html | 12 +++ tests/electron/playwright.config.ts | 2 + tests/page/page-set-input-files.spec.ts | 73 ++++++++++++++++++- tests/webview2/playwright.config.ts | 1 + 17 files changed, 245 insertions(+), 59 deletions(-) create mode 100644 tests/assets/input/folderupload.html diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index dbf99eb3a2..24b8bb2b6f 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -953,6 +953,7 @@ When all steps combined have not finished during the specified [`option: timeout Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the current working directory. For empty array, clears the selected files. +For inputs with a `[webkitdirectory]` attribute, only a single directory path is supported. This method expects [ElementHandle] to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). However, if the element is inside the `