Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/api/class-clock.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Time may be the number of milliseconds to advance the clock by or a human-readab
Install fake implementations for the following time-related functions:

* `Date`
* `Temporal.Now`
* `setTimeout`
* `clearTimeout`
* `setInterval`
Expand Down
1 change: 1 addition & 0 deletions docs/src/clock.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The recommended approach is to use `setFixedTime` to set the time to a specific
:::note
[`property: Page.clock`] overrides native global classes and functions related to time allowing them to be manually controlled:
- `Date`
- `Temporal.Now`
- `setTimeout`
- `clearTimeout`
- `setInterval`
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ const noWebGlobalsRuleList = [
{ name: "cancelIdleCallback", message: "Use InjectedScript.utils.builtins.cancelIdleCallback instead" },
{ name: "Date", message: "Use InjectedScript.utils.builtins.Date instead" },
{ name: "Intl", message: "Use InjectedScript.utils.builtins.Intl instead" },
{ name: "Temporal", message: "Use InjectedScript.utils.builtins.Temporal instead" },
{ name: "performance", message: "Use InjectedScript.utils.builtins.performance instead" },
];

Expand Down
27 changes: 27 additions & 0 deletions packages/injected/src/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,27 @@ function createIntl(clock: ClockController, NativeIntl: Builtins['Intl']): Built
return ClockIntl;
}

function createTemporal(clock: ClockController, NativeTemporal: Builtins['Temporal']): Builtins['Temporal'] {
const NativeNow = NativeTemporal.Now;
const instant = () => NativeTemporal.Instant.fromEpochMilliseconds(Math.trunc(clock.now()));
const zonedDateTimeISO = (timeZone?: Temporal.TimeZoneLike) => instant().toZonedDateTimeISO(timeZone ?? NativeNow.timeZoneId());
const overrides: Partial<typeof Temporal.Now> = {
instant,
zonedDateTimeISO,
plainDateTimeISO: timeZone => zonedDateTimeISO(timeZone).toPlainDateTime(),
plainDateISO: timeZone => zonedDateTimeISO(timeZone).toPlainDate(),
plainTimeISO: timeZone => zonedDateTimeISO(timeZone).toPlainTime(),
};

// All properties of Temporal are non-enumerable, mirror them with their descriptors.
const ClockNow = Object.create(Object.getPrototypeOf(NativeNow), Object.getOwnPropertyDescriptors(NativeNow));
for (const [key, value] of Object.entries(overrides))
Object.defineProperty(ClockNow, key, { value, writable: true, enumerable: false, configurable: true });
const ClockTemporal = Object.create(Object.getPrototypeOf(NativeTemporal), Object.getOwnPropertyDescriptors(NativeTemporal));
Object.defineProperty(ClockTemporal, 'Now', { value: ClockNow, writable: true, enumerable: false, configurable: true });
return ClockTemporal;
}

function compareTimers(a: Timer, b: Timer) {
// Sort first by absolute timing
if (a.callAt < b.callAt)
Expand Down Expand Up @@ -623,6 +644,7 @@ function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: Buil
performance: globalObject.performance,
Intl: (globalObject as any).Intl,
AbortSignal: (globalObject as any).AbortSignal,
Temporal: (globalObject as any).Temporal,
};
const bound = { ...raw };
for (const key of Object.keys(bound) as (keyof Builtins)[]) {
Expand Down Expand Up @@ -700,6 +722,7 @@ function createApi(clock: ClockController, originals: Builtins, browserName?: st
Date: createDate(clock, originals.Date),
performance: originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']),
AbortSignal: originals.AbortSignal ? fakeAbortSignal(clock, originals.AbortSignal, browserName) : (undefined as unknown as Builtins['AbortSignal']),
Temporal: originals.Temporal ? createTemporal(clock, originals.Temporal) : (undefined as unknown as Builtins['Temporal']),
};
}

Expand Down Expand Up @@ -800,12 +823,16 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof Builtins)[];

for (const method of toFake) {
if (method === 'Temporal' && !api.Temporal)
continue;
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 === 'AbortSignal') {
(globalObject as any).AbortSignal = api[method]!;
} else if (method === 'Temporal') {
(globalObject as any).Temporal = api[method]!;
} else if (method === 'performance') {
(globalObject as any).performance = api[method]!;
const kEventTimeStamp = Symbol('playwrightEventTimeStamp');
Expand Down
2 changes: 2 additions & 0 deletions packages/injected/src/utilityScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type Builtins = {
Intl: typeof window['Intl'],
Date: typeof window['Date'],
AbortSignal: typeof window['AbortSignal'],
Temporal: typeof window['Temporal'],
};

export class UtilityScript {
Expand Down Expand Up @@ -57,6 +58,7 @@ export class UtilityScript {
Intl: global.Intl,
Date: global.Date,
AbortSignal: global.AbortSignal,
Temporal: (global as any).Temporal,
} satisfies Builtins;
}
if (this.isUnderTest)
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20604,6 +20604,7 @@ export interface Clock {
/**
* Install fake implementations for the following time-related functions:
* - `Date`
* - `Temporal.Now`
* - `setTimeout`
* - `clearTimeout`
* - `setInterval`
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20604,6 +20604,7 @@ export interface Clock {
/**
* Install fake implementations for the following time-related functions:
* - `Date`
* - `Temporal.Now`
* - `setTimeout`
* - `clearTimeout`
* - `setInterval`
Expand Down
74 changes: 74 additions & 0 deletions tests/library/page-clock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,80 @@ it.describe('Date.now', () => {
});
});

it.describe('Temporal.Now', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42830' } }, () => {
it.beforeEach(async ({ page }) => {
it.skip(!await page.evaluate(() => 'Temporal' in globalThis), 'Temporal is not supported');
});

it('should follow fixed time', async ({ page }) => {
await page.clock.setFixedTime(new Date('2020-01-01T00:00:00Z'));
await page.goto('data:text/html,');
expect(await page.evaluate(() => {
const Temporal = (globalThis as any).Temporal;
return {
instant: Temporal.Now.instant().toString(),
zonedDateTime: Temporal.Now.zonedDateTimeISO('UTC').toString(),
plainDateTime: Temporal.Now.plainDateTimeISO('UTC').toString(),
plainDate: Temporal.Now.plainDateISO('UTC').toString(),
plainTime: Temporal.Now.plainTimeISO('UTC').toString(),
};
})).toEqual({
instant: '2020-01-01T00:00:00Z',
zonedDateTime: '2020-01-01T00:00:00+00:00[UTC]',
plainDateTime: '2020-01-01T00:00:00',
plainDate: '2020-01-01',
plainTime: '00:00:00',
});
});

it('should advance with the clock', async ({ page }) => {
await page.clock.install({ time: 0 });
await page.goto('data:text/html,');
await page.clock.pauseAt(1000);
await page.clock.runFor(2000);
expect(await page.evaluate(() => (globalThis as any).Temporal.Now.instant().epochMilliseconds)).toBe(3000);
});

it('should use the system time zone by default', async ({ browser }) => {
const context = await browser.newContext({ timezoneId: 'America/Los_Angeles' });
const page = await context.newPage();
await page.clock.setFixedTime(new Date('2020-01-01T00:00:00Z'));
await page.goto('data:text/html,');
expect(await page.evaluate(() => {
const Temporal = (globalThis as any).Temporal;
return {
timeZoneId: Temporal.Now.timeZoneId(),
zonedDateTime: Temporal.Now.zonedDateTimeISO().toString(),
plainDateTime: Temporal.Now.plainDateTimeISO().toString(),
};
})).toEqual({
timeZoneId: 'America/Los_Angeles',
zonedDateTime: '2019-12-31T16:00:00-08:00[America/Los_Angeles]',
plainDateTime: '2019-12-31T16:00:00',
});
await context.close();
});

it('should keep the rest of Temporal intact', async ({ page }) => {
await page.clock.install({ time: 0 });
await page.goto('data:text/html,');
expect(await page.evaluate(() => {
const Temporal = (globalThis as any).Temporal;
return {
tag: Object.prototype.toString.call(Temporal.Now),
enumerable: Object.keys(Temporal).concat(Object.keys(Temporal.Now)),
isInstant: Temporal.Now.instant() instanceof Temporal.Instant,
duration: Temporal.Duration.from({ hours: 1 }).toString(),
};
})).toEqual({
tag: '[object Temporal.Now]',
enumerable: [],
isInstant: true,
duration: 'PT1H',
});
});
});

it('AbortSignal.timeout', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39293' } }, async ({ page, browserName }) => {
await page.clock.install({ time: 0 });
const controller = await page.evaluateHandle(() => {
Expand Down
Loading