feat(clock): expire cookies naively
This commit is contained in:
parent
897f7449ef
commit
cdec1a9324
|
|
@ -69,6 +69,22 @@ Fake timers are used to manually control the flow of time in tests. They allow y
|
||||||
|
|
||||||
Time to initialize with, current system time by default.
|
Time to initialize with, current system time by default.
|
||||||
|
|
||||||
|
### option: Clock.install.expireCookies
|
||||||
|
* since: v1.45
|
||||||
|
- `expireCookies` <[boolean]>
|
||||||
|
|
||||||
|
Whether to simulate cookies' expiration over time as clock is fast forwarded.
|
||||||
|
Requires clock to be installed with current time, i.e. [`option: time`] should not be specified. Defaults to `false`.
|
||||||
|
|
||||||
|
**Details**
|
||||||
|
|
||||||
|
Cookies are managed by the network stack, which is a part of the operating system in case of macOS.
|
||||||
|
Playwright only simulates the browser context time, so the network is largely unaware of the changes.
|
||||||
|
|
||||||
|
Passing this option will make Playwright simulate the cookie expiration over time via shifting its
|
||||||
|
expiration date into the past. Note that getting and setting cookies of the [BrowserContext] will
|
||||||
|
return raw cookie values as in the cookie jar.
|
||||||
|
|
||||||
## async method: Clock.runFor
|
## async method: Clock.runFor
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ export class Clock implements api.Clock {
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
async install(options: { time?: number | string | Date } = { }) {
|
async install(options: { time?: number | string | Date, expireCookies?: boolean } = { }) {
|
||||||
await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {});
|
await this._browserContext._channel.clockInstall({ ...parseTime(options.time), expireCookies: options.expireCookies });
|
||||||
}
|
}
|
||||||
|
|
||||||
async fastForward(ticks: number | string) {
|
async fastForward(ticks: number | string) {
|
||||||
|
|
@ -53,12 +53,14 @@ export class Clock implements api.Clock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTime(time: string | number | Date): { timeNumber?: number, timeString?: string } {
|
function parseTime(time: string | number | Date | undefined): { timeNumber?: number, timeString?: string } {
|
||||||
if (typeof time === 'number')
|
if (typeof time === 'number')
|
||||||
return { timeNumber: time };
|
return { timeNumber: time };
|
||||||
if (typeof time === 'string')
|
if (typeof time === 'string')
|
||||||
return { timeString: time };
|
return { timeString: time };
|
||||||
return { timeNumber: time.getTime() };
|
if (time)
|
||||||
|
return { timeNumber: time.getTime() };
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTicks(ticks: string | number): { ticksNumber?: number, ticksString?: string } {
|
function parseTicks(ticks: string | number): { ticksNumber?: number, ticksString?: string } {
|
||||||
|
|
|
||||||
|
|
@ -975,6 +975,7 @@ scheme.BrowserContextClockFastForwardResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextClockInstallParams = tObject({
|
scheme.BrowserContextClockInstallParams = tObject({
|
||||||
timeNumber: tOptional(tNumber),
|
timeNumber: tOptional(tNumber),
|
||||||
timeString: tOptional(tString),
|
timeString: tOptional(tString),
|
||||||
|
expireCookies: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextClockInstallResult = tOptional(tObject({}));
|
scheme.BrowserContextClockInstallResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextClockPauseAtParams = tObject({
|
scheme.BrowserContextClockPauseAtParams = tObject({
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
return await this.doGetCookies(urls as string[]);
|
return await this.doGetCookies(urls as string[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearCookies(options: {name?: string | RegExp, domain?: string | RegExp, path?: string | RegExp}): Promise<void> {
|
async clearCookies(options: {name?: string | RegExp, domain?: string | RegExp, path?: string | RegExp} = {}): Promise<void> {
|
||||||
const currentCookies = await this.cookies();
|
const currentCookies = await this.cookies();
|
||||||
await this.doClearCookies();
|
await this.doClearCookies();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { isJavaScriptErrorInEvaluate } from './javascript';
|
||||||
export class Clock {
|
export class Clock {
|
||||||
private _browserContext: BrowserContext;
|
private _browserContext: BrowserContext;
|
||||||
private _scriptInstalled = false;
|
private _scriptInstalled = false;
|
||||||
|
private _expireCookies = false;
|
||||||
|
|
||||||
constructor(browserContext: BrowserContext) {
|
constructor(browserContext: BrowserContext) {
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
|
|
@ -31,11 +32,15 @@ export class Clock {
|
||||||
const ticksMillis = parseTicks(ticks);
|
const ticksMillis = parseTicks(ticks);
|
||||||
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${ticksMillis})`);
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${ticksMillis})`);
|
||||||
await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForward(${ticksMillis})`);
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForward(${ticksMillis})`);
|
||||||
|
await this._expireCookiesIfNeeded(ticksMillis);
|
||||||
}
|
}
|
||||||
|
|
||||||
async install(time: number | string | undefined) {
|
async install(time: number | string | undefined, expireCookies: boolean) {
|
||||||
await this._installIfNeeded();
|
await this._installIfNeeded();
|
||||||
const timeMillis = time !== undefined ? parseTime(time) : Date.now();
|
const timeMillis = time !== undefined ? parseTime(time) : Date.now();
|
||||||
|
this._expireCookies = expireCookies;
|
||||||
|
if (time !== undefined && expireCookies)
|
||||||
|
throw new Error('Cannot expire cookies when installing the clock with the non-current time');
|
||||||
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('install', ${Date.now()}, ${timeMillis})`);
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('install', ${Date.now()}, ${timeMillis})`);
|
||||||
await this._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`);
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`);
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +49,8 @@ export class Clock {
|
||||||
await this._installIfNeeded();
|
await this._installIfNeeded();
|
||||||
const timeMillis = parseTime(ticks);
|
const timeMillis = parseTime(ticks);
|
||||||
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pauseAt', ${Date.now()}, ${timeMillis})`);
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pauseAt', ${Date.now()}, ${timeMillis})`);
|
||||||
await this._evaluateInFrames(`globalThis.__pwClock.controller.pauseAt(${timeMillis})`);
|
const consumedTicks = await this._evaluateInFrames(`globalThis.__pwClock.controller.pauseAt(${timeMillis})`);
|
||||||
|
this._expireCookiesIfNeeded(consumedTicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resume() {
|
async resume() {
|
||||||
|
|
@ -64,7 +70,8 @@ export class Clock {
|
||||||
await this._installIfNeeded();
|
await this._installIfNeeded();
|
||||||
const timeMillis = parseTime(time);
|
const timeMillis = parseTime(time);
|
||||||
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setSystemTime', ${Date.now()}, ${timeMillis})`);
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setSystemTime', ${Date.now()}, ${timeMillis})`);
|
||||||
await this._evaluateInFrames(`globalThis.__pwClock.controller.setSystemTime(${timeMillis})`);
|
const consumedTicks = await this._evaluateInFrames(`globalThis.__pwClock.controller.setSystemTime(${timeMillis})`);
|
||||||
|
this._expireCookiesIfNeeded(consumedTicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runFor(ticks: number | string) {
|
async runFor(ticks: number | string) {
|
||||||
|
|
@ -72,6 +79,7 @@ export class Clock {
|
||||||
const ticksMillis = parseTicks(ticks);
|
const ticksMillis = parseTicks(ticks);
|
||||||
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('runFor', ${Date.now()}, ${ticksMillis})`);
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('runFor', ${Date.now()}, ${ticksMillis})`);
|
||||||
await this._evaluateInFrames(`globalThis.__pwClock.controller.runFor(${ticksMillis})`);
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.runFor(${ticksMillis})`);
|
||||||
|
await this._expireCookiesIfNeeded(ticksMillis);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _installIfNeeded() {
|
private async _installIfNeeded() {
|
||||||
|
|
@ -95,7 +103,7 @@ export class Clock {
|
||||||
const frames = this._browserContext.pages().map(page => page.frames()).flat();
|
const frames = this._browserContext.pages().map(page => page.frames()).flat();
|
||||||
const results = await Promise.all(frames.map(async frame => {
|
const results = await Promise.all(frames.map(async frame => {
|
||||||
try {
|
try {
|
||||||
await frame.nonStallingEvaluateInExistingContext(script, false, 'main');
|
return await frame.nonStallingEvaluateInExistingContext(script, false, 'main');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isJavaScriptErrorInEvaluate(e))
|
if (isJavaScriptErrorInEvaluate(e))
|
||||||
throw e;
|
throw e;
|
||||||
|
|
@ -103,8 +111,26 @@ export class Clock {
|
||||||
}));
|
}));
|
||||||
return results[0];
|
return results[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _expireCookiesIfNeeded(ticks: number) {
|
||||||
|
if (!this._expireCookies)
|
||||||
|
return;
|
||||||
|
const cookies = await this._browserContext.cookies();
|
||||||
|
let dirty = false;
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
if (cookie.expires > 0) {
|
||||||
|
cookie.expires -= ticks / 1000;
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dirty) {
|
||||||
|
await this._browserContext.clearCookies();
|
||||||
|
await this._browserContext.addCookies(cookies);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into
|
* Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into
|
||||||
* number of milliseconds. This is used to support human-readable strings passed
|
* number of milliseconds. This is used to support human-readable strings passed
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
}
|
}
|
||||||
|
|
||||||
async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallResult> {
|
async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallResult> {
|
||||||
await this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined);
|
await this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined, !!params.expireCookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clockPauseAt(params: channels.BrowserContextClockPauseAtParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockPauseAtResult> {
|
async clockPauseAt(params: channels.BrowserContextClockPauseAtParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockPauseAtResult> {
|
||||||
|
|
|
||||||
15
packages/playwright-core/types/types.d.ts
vendored
15
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -17286,6 +17286,21 @@ export interface Clock {
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
install(options?: {
|
install(options?: {
|
||||||
|
/**
|
||||||
|
* Whether to simulate cookies' expiration over time as clock is fast forwarded. Requires clock to be installed with
|
||||||
|
* current time, i.e. `time` should not be specified. Defaults to `false`.
|
||||||
|
*
|
||||||
|
* **Details**
|
||||||
|
*
|
||||||
|
* Cookies are managed by the network stack, which is a part of the operating system in case of macOS. Playwright only
|
||||||
|
* simulates the browser context time, so the network is largely unaware of the changes.
|
||||||
|
*
|
||||||
|
* Passing this option will make Playwright simulate the cookie expiration over time via shifting its expiration date
|
||||||
|
* into the past. Note that getting and setting cookies of the {@link BrowserContext} will return raw cookie values as
|
||||||
|
* in the cookie jar.
|
||||||
|
*/
|
||||||
|
expireCookies?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time to initialize with, current system time by default.
|
* Time to initialize with, current system time by default.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1771,10 +1771,12 @@ export type BrowserContextClockFastForwardResult = void;
|
||||||
export type BrowserContextClockInstallParams = {
|
export type BrowserContextClockInstallParams = {
|
||||||
timeNumber?: number,
|
timeNumber?: number,
|
||||||
timeString?: string,
|
timeString?: string,
|
||||||
|
expireCookies?: boolean,
|
||||||
};
|
};
|
||||||
export type BrowserContextClockInstallOptions = {
|
export type BrowserContextClockInstallOptions = {
|
||||||
timeNumber?: number,
|
timeNumber?: number,
|
||||||
timeString?: string,
|
timeString?: string,
|
||||||
|
expireCookies?: boolean,
|
||||||
};
|
};
|
||||||
export type BrowserContextClockInstallResult = void;
|
export type BrowserContextClockInstallResult = void;
|
||||||
export type BrowserContextClockPauseAtParams = {
|
export type BrowserContextClockPauseAtParams = {
|
||||||
|
|
|
||||||
|
|
@ -1222,6 +1222,7 @@ BrowserContext:
|
||||||
parameters:
|
parameters:
|
||||||
timeNumber: number?
|
timeNumber: number?
|
||||||
timeString: string?
|
timeString: string?
|
||||||
|
expireCookies: boolean?
|
||||||
|
|
||||||
clockPauseAt:
|
clockPauseAt:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
|
||||||
|
|
@ -533,3 +533,42 @@ it.describe('while on pause', () => {
|
||||||
expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]);
|
expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('expireCookies should throw for non-current time', async ({ page }) => {
|
||||||
|
await expect(page.clock.install({ time: 0, expireCookies: true })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it.describe('cookies', () => {
|
||||||
|
it.beforeEach(async ({ page, server }) => {
|
||||||
|
await page.clock.install({ expireCookies: true });
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
const date = Date.now() + 1000 * 60 * 60 * 24;
|
||||||
|
await page.evaluate(timestamp => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
document.cookie = `username=John Doe;expires=${date.toUTCString()}`;
|
||||||
|
return document.cookie;
|
||||||
|
}, date);
|
||||||
|
expect(await page.evaluate(() => document.cookie)).toBe('username=John Doe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expire with fastForward', async ({ page }) => {
|
||||||
|
await page.clock.fastForward(1000 * 60 * 60 * 23);
|
||||||
|
expect(await page.evaluate(() => document.cookie)).toBe('username=John Doe');
|
||||||
|
await page.clock.fastForward(1000 * 60 * 60 * 1);
|
||||||
|
expect(await page.evaluate(() => document.cookie)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expire with runFor', async ({ page }) => {
|
||||||
|
await page.clock.fastForward(1000 * 60 * 60 * 23.9);
|
||||||
|
expect(await page.evaluate(() => document.cookie)).toBe('username=John Doe');
|
||||||
|
await page.clock.runFor(1000 * 60 * 60 * 0.1);
|
||||||
|
expect(await page.evaluate(() => document.cookie)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expire with pauseAt', async ({ page }) => {
|
||||||
|
const now = await page.evaluate(() => Date.now());
|
||||||
|
await page.clock.pauseAt(now + 1000 * 60 * 60 * 24);
|
||||||
|
await expect.poll(async () => await page.evaluate(() => document.cookie)).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue