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(); + }); +});