From 7d133a4383be930700f8c126b6a379369bafbf40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:45:26 +0000 Subject: [PATCH 1/6] Initial plan From baa751fb2248462d4ce1a8f2134a12430990cef3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:51:52 +0000 Subject: [PATCH 2/6] Add Event to Builtins and move timeStamp patching to createApi Co-authored-by: Skn0tt <14912729+Skn0tt@users.noreply.github.com> --- packages/injected/src/clock.ts | 44 +++++++++++++++++++------- packages/injected/src/utilityScript.ts | 2 ++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index c623e0f54f527..60f8662d08ca9 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, + Event: (globalObject as any).Event, }; const bound = { ...raw }; for (const key of Object.keys(bound) as (keyof Builtins)[]) { - if (key !== 'Date' && typeof bound[key] === 'function') + if (key !== 'Date' && key !== 'Event' && typeof bound[key] === 'function') bound[key] = (bound[key] as any).bind(globalObject); } return { raw, bound }; @@ -632,6 +633,7 @@ function getScheduleHandler(type: TimerType) { } function createApi(clock: ClockController, originals: Builtins): Builtins { + const performance = originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']); return { setTimeout: (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => { const delay = timeout ? +timeout : timeout; @@ -687,7 +689,8 @@ 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']), + performance, + Event: originals.Event && performance ? fakeEvent(clock, originals.Event, performance) : originals.Event, }; } @@ -715,6 +718,22 @@ function fakePerformance(clock: ClockController, performance: Builtins['performa return result; } +function fakeEvent(clock: ClockController, NativeEvent: Builtins['Event'], fakePerformance: Builtins['performance']): Builtins['Event'] { + const kEventTimeStamp = Symbol('playwrightEventTimeStamp'); + const originalDescriptor = Object.getOwnPropertyDescriptor(NativeEvent.prototype, 'timeStamp'); + Object.defineProperty(NativeEvent.prototype, 'timeStamp', { + get() { + if (!this[kEventTimeStamp]) + this[kEventTimeStamp] = fakePerformance?.now(); + return this[kEventTimeStamp]; + }, + configurable: true, + }); + // Store the original descriptor so we can restore it later + (NativeEvent as any).__originalTimeStampDescriptor = originalDescriptor; + return NativeEvent; +} + export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: Builtins, originals: Builtins } { const originals = platformOriginals(globalObject); const embedder: Embedder = { @@ -750,16 +769,8 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install (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]!; - const kEventTimeStamp = Symbol('playwrightEventTimeStamp'); - Object.defineProperty(Event.prototype, 'timeStamp', { - get() { - if (!this[kEventTimeStamp]) - this[kEventTimeStamp] = api.performance?.now(); - return this[kEventTimeStamp]; - } - }); + } else if (method === 'performance' || method === 'Event') { + (globalObject as any)[method] = api[method]!; } else { (globalObject as any)[method] = (...args: any[]) => { return (api[method] as any).apply(api, args); @@ -768,6 +779,15 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install clock.disposables.push(() => { (globalObject as any)[method] = originals[method]; }); + // Special handling for Event: restore the original timeStamp descriptor + if (method === 'Event' && (originals.Event as any).__originalTimeStampDescriptor) { + clock.disposables.push(() => { + const descriptor = (originals.Event as any).__originalTimeStampDescriptor; + if (descriptor) { + Object.defineProperty(originals.Event.prototype, 'timeStamp', descriptor); + } + }); + } } return { clock, api, originals }; diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index e61ad96d45b5f..1f2ab18080297 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'], + Event: typeof window['Event'], }; export class UtilityScript { @@ -55,6 +56,7 @@ export class UtilityScript { performance: global.performance, Intl: global.Intl, Date: global.Date, + Event: global.Event, } satisfies Builtins; } if (this.isUnderTest) From 711b24580da9320d550c5f4233d45d6680b223f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:58:30 +0000 Subject: [PATCH 3/6] Add AbortSignal.timeout support to fake clock Co-authored-by: Skn0tt <14912729+Skn0tt@users.noreply.github.com> --- packages/injected/src/clock.ts | 46 +++++++++++---- packages/injected/src/utilityScript.ts | 2 + tests/library/page-clock.spec.ts | 81 ++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 60f8662d08ca9..5cf20b18e7a94 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -613,10 +613,11 @@ function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: Buil performance: globalObject.performance, Intl: (globalObject as any).Intl, Event: (globalObject as any).Event, + AbortSignal: (globalObject as any).AbortSignal, }; const bound = { ...raw }; for (const key of Object.keys(bound) as (keyof Builtins)[]) { - if (key !== 'Date' && key !== 'Event' && typeof bound[key] === 'function') + if (key !== 'Date' && key !== 'Event' && key !== 'AbortSignal' && typeof bound[key] === 'function') bound[key] = (bound[key] as any).bind(globalObject); } return { raw, bound }; @@ -634,16 +635,17 @@ function getScheduleHandler(type: TimerType) { function createApi(clock: ClockController, originals: Builtins): Builtins { const performance = originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']); + 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); @@ -691,6 +693,7 @@ function createApi(clock: ClockController, originals: Builtins): Builtins { Date: createDate(clock, originals.Date), performance, Event: originals.Event && performance ? fakeEvent(clock, originals.Event, performance) : originals.Event, + AbortSignal: originals.AbortSignal ? fakeAbortSignal(clock, originals.AbortSignal, setTimeout) : originals.AbortSignal, }; } @@ -734,6 +737,18 @@ function fakeEvent(clock: ClockController, NativeEvent: Builtins['Event'], fakeP return NativeEvent; } +function fakeAbortSignal(clock: ClockController, NativeAbortSignal: Builtins['AbortSignal'], fakeSetTimeout: Builtins['setTimeout']): Builtins['AbortSignal'] { + const originalTimeout = NativeAbortSignal.timeout; + (NativeAbortSignal as any).timeout = function(ms: number): AbortSignal { + const controller = new AbortController(); + fakeSetTimeout(() => controller.abort(), ms); + return controller.signal; + }; + // Store the original timeout function so we can restore it later + (NativeAbortSignal as any).__originalTimeout = originalTimeout; + return NativeAbortSignal; +} + export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: Builtins, originals: Builtins } { const originals = platformOriginals(globalObject); const embedder: Embedder = { @@ -769,7 +784,7 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install (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' || method === 'Event') { + } else if (method === 'performance' || method === 'Event' || method === 'AbortSignal') { (globalObject as any)[method] = api[method]!; } else { (globalObject as any)[method] = (...args: any[]) => { @@ -788,6 +803,15 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install } }); } + // Special handling for AbortSignal: restore the original timeout method + if (method === 'AbortSignal' && (originals.AbortSignal as any).__originalTimeout) { + clock.disposables.push(() => { + const originalTimeout = (originals.AbortSignal as any).__originalTimeout; + if (originalTimeout !== undefined) { + (originals.AbortSignal as any).timeout = originalTimeout; + } + }); + } } return { clock, api, originals }; diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index 1f2ab18080297..b5a50e7928711 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -30,6 +30,7 @@ export type Builtins = { Intl: typeof window['Intl'], Date: typeof window['Date'], Event: typeof window['Event'], + AbortSignal: typeof window['AbortSignal'], }; export class UtilityScript { @@ -57,6 +58,7 @@ export class UtilityScript { Intl: global.Intl, Date: global.Date, Event: global.Event, + 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..3ea5a58acd2db 100644 --- a/tests/library/page-clock.spec.ts +++ b/tests/library/page-clock.spec.ts @@ -565,3 +565,84 @@ 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 work with fake timers', async ({ page }) => { + await page.clock.install({ time: 0 }); + + const result = await page.evaluate(async () => { + const signal = AbortSignal.timeout(5000); + return signal.aborted; + }); + expect(result).toBe(false); + + await page.clock.fastForward(4000); + + const result2 = await page.evaluate(() => { + const signal = AbortSignal.timeout(5000); + return signal.aborted; + }); + expect(result2).toBe(false); + + await page.clock.fastForward(2000); + + const result3 = await page.evaluate(async () => { + const signal = AbortSignal.timeout(1); + // Need to advance time for the timer to fire + return signal.aborted; + }); + // The signal hasn't been aborted yet because we need to run timers + expect(result3).toBe(false); + + // Now run the timer + await page.clock.runFor(10); + + const result4 = await page.evaluate(() => { + const signal = AbortSignal.timeout(1); + return signal.aborted; + }); + expect(result4).toBe(false); + }); + + it('should abort signal after timeout with runFor', async ({ page }) => { + await page.clock.install({ time: 0 }); + + const setup = await page.evaluate(async () => { + (window as any).abortedSignals = []; + const signal = AbortSignal.timeout(5000); + signal.addEventListener('abort', () => { + (window as any).abortedSignals.push('signal1'); + }); + return { aborted: signal.aborted }; + }); + expect(setup.aborted).toBe(false); + + await page.clock.runFor(6000); + + const result = await page.evaluate(() => { + return (window as any).abortedSignals; + }); + expect(result).toEqual(['signal1']); + }); + + it('should work with fast forward', async ({ page }) => { + await page.clock.install({ time: 0 }); + + const setup = await page.evaluate(async () => { + (window as any).abortedSignals = []; + const signal = AbortSignal.timeout(5000); + signal.addEventListener('abort', () => { + (window as any).abortedSignals.push('signal1'); + }); + return { aborted: signal.aborted }; + }); + expect(setup.aborted).toBe(false); + + await page.clock.fastForward(6000); + + const result = await page.evaluate(() => { + return (window as any).abortedSignals; + }); + expect(result).toEqual(['signal1']); + }); +}); From b6527da12cd121eb4bcccb1b16fd65abfbd2878b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:02:26 +0000 Subject: [PATCH 4/6] Fix code review feedback: remove trailing whitespace and prevent double-patching Co-authored-by: Skn0tt <14912729+Skn0tt@users.noreply.github.com> --- packages/injected/src/clock.ts | 44 ++++++++++++++++++-------------- tests/library/page-clock.spec.ts | 8 +++--- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 5cf20b18e7a94..8feac1d4b11af 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -723,29 +723,35 @@ function fakePerformance(clock: ClockController, performance: Builtins['performa function fakeEvent(clock: ClockController, NativeEvent: Builtins['Event'], fakePerformance: Builtins['performance']): Builtins['Event'] { const kEventTimeStamp = Symbol('playwrightEventTimeStamp'); - const originalDescriptor = Object.getOwnPropertyDescriptor(NativeEvent.prototype, 'timeStamp'); - Object.defineProperty(NativeEvent.prototype, 'timeStamp', { - get() { - if (!this[kEventTimeStamp]) - this[kEventTimeStamp] = fakePerformance?.now(); - return this[kEventTimeStamp]; - }, - configurable: true, - }); - // Store the original descriptor so we can restore it later - (NativeEvent as any).__originalTimeStampDescriptor = originalDescriptor; + // Only patch if not already patched + if (!(NativeEvent as any).__originalTimeStampDescriptor) { + const originalDescriptor = Object.getOwnPropertyDescriptor(NativeEvent.prototype, 'timeStamp'); + Object.defineProperty(NativeEvent.prototype, 'timeStamp', { + get() { + if (!this[kEventTimeStamp]) + this[kEventTimeStamp] = fakePerformance?.now(); + return this[kEventTimeStamp]; + }, + configurable: true, + }); + // Store the original descriptor so we can restore it later + (NativeEvent as any).__originalTimeStampDescriptor = originalDescriptor; + } return NativeEvent; } function fakeAbortSignal(clock: ClockController, NativeAbortSignal: Builtins['AbortSignal'], fakeSetTimeout: Builtins['setTimeout']): Builtins['AbortSignal'] { - const originalTimeout = NativeAbortSignal.timeout; - (NativeAbortSignal as any).timeout = function(ms: number): AbortSignal { - const controller = new AbortController(); - fakeSetTimeout(() => controller.abort(), ms); - return controller.signal; - }; - // Store the original timeout function so we can restore it later - (NativeAbortSignal as any).__originalTimeout = originalTimeout; + // Only patch if not already patched + if (!(NativeAbortSignal as any).__originalTimeout) { + const originalTimeout = NativeAbortSignal.timeout; + (NativeAbortSignal as any).timeout = function(ms: number): AbortSignal { + const controller = new AbortController(); + fakeSetTimeout(() => controller.abort(), ms); + return controller.signal; + }; + // Store the original timeout function so we can restore it later + (NativeAbortSignal as any).__originalTimeout = originalTimeout; + } return NativeAbortSignal; } diff --git a/tests/library/page-clock.spec.ts b/tests/library/page-clock.spec.ts index 3ea5a58acd2db..545715e0f512c 100644 --- a/tests/library/page-clock.spec.ts +++ b/tests/library/page-clock.spec.ts @@ -577,7 +577,7 @@ it.describe('AbortSignal.timeout', () => { expect(result).toBe(false); await page.clock.fastForward(4000); - + const result2 = await page.evaluate(() => { const signal = AbortSignal.timeout(5000); return signal.aborted; @@ -585,7 +585,7 @@ it.describe('AbortSignal.timeout', () => { expect(result2).toBe(false); await page.clock.fastForward(2000); - + const result3 = await page.evaluate(async () => { const signal = AbortSignal.timeout(1); // Need to advance time for the timer to fire @@ -593,10 +593,10 @@ it.describe('AbortSignal.timeout', () => { }); // The signal hasn't been aborted yet because we need to run timers expect(result3).toBe(false); - + // Now run the timer await page.clock.runFor(10); - + const result4 = await page.evaluate(() => { const signal = AbortSignal.timeout(1); return signal.aborted; From 6f1bff47374c30bba001d37826d227bcfd8e8548 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:05:05 +0000 Subject: [PATCH 5/6] Address additional code review feedback Co-authored-by: Skn0tt <14912729+Skn0tt@users.noreply.github.com> --- packages/injected/src/clock.ts | 6 ++---- tests/library/page-clock.spec.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 8feac1d4b11af..939a83b443b46 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -729,7 +729,7 @@ function fakeEvent(clock: ClockController, NativeEvent: Builtins['Event'], fakeP Object.defineProperty(NativeEvent.prototype, 'timeStamp', { get() { if (!this[kEventTimeStamp]) - this[kEventTimeStamp] = fakePerformance?.now(); + this[kEventTimeStamp] = fakePerformance.now(); return this[kEventTimeStamp]; }, configurable: true, @@ -813,9 +813,7 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install if (method === 'AbortSignal' && (originals.AbortSignal as any).__originalTimeout) { clock.disposables.push(() => { const originalTimeout = (originals.AbortSignal as any).__originalTimeout; - if (originalTimeout !== undefined) { - (originals.AbortSignal as any).timeout = originalTimeout; - } + (originals.AbortSignal as any).timeout = originalTimeout; }); } } diff --git a/tests/library/page-clock.spec.ts b/tests/library/page-clock.spec.ts index 545715e0f512c..37db65b427a7b 100644 --- a/tests/library/page-clock.spec.ts +++ b/tests/library/page-clock.spec.ts @@ -586,7 +586,7 @@ it.describe('AbortSignal.timeout', () => { await page.clock.fastForward(2000); - const result3 = await page.evaluate(async () => { + const result3 = await page.evaluate(() => { const signal = AbortSignal.timeout(1); // Need to advance time for the timer to fire return signal.aborted; @@ -606,8 +606,8 @@ it.describe('AbortSignal.timeout', () => { it('should abort signal after timeout with runFor', async ({ page }) => { await page.clock.install({ time: 0 }); - - const setup = await page.evaluate(async () => { + + const setup = await page.evaluate(() => { (window as any).abortedSignals = []; const signal = AbortSignal.timeout(5000); signal.addEventListener('abort', () => { @@ -618,7 +618,7 @@ it.describe('AbortSignal.timeout', () => { expect(setup.aborted).toBe(false); await page.clock.runFor(6000); - + const result = await page.evaluate(() => { return (window as any).abortedSignals; }); @@ -627,8 +627,8 @@ it.describe('AbortSignal.timeout', () => { it('should work with fast forward', async ({ page }) => { await page.clock.install({ time: 0 }); - - const setup = await page.evaluate(async () => { + + const setup = await page.evaluate(() => { (window as any).abortedSignals = []; const signal = AbortSignal.timeout(5000); signal.addEventListener('abort', () => { From 64b73490e3110c6d78379b7039176213f8517a1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:19:02 +0000 Subject: [PATCH 6/6] Simplify implementation and fix tests per code review Co-authored-by: Skn0tt <14912729+Skn0tt@users.noreply.github.com> --- packages/injected/src/clock.ts | 76 +++++------------ packages/injected/src/utilityScript.ts | 2 - tests/library/page-clock.spec.ts | 110 ++++++++++++++----------- 3 files changed, 83 insertions(+), 105 deletions(-) diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 939a83b443b46..c0cceaca9e11c 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -612,12 +612,11 @@ function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: Buil Date: (globalObject as any).Date, performance: globalObject.performance, Intl: (globalObject as any).Intl, - Event: (globalObject as any).Event, AbortSignal: (globalObject as any).AbortSignal, }; const bound = { ...raw }; for (const key of Object.keys(bound) as (keyof Builtins)[]) { - if (key !== 'Date' && key !== 'Event' && key !== 'AbortSignal' && typeof bound[key] === 'function') + if (key !== 'Date' && key !== 'AbortSignal' && typeof bound[key] === 'function') bound[key] = (bound[key] as any).bind(globalObject); } return { raw, bound }; @@ -634,7 +633,6 @@ function getScheduleHandler(type: TimerType) { } function createApi(clock: ClockController, originals: Builtins): Builtins { - const performance = originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']); const setTimeout = (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => { const delay = timeout ? +timeout : timeout; return clock.addTimer({ @@ -691,9 +689,8 @@ 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, - Event: originals.Event && performance ? fakeEvent(clock, originals.Event, performance) : originals.Event, - AbortSignal: originals.AbortSignal ? fakeAbortSignal(clock, originals.AbortSignal, setTimeout) : originals.AbortSignal, + performance: originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']), + AbortSignal: originals.AbortSignal ? fakeAbortSignal(originals.AbortSignal, setTimeout) : originals.AbortSignal, }; } @@ -721,37 +718,12 @@ function fakePerformance(clock: ClockController, performance: Builtins['performa return result; } -function fakeEvent(clock: ClockController, NativeEvent: Builtins['Event'], fakePerformance: Builtins['performance']): Builtins['Event'] { - const kEventTimeStamp = Symbol('playwrightEventTimeStamp'); - // Only patch if not already patched - if (!(NativeEvent as any).__originalTimeStampDescriptor) { - const originalDescriptor = Object.getOwnPropertyDescriptor(NativeEvent.prototype, 'timeStamp'); - Object.defineProperty(NativeEvent.prototype, 'timeStamp', { - get() { - if (!this[kEventTimeStamp]) - this[kEventTimeStamp] = fakePerformance.now(); - return this[kEventTimeStamp]; - }, - configurable: true, - }); - // Store the original descriptor so we can restore it later - (NativeEvent as any).__originalTimeStampDescriptor = originalDescriptor; - } - return NativeEvent; -} - -function fakeAbortSignal(clock: ClockController, NativeAbortSignal: Builtins['AbortSignal'], fakeSetTimeout: Builtins['setTimeout']): Builtins['AbortSignal'] { - // Only patch if not already patched - if (!(NativeAbortSignal as any).__originalTimeout) { - const originalTimeout = NativeAbortSignal.timeout; - (NativeAbortSignal as any).timeout = function(ms: number): AbortSignal { - const controller = new AbortController(); - fakeSetTimeout(() => controller.abort(), ms); - return controller.signal; - }; - // Store the original timeout function so we can restore it later - (NativeAbortSignal as any).__originalTimeout = originalTimeout; - } +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; } @@ -790,8 +762,18 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install (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' || method === 'Event' || method === 'AbortSignal') { - (globalObject as any)[method] = api[method]!; + } else if (method === 'performance') { + (globalObject as any).performance = api[method]!; + const kEventTimeStamp = Symbol('playwrightEventTimeStamp'); + Object.defineProperty(Event.prototype, 'timeStamp', { + get() { + if (!this[kEventTimeStamp]) + this[kEventTimeStamp] = api.performance?.now(); + 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); @@ -800,22 +782,6 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install clock.disposables.push(() => { (globalObject as any)[method] = originals[method]; }); - // Special handling for Event: restore the original timeStamp descriptor - if (method === 'Event' && (originals.Event as any).__originalTimeStampDescriptor) { - clock.disposables.push(() => { - const descriptor = (originals.Event as any).__originalTimeStampDescriptor; - if (descriptor) { - Object.defineProperty(originals.Event.prototype, 'timeStamp', descriptor); - } - }); - } - // Special handling for AbortSignal: restore the original timeout method - if (method === 'AbortSignal' && (originals.AbortSignal as any).__originalTimeout) { - clock.disposables.push(() => { - const originalTimeout = (originals.AbortSignal as any).__originalTimeout; - (originals.AbortSignal as any).timeout = originalTimeout; - }); - } } return { clock, api, originals }; diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index b5a50e7928711..5c54d9ed98814 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -29,7 +29,6 @@ export type Builtins = { performance: Window['performance'], Intl: typeof window['Intl'], Date: typeof window['Date'], - Event: typeof window['Event'], AbortSignal: typeof window['AbortSignal'], }; @@ -57,7 +56,6 @@ export class UtilityScript { performance: global.performance, Intl: global.Intl, Date: global.Date, - Event: global.Event, AbortSignal: global.AbortSignal, } satisfies Builtins; } diff --git a/tests/library/page-clock.spec.ts b/tests/library/page-clock.spec.ts index 37db65b427a7b..092854495f162 100644 --- a/tests/library/page-clock.spec.ts +++ b/tests/library/page-clock.spec.ts @@ -567,82 +567,96 @@ it('correctly increments Date.now()/performance.now() during blocking execution' }); it.describe('AbortSignal.timeout', () => { - it('should work with fake timers', async ({ page }) => { + it('should abort signal after timeout', async ({ page }) => { await page.clock.install({ time: 0 }); - const result = await page.evaluate(async () => { - const signal = AbortSignal.timeout(5000); - return signal.aborted; + const signalHandle = await page.evaluateHandle(() => { + return AbortSignal.timeout(5000); }); - expect(result).toBe(false); - await page.clock.fastForward(4000); + // Check initial state + expect(await signalHandle.evaluate(s => s.aborted)).toBe(false); - const result2 = await page.evaluate(() => { - const signal = AbortSignal.timeout(5000); - return signal.aborted; - }); - expect(result2).toBe(false); + // Fast forward past the timeout + await page.clock.runFor(6000); - await page.clock.fastForward(2000); + // 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 result3 = await page.evaluate(() => { - const signal = AbortSignal.timeout(1); - // Need to advance time for the timer to fire - return signal.aborted; + 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); + }); }); - // The signal hasn't been aborted yet because we need to run timers - expect(result3).toBe(false); - // Now run the timer - await page.clock.runFor(10); + // Run for the timeout duration + await page.clock.runFor(6000); - const result4 = await page.evaluate(() => { - const signal = AbortSignal.timeout(1); - return signal.aborted; - }); - expect(result4).toBe(false); + expect(result).toBe(true); }); - it('should abort signal after timeout with runFor', async ({ page }) => { + it('should fire onabort handler', async ({ page }) => { await page.clock.install({ time: 0 }); - const setup = await page.evaluate(() => { - (window as any).abortedSignals = []; - const signal = AbortSignal.timeout(5000); - signal.addEventListener('abort', () => { - (window as any).abortedSignals.push('signal1'); + 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); }); - return { aborted: signal.aborted }; }); - expect(setup.aborted).toBe(false); + // Run for the timeout duration await page.clock.runFor(6000); - const result = await page.evaluate(() => { - return (window as any).abortedSignals; - }); - expect(result).toEqual(['signal1']); + expect(result).toBe(true); }); - it('should work with fast forward', async ({ page }) => { + it('should work with fastForward', async ({ page }) => { await page.clock.install({ time: 0 }); - const setup = await page.evaluate(() => { - (window as any).abortedSignals = []; - const signal = AbortSignal.timeout(5000); - signal.addEventListener('abort', () => { - (window as any).abortedSignals.push('signal1'); - }); - return { aborted: signal.aborted }; + const signalHandle = await page.evaluateHandle(() => { + return AbortSignal.timeout(5000); }); - expect(setup.aborted).toBe(false); + + 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(() => { - return (window as any).abortedSignals; + 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); + }); }); - expect(result).toEqual(['signal1']); + + await page.clock.runFor(5000); + + expect(result).toEqual([1, 2, 3]); }); });