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.
|
||||
|
||||
### 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
|
||||
* since: v1.45
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ export class Clock implements api.Clock {
|
|||
this._browserContext = browserContext;
|
||||
}
|
||||
|
||||
async install(options: { time?: number | string | Date } = { }) {
|
||||
await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {});
|
||||
async install(options: { time?: number | string | Date, expireCookies?: boolean } = { }) {
|
||||
await this._browserContext._channel.clockInstall({ ...parseTime(options.time), expireCookies: options.expireCookies });
|
||||
}
|
||||
|
||||
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')
|
||||
return { timeNumber: time };
|
||||
if (typeof time === 'string')
|
||||
return { timeString: time };
|
||||
return { timeNumber: time.getTime() };
|
||||
if (time)
|
||||
return { timeNumber: time.getTime() };
|
||||
return {};
|
||||
}
|
||||
|
||||
function parseTicks(ticks: string | number): { ticksNumber?: number, ticksString?: string } {
|
||||
|
|
|
|||
|
|
@ -975,6 +975,7 @@ scheme.BrowserContextClockFastForwardResult = tOptional(tObject({}));
|
|||
scheme.BrowserContextClockInstallParams = tObject({
|
||||
timeNumber: tOptional(tNumber),
|
||||
timeString: tOptional(tString),
|
||||
expireCookies: tOptional(tBoolean),
|
||||
});
|
||||
scheme.BrowserContextClockInstallResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextClockPauseAtParams = tObject({
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
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();
|
||||
await this.doClearCookies();
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { isJavaScriptErrorInEvaluate } from './javascript';
|
|||
export class Clock {
|
||||
private _browserContext: BrowserContext;
|
||||
private _scriptInstalled = false;
|
||||
private _expireCookies = false;
|
||||
|
||||
constructor(browserContext: BrowserContext) {
|
||||
this._browserContext = browserContext;
|
||||
|
|
@ -31,11 +32,15 @@ export class Clock {
|
|||
const ticksMillis = parseTicks(ticks);
|
||||
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${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();
|
||||
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._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`);
|
||||
}
|
||||
|
|
@ -44,7 +49,8 @@ export class Clock {
|
|||
await this._installIfNeeded();
|
||||
const timeMillis = parseTime(ticks);
|
||||
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() {
|
||||
|
|
@ -64,7 +70,8 @@ export class Clock {
|
|||
await this._installIfNeeded();
|
||||
const timeMillis = parseTime(time);
|
||||
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) {
|
||||
|
|
@ -72,6 +79,7 @@ export class Clock {
|
|||
const ticksMillis = parseTicks(ticks);
|
||||
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('runFor', ${Date.now()}, ${ticksMillis})`);
|
||||
await this._evaluateInFrames(`globalThis.__pwClock.controller.runFor(${ticksMillis})`);
|
||||
await this._expireCookiesIfNeeded(ticksMillis);
|
||||
}
|
||||
|
||||
private async _installIfNeeded() {
|
||||
|
|
@ -95,7 +103,7 @@ export class Clock {
|
|||
const frames = this._browserContext.pages().map(page => page.frames()).flat();
|
||||
const results = await Promise.all(frames.map(async frame => {
|
||||
try {
|
||||
await frame.nonStallingEvaluateInExistingContext(script, false, 'main');
|
||||
return await frame.nonStallingEvaluateInExistingContext(script, false, 'main');
|
||||
} catch (e) {
|
||||
if (isJavaScriptErrorInEvaluate(e))
|
||||
throw e;
|
||||
|
|
@ -103,8 +111,26 @@ export class Clock {
|
|||
}));
|
||||
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
|
||||
* 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> {
|
||||
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> {
|
||||
|
|
|
|||
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
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1771,10 +1771,12 @@ export type BrowserContextClockFastForwardResult = void;
|
|||
export type BrowserContextClockInstallParams = {
|
||||
timeNumber?: number,
|
||||
timeString?: string,
|
||||
expireCookies?: boolean,
|
||||
};
|
||||
export type BrowserContextClockInstallOptions = {
|
||||
timeNumber?: number,
|
||||
timeString?: string,
|
||||
expireCookies?: boolean,
|
||||
};
|
||||
export type BrowserContextClockInstallResult = void;
|
||||
export type BrowserContextClockPauseAtParams = {
|
||||
|
|
|
|||
|
|
@ -1222,6 +1222,7 @@ BrowserContext:
|
|||
parameters:
|
||||
timeNumber: number?
|
||||
timeString: string?
|
||||
expireCookies: boolean?
|
||||
|
||||
clockPauseAt:
|
||||
parameters:
|
||||
|
|
|
|||
|
|
@ -533,3 +533,42 @@ it.describe('while on pause', () => {
|
|||
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