chore: add clock.next() (#31097)

This commit is contained in:
Pavel Feldman 2024-05-31 08:09:24 -07:00 committed by GitHub
parent a617dd0df8
commit 76e977a934
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 160 additions and 9 deletions

View file

@ -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.

View file

@ -37,6 +37,11 @@ export class Clock implements api.Clock {
});
}
async next(): Promise<number> {
const result = await this._browserContext._channel.clockNext();
return result.fakeTime;
}
async runAll(): Promise<number> {
const result = await this._browserContext._channel.clockRunAll();
return result.fakeTime;

View file

@ -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,

View file

@ -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<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.next()`);
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.nextAsync()`);
}
async runAll(): Promise<number> {

View file

@ -320,6 +320,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await this._context.clock.jump(params.timeString || params.timeNumber || 0);
}
async clockNext(params: channels.BrowserContextClockNextParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockNextResult> {
return { fakeTime: await this._context.clock.next() };
}
async clockRunAll(params: channels.BrowserContextClockRunAllParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllResult> {
return { fakeTime: await this._context.clock.runAll() };
}

View file

@ -1654,7 +1654,6 @@ function withGlobal(_global) {
* @returns {Clock}
*/
function install(config) {
console.log('INSTALL', config);
if (
arguments.length > 1 ||
config instanceof Date ||

View file

@ -17289,6 +17289,11 @@ export interface Clock {
*/
jump(time: number|string): Promise<void>;
/**
* Advances the clock to the the moment of the first scheduled timer, firing it.
*/
next(): Promise<number>;
/**
* 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

View file

@ -1462,6 +1462,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallResult>;
clockJump(params: BrowserContextClockJumpParams, metadata?: CallMetadata): Promise<BrowserContextClockJumpResult>;
clockNext(params?: BrowserContextClockNextParams, metadata?: CallMetadata): Promise<BrowserContextClockNextResult>;
clockRunAll(params?: BrowserContextClockRunAllParams, metadata?: CallMetadata): Promise<BrowserContextClockRunAllResult>;
clockRunToLast(params?: BrowserContextClockRunToLastParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToLastResult>;
clockTick(params: BrowserContextClockTickParams, metadata?: CallMetadata): Promise<BrowserContextClockTickResult>;
@ -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 = {

View file

@ -1219,6 +1219,10 @@ BrowserContext:
timeNumber: number?
timeString: string?
clockNext:
returns:
fakeTime: number
clockRunAll:
returns:
fakeTime: number

View file

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