diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index c623e0f54f527..c0cceaca9e11c 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -612,10 +612,11 @@ function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: Buil Date: (globalObject as any).Date, performance: globalObject.performance, Intl: (globalObject as any).Intl, + AbortSignal: (globalObject as any).AbortSignal, }; const bound = { ...raw }; for (const key of Object.keys(bound) as (keyof Builtins)[]) { - if (key !== 'Date' && typeof bound[key] === 'function') + if (key !== 'Date' && key !== 'AbortSignal' && typeof bound[key] === 'function') bound[key] = (bound[key] as any).bind(globalObject); } return { raw, bound }; @@ -632,16 +633,17 @@ function getScheduleHandler(type: TimerType) { } function createApi(clock: ClockController, originals: Builtins): Builtins { + const setTimeout = (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => { + const delay = timeout ? +timeout : timeout; + return clock.addTimer({ + type: TimerType.Timeout, + func, + args, + delay + }); + }; return { - setTimeout: (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => { - const delay = timeout ? +timeout : timeout; - return clock.addTimer({ - type: TimerType.Timeout, - func, - args, - delay - }); - }, + setTimeout, clearTimeout: (timerId: number | undefined): void => { if (timerId) clock.clearTimer(timerId, TimerType.Timeout); @@ -688,6 +690,7 @@ function createApi(clock: ClockController, originals: Builtins): Builtins { Intl: originals.Intl ? createIntl(clock, originals.Intl) : (undefined as unknown as Builtins['Intl']), Date: createDate(clock, originals.Date), performance: originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']), + AbortSignal: originals.AbortSignal ? fakeAbortSignal(originals.AbortSignal, setTimeout) : originals.AbortSignal, }; } @@ -715,6 +718,15 @@ function fakePerformance(clock: ClockController, performance: Builtins['performa return result; } +function fakeAbortSignal(NativeAbortSignal: Builtins['AbortSignal'], fakeSetTimeout: Builtins['setTimeout']): Builtins['AbortSignal'] { + (NativeAbortSignal as any).timeout = function(ms: number): AbortSignal { + const controller = new AbortController(); + fakeSetTimeout(() => controller.abort(), ms); + return controller.signal; + }; + return NativeAbortSignal; +} + export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: Builtins, originals: Builtins } { const originals = platformOriginals(globalObject); const embedder: Embedder = { @@ -760,6 +772,8 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install return this[kEventTimeStamp]; } }); + } else if (method === 'AbortSignal') { + (globalObject as any).AbortSignal = api[method]!; } else { (globalObject as any)[method] = (...args: any[]) => { return (api[method] as any).apply(api, args); diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index e61ad96d45b5f..5c54d9ed98814 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -29,6 +29,7 @@ export type Builtins = { performance: Window['performance'], Intl: typeof window['Intl'], Date: typeof window['Date'], + AbortSignal: typeof window['AbortSignal'], }; export class UtilityScript { @@ -55,6 +56,7 @@ export class UtilityScript { performance: global.performance, Intl: global.Intl, Date: global.Date, + AbortSignal: global.AbortSignal, } satisfies Builtins; } if (this.isUnderTest) diff --git a/tests/library/page-clock.spec.ts b/tests/library/page-clock.spec.ts index 277ec4672696a..092854495f162 100644 --- a/tests/library/page-clock.spec.ts +++ b/tests/library/page-clock.spec.ts @@ -565,3 +565,98 @@ it('correctly increments Date.now()/performance.now() during blocking execution' await page.goto(server.PREFIX + '/repro.html'); await waitForDone; }); + +it.describe('AbortSignal.timeout', () => { + it('should abort signal after timeout', async ({ page }) => { + await page.clock.install({ time: 0 }); + + const signalHandle = await page.evaluateHandle(() => { + return AbortSignal.timeout(5000); + }); + + // Check initial state + expect(await signalHandle.evaluate(s => s.aborted)).toBe(false); + + // Fast forward past the timeout + await page.clock.runFor(6000); + + // Check that signal is now aborted + expect(await signalHandle.evaluate(s => s.aborted)).toBe(true); + }); + + it('should fire addEventListener', async ({ page }) => { + await page.clock.install({ time: 0 }); + + const result = await page.evaluate(() => { + return new Promise(resolve => { + const signal = AbortSignal.timeout(5000); + let didAbort = false; + signal.addEventListener('abort', () => { + didAbort = true; + resolve(didAbort); + }); + // If the signal doesn't abort, resolve after a longer time + setTimeout(() => resolve(didAbort), 10000); + }); + }); + + // Run for the timeout duration + await page.clock.runFor(6000); + + expect(result).toBe(true); + }); + + it('should fire onabort handler', async ({ page }) => { + await page.clock.install({ time: 0 }); + + const result = await page.evaluate(() => { + return new Promise(resolve => { + const signal = AbortSignal.timeout(5000); + let didAbort = false; + signal.onabort = () => { + didAbort = true; + resolve(didAbort); + }; + // If the signal doesn't abort, resolve after a longer time + setTimeout(() => resolve(didAbort), 10000); + }); + }); + + // Run for the timeout duration + await page.clock.runFor(6000); + + expect(result).toBe(true); + }); + + it('should work with fastForward', async ({ page }) => { + await page.clock.install({ time: 0 }); + + const signalHandle = await page.evaluateHandle(() => { + return AbortSignal.timeout(5000); + }); + + expect(await signalHandle.evaluate(s => s.aborted)).toBe(false); + + await page.clock.fastForward(6000); + + expect(await signalHandle.evaluate(s => s.aborted)).toBe(true); + }); + + it('should work with multiple signals', async ({ page }) => { + await page.clock.install({ time: 0 }); + + const result = await page.evaluate(() => { + const results: number[] = []; + AbortSignal.timeout(1000).addEventListener('abort', () => results.push(1)); + AbortSignal.timeout(2000).addEventListener('abort', () => results.push(2)); + AbortSignal.timeout(3000).addEventListener('abort', () => results.push(3)); + return new Promise(resolve => { + setTimeout(() => resolve(results), 5000); + }); + }); + + await page.clock.runFor(5000); + + expect(result).toEqual([1, 2, 3]); + }); +});