feat(fetch): import/export storageState (#9244)

This commit is contained in:
Yury Semikhatsky 2021-09-30 14:14:29 -07:00 committed by GitHub
parent a1d0878fa1
commit 4e372dccb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 412 additions and 39 deletions

View file

@ -1170,12 +1170,7 @@ Returns storage state for this browser context, contains current cookies and loc
* langs: csharp, java * langs: csharp, java
- returns: <[string]> - returns: <[string]>
### option: BrowserContext.storageState.path ### option: BrowserContext.storageState.path = %%-storagestate-option-path-%%
- `path` <[path]>
The file path to save the storage state to. If [`option: path`] is a relative path, then it is resolved relative to
current working directory. If no path is provided, storage
state is still returned, but won't be saved to the disk.
## property: BrowserContext.tracing ## property: BrowserContext.tracing
- type: <[Tracing]> - type: <[Tracing]>

View file

@ -134,3 +134,24 @@ Whether to throw on response codes other than 2xx and 3xx. By default response o
for all status codes. for all status codes.
### option: FetchRequest.post.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%% ### option: FetchRequest.post.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%%
## async method: FetchRequest.storageState
- returns: <[Object]>
- `cookies` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
- `domain` <[string]>
- `path` <[string]>
- `expires` <[float]> Unix time in seconds.
- `httpOnly` <[boolean]>
- `secure` <[boolean]>
- `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">>
- `origins` <[Array]<[Object]>>
- `origin` <[string]>
- `localStorage` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.
### option: FetchRequest.storageState.path = %%-storagestate-option-path-%%

View file

@ -112,6 +112,28 @@ When using [`method: FetchRequest.get`], [`method: FetchRequest.post`], [`method
* baseURL: `http://localhost:3000` and sending rquest to `/bar.html` results in `http://localhost:3000/bar.html` * baseURL: `http://localhost:3000` and sending rquest to `/bar.html` results in `http://localhost:3000/bar.html`
* baseURL: `http://localhost:3000/foo/` and sending rquest to `./bar.html` results in `http://localhost:3000/foo/bar.html` * baseURL: `http://localhost:3000/foo/` and sending rquest to `./bar.html` results in `http://localhost:3000/foo/bar.html`
### option: Playwright._newRequest.storageState
- `storageState` <[path]|[Object]>
- `cookies` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
- `domain` <[string]>
- `path` <[string]>
- `expires` <[float]> Unix time in seconds.
- `httpOnly` <[boolean]>
- `secure` <[boolean]>
- `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">>
- `origins` <[Array]<[Object]>>
- `origin` <[string]>
- `localStorage` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
Populates context with given storage state. This option can be used to initialize context with logged-in information
obtained via [`method: BrowserContext.storageState`] or [`method: FetchRequest.storageState`]. Either a path to the
file with saved storage, or the value returned by one of [`method: BrowserContext.storageState`] or
[`method: FetchRequest.storageState`] methods.
## property: Playwright.chromium ## property: Playwright.chromium
- type: <[BrowserType]> - type: <[BrowserType]>

View file

@ -240,6 +240,13 @@ obtained via [`method: BrowserContext.storageState`].
Populates context with given storage state. This option can be used to initialize context with logged-in information Populates context with given storage state. This option can be used to initialize context with logged-in information
obtained via [`method: BrowserContext.storageState`]. Path to the file with saved storage state. obtained via [`method: BrowserContext.storageState`]. Path to the file with saved storage state.
## storagestate-option-path
- `path` <[path]>
The file path to save the storage state to. If [`option: path`] is a relative path, then it is resolved relative to
current working directory. If no path is provided, storage
state is still returned, but won't be saved to the disk.
## context-option-acceptdownloads ## context-option-acceptdownloads
- `acceptDownloads` <[boolean]> - `acceptDownloads` <[boolean]>

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { ReadStream } from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import * as mime from 'mime'; import * as mime from 'mime';
import { Serializable } from '../../types/structs'; import { Serializable } from '../../types/structs';
@ -22,11 +22,11 @@ import * as api from '../../types/types';
import { HeadersArray } from '../common/types'; import { HeadersArray } from '../common/types';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { kBrowserOrContextClosedError } from '../utils/errors'; import { kBrowserOrContextClosedError } from '../utils/errors';
import { assert, headersObjectToArray, isFilePayload, isString, objectToArray } from '../utils/utils'; import { assert, headersObjectToArray, isFilePayload, isString, mkdirIfNeeded, objectToArray } from '../utils/utils';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import * as network from './network'; import * as network from './network';
import { RawHeaders } from './network'; import { RawHeaders } from './network';
import { FilePayload, Headers } from './types'; import { FilePayload, Headers, StorageState } from './types';
export type FetchOptions = { export type FetchOptions = {
params?: { [key: string]: string; }, params?: { [key: string]: string; },
@ -110,8 +110,8 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
if (!Buffer.isBuffer(payload.buffer)) if (!Buffer.isBuffer(payload.buffer))
throw new Error(`Unexpected buffer type of 'data.${name}'`); throw new Error(`Unexpected buffer type of 'data.${name}'`);
formData[name] = filePayloadToJson(payload); formData[name] = filePayloadToJson(payload);
} else if (value instanceof ReadStream) { } else if (value instanceof fs.ReadStream) {
formData[name] = await readStreamToJson(value as ReadStream); formData[name] = await readStreamToJson(value as fs.ReadStream);
} else { } else {
formData[name] = value; formData[name] = value;
} }
@ -139,6 +139,17 @@ export class FetchRequest extends ChannelOwner<channels.FetchRequestChannel, cha
return new FetchResponse(this, result.response!); return new FetchResponse(this, result.response!);
}); });
} }
async storageState(options: { path?: string } = {}): Promise<StorageState> {
return await this._wrapApiCall(async (channel: channels.FetchRequestChannel) => {
const state = await channel.storageState();
if (options.path) {
await mkdirIfNeeded(options.path);
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
}
return state;
});
}
} }
export class FetchResponse implements api.FetchResponse { export class FetchResponse implements api.FetchResponse {
@ -226,7 +237,7 @@ function filePayloadToJson(payload: FilePayload): ServerFilePayload {
}; };
} }
async function readStreamToJson(stream: ReadStream): Promise<ServerFilePayload> { async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayload> {
const buffer = await new Promise<Buffer>((resolve, reject) => { const buffer = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
stream.on('data', chunk => chunks.push(chunk)); stream.on('data', chunk => chunks.push(chunk));

View file

@ -15,6 +15,7 @@
*/ */
import dns from 'dns'; import dns from 'dns';
import fs from 'fs';
import net from 'net'; import net from 'net';
import util from 'util'; import util from 'util';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
@ -72,9 +73,13 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
async _newRequest(options: NewRequestOptions = {}): Promise<FetchRequest> { async _newRequest(options: NewRequestOptions = {}): Promise<FetchRequest> {
return await this._wrapApiCall(async (channel: channels.PlaywrightChannel) => { return await this._wrapApiCall(async (channel: channels.PlaywrightChannel) => {
const storageState = typeof options.storageState === 'string' ?
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
options.storageState;
return FetchRequest.from((await channel.newRequest({ return FetchRequest.from((await channel.newRequest({
...options, ...options,
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
storageState,
})).request); })).request);
}); });
} }

View file

@ -17,7 +17,7 @@
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import type { Size } from '../common/types'; import type { Size } from '../common/types';
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray, NewRequestOptions } from '../common/types'; export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray } from '../common/types';
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error'; type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
export interface Logger { export interface Logger {
@ -58,7 +58,7 @@ export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'vie
logger?: Logger, logger?: Logger,
videosPath?: string, videosPath?: string,
videoSize?: Size, videoSize?: Size,
storageState?: string | channels.BrowserNewContextOptions['storageState'], storageState?: string | SetStorageState,
}; };
type LaunchOverrides = { type LaunchOverrides = {
@ -118,3 +118,8 @@ export type SelectorEngine = {
export type RemoteAddr = channels.RemoteAddr; export type RemoteAddr = channels.RemoteAddr;
export type SecurityDetails = channels.SecurityDetails; export type SecurityDetails = channels.SecurityDetails;
export type NewRequestOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'storageState'> & {
extraHTTPHeaders?: Headers,
storageState?: string | StorageState,
};

View file

@ -21,21 +21,4 @@ export type Quad = [ Point, Point, Point, Point ];
export type URLMatch = string | RegExp | ((url: URL) => boolean); export type URLMatch = string | RegExp | ((url: URL) => boolean);
export type TimeoutOptions = { timeout?: number }; export type TimeoutOptions = { timeout?: number };
export type NameValue = { name: string, value: string }; export type NameValue = { name: string, value: string };
export type HeadersArray = NameValue[]; export type HeadersArray = NameValue[];
export type NewRequestOptions = {
baseURL?: string;
extraHTTPHeaders?: { [key: string]: string; };
httpCredentials?: {
username: string;
password: string;
};
ignoreHTTPSErrors?: boolean;
proxy?: {
server: string;
bypass?: string;
username?: string;
password?: string;
};
timeout?: number;
userAgent?: string;
};

View file

@ -163,7 +163,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise<channels.BrowserContextStorageStateResult> { async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise<channels.BrowserContextStorageStateResult> {
return await this._context.storageState(metadata); return await this._context.storageState();
} }
async close(params: channels.BrowserContextCloseParams, metadata: CallMetadata): Promise<void> { async close(params: channels.BrowserContextCloseParams, metadata: CallMetadata): Promise<void> {

View file

@ -177,6 +177,10 @@ export class FetchRequestDispatcher extends Dispatcher<FetchRequest, channels.Fe
}); });
} }
async storageState(params?: channels.FetchRequestStorageStateParams): Promise<channels.FetchRequestStorageStateResult> {
return this._object.storageState();
}
async dispose(params?: channels.FetchRequestDisposeParams): Promise<void> { async dispose(params?: channels.FetchRequestDisposeParams): Promise<void> {
this._object.dispose(); this._object.dispose();
} }

View file

@ -162,6 +162,7 @@ export type FetchRequestInitializer = {};
export interface FetchRequestChannel extends Channel { export interface FetchRequestChannel extends Channel {
fetch(params: FetchRequestFetchParams, metadata?: Metadata): Promise<FetchRequestFetchResult>; fetch(params: FetchRequestFetchParams, metadata?: Metadata): Promise<FetchRequestFetchResult>;
fetchResponseBody(params: FetchRequestFetchResponseBodyParams, metadata?: Metadata): Promise<FetchRequestFetchResponseBodyResult>; fetchResponseBody(params: FetchRequestFetchResponseBodyParams, metadata?: Metadata): Promise<FetchRequestFetchResponseBodyResult>;
storageState(params?: FetchRequestStorageStateParams, metadata?: Metadata): Promise<FetchRequestStorageStateResult>;
disposeFetchResponse(params: FetchRequestDisposeFetchResponseParams, metadata?: Metadata): Promise<FetchRequestDisposeFetchResponseResult>; disposeFetchResponse(params: FetchRequestDisposeFetchResponseParams, metadata?: Metadata): Promise<FetchRequestDisposeFetchResponseResult>;
dispose(params?: FetchRequestDisposeParams, metadata?: Metadata): Promise<FetchRequestDisposeResult>; dispose(params?: FetchRequestDisposeParams, metadata?: Metadata): Promise<FetchRequestDisposeResult>;
} }
@ -199,6 +200,12 @@ export type FetchRequestFetchResponseBodyOptions = {
export type FetchRequestFetchResponseBodyResult = { export type FetchRequestFetchResponseBodyResult = {
binary?: Binary, binary?: Binary,
}; };
export type FetchRequestStorageStateParams = {};
export type FetchRequestStorageStateOptions = {};
export type FetchRequestStorageStateResult = {
cookies: NetworkCookie[],
origins: OriginStorage[],
};
export type FetchRequestDisposeFetchResponseParams = { export type FetchRequestDisposeFetchResponseParams = {
fetchUid: string, fetchUid: string,
}; };
@ -346,6 +353,10 @@ export type PlaywrightNewRequestParams = {
password?: string, password?: string,
}, },
timeout?: number, timeout?: number,
storageState?: {
cookies: NetworkCookie[],
origins: OriginStorage[],
},
}; };
export type PlaywrightNewRequestOptions = { export type PlaywrightNewRequestOptions = {
baseURL?: string, baseURL?: string,
@ -363,6 +374,10 @@ export type PlaywrightNewRequestOptions = {
password?: string, password?: string,
}, },
timeout?: number, timeout?: number,
storageState?: {
cookies: NetworkCookie[],
origins: OriginStorage[],
},
}; };
export type PlaywrightNewRequestResult = { export type PlaywrightNewRequestResult = {
request: FetchRequestChannel, request: FetchRequestChannel,

View file

@ -256,6 +256,15 @@ FetchRequest:
returns: returns:
binary?: binary binary?: binary
storageState:
returns:
cookies:
type: array
items: NetworkCookie
origins:
type: array
items: OriginStorage
disposeFetchResponse: disposeFetchResponse:
parameters: parameters:
fetchUid: string fetchUid: string
@ -484,6 +493,15 @@ Playwright:
username: string? username: string?
password: string? password: string?
timeout: number? timeout: number?
storageState:
type: object?
properties:
cookies:
type: array
items: NetworkCookie
origins:
type: array
items: OriginStorage
returns: returns:
request: FetchRequest request: FetchRequest

View file

@ -167,6 +167,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.FetchRequestFetchResponseBodyParams = tObject({ scheme.FetchRequestFetchResponseBodyParams = tObject({
fetchUid: tString, fetchUid: tString,
}); });
scheme.FetchRequestStorageStateParams = tOptional(tObject({}));
scheme.FetchRequestDisposeFetchResponseParams = tObject({ scheme.FetchRequestDisposeFetchResponseParams = tObject({
fetchUid: tString, fetchUid: tString,
}); });
@ -217,6 +218,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
password: tOptional(tString), password: tOptional(tString),
})), })),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
storageState: tOptional(tObject({
cookies: tArray(tType('NetworkCookie')),
origins: tArray(tType('OriginStorage')),
})),
}); });
scheme.SelectorsRegisterParams = tObject({ scheme.SelectorsRegisterParams = tObject({
name: tString, name: tString,

View file

@ -325,7 +325,7 @@ export abstract class BrowserContext extends SdkObject {
this._origins.add(origin); this._origins.add(origin);
} }
async storageState(metadata: CallMetadata): Promise<types.StorageState> { async storageState(): Promise<types.StorageState> {
const result: types.StorageState = { const result: types.StorageState = {
cookies: (await this.cookies()).filter(c => c.value !== ''), cookies: (await this.cookies()).filter(c => c.value !== ''),
origins: [] origins: []

View file

@ -68,13 +68,20 @@ export class CookieStore {
cookies(url: URL): types.NetworkCookie[] { cookies(url: URL): types.NetworkCookie[] {
const result = []; const result = [];
for (const cookie of this._allCookies()) { for (const cookie of this._cookiesIterator()) {
if (cookie.matches(url)) if (cookie.matches(url))
result.push(cookie.networkCookie()); result.push(cookie.networkCookie());
} }
return result; return result;
} }
allCookies(): types.NetworkCookie[] {
const result = [];
for (const cookie of this._cookiesIterator())
result.push(cookie.networkCookie());
return result;
}
private _addCookie(cookie: Cookie) { private _addCookie(cookie: Cookie) {
if (cookie.expired()) if (cookie.expired())
return; return;
@ -94,7 +101,7 @@ export class CookieStore {
set.add(cookie); set.add(cookie);
} }
private *_allCookies(): IterableIterator<Cookie> { private *_cookiesIterator(): IterableIterator<Cookie> {
for (const [name, cookies] of this._nameToCookies) { for (const [name, cookies] of this._nameToCookies) {
CookieStore.pruneExpired(cookies); CookieStore.pruneExpired(cookies);
for (const cookie of cookies) for (const cookie of cookies)

View file

@ -21,7 +21,7 @@ import { pipeline, Readable, Transform } from 'stream';
import url from 'url'; import url from 'url';
import zlib from 'zlib'; import zlib from 'zlib';
import { HTTPCredentials } from '../../types/types'; import { HTTPCredentials } from '../../types/types';
import { NameValue, NewRequestOptions } from '../common/types'; import * as channels from '../protocol/channels';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import { assert, createGuid, getPlaywrightVersion, isFilePayload, monotonicTime } from '../utils/utils'; import { assert, createGuid, getPlaywrightVersion, isFilePayload, monotonicTime } from '../utils/utils';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
@ -76,6 +76,7 @@ export abstract class FetchRequest extends SdkObject {
abstract _defaultOptions(): FetchRequestOptions; abstract _defaultOptions(): FetchRequestOptions;
abstract _addCookies(cookies: types.NetworkCookie[]): Promise<void>; abstract _addCookies(cookies: types.NetworkCookie[]): Promise<void>;
abstract _cookies(url: URL): Promise<types.NetworkCookie[]>; abstract _cookies(url: URL): Promise<types.NetworkCookie[]>;
abstract storageState(): Promise<channels.FetchRequestStorageStateResult>;
private _storeResponseBody(body: Buffer): string { private _storeResponseBody(body: Buffer): string {
const uid = createGuid(); const uid = createGuid();
@ -337,13 +338,19 @@ export class BrowserContextFetchRequest extends FetchRequest {
async _cookies(url: URL): Promise<types.NetworkCookie[]> { async _cookies(url: URL): Promise<types.NetworkCookie[]> {
return await this._context.cookies(url.toString()); return await this._context.cookies(url.toString());
} }
override async storageState(): Promise<channels.FetchRequestStorageStateResult> {
return this._context.storageState();
}
} }
export class GlobalFetchRequest extends FetchRequest { export class GlobalFetchRequest extends FetchRequest {
private readonly _cookieStore: CookieStore = new CookieStore(); private readonly _cookieStore: CookieStore = new CookieStore();
private readonly _options: FetchRequestOptions; private readonly _options: FetchRequestOptions;
constructor(playwright: Playwright, options: Omit<NewRequestOptions, 'extraHTTPHeaders'> & { extraHTTPHeaders?: NameValue[] }) { private readonly _origins: channels.OriginStorage[] | undefined;
constructor(playwright: Playwright, options: channels.PlaywrightNewRequestOptions) {
super(playwright); super(playwright);
const timeoutSettings = new TimeoutSettings(); const timeoutSettings = new TimeoutSettings();
if (options.timeout !== undefined) if (options.timeout !== undefined)
@ -355,6 +362,10 @@ export class GlobalFetchRequest extends FetchRequest {
url = 'http://' + url; url = 'http://' + url;
proxy.server = url; proxy.server = url;
} }
if (options.storageState) {
this._origins = options.storageState.origins;
this._cookieStore.addCookies(options.storageState.cookies);
}
this._options = { this._options = {
baseURL: options.baseURL, baseURL: options.baseURL,
userAgent: options.userAgent || `Playwright/${getPlaywrightVersion()}`, userAgent: options.userAgent || `Playwright/${getPlaywrightVersion()}`,
@ -382,6 +393,13 @@ export class GlobalFetchRequest extends FetchRequest {
async _cookies(url: URL): Promise<types.NetworkCookie[]> { async _cookies(url: URL): Promise<types.NetworkCookie[]> {
return this._cookieStore.cookies(url); return this._cookieStore.cookies(url);
} }
override async storageState(): Promise<channels.FetchRequestStorageStateResult> {
return {
cookies: this._cookieStore.allCookies(),
origins: this._origins || []
};
}
} }
function toHeadersArray(rawHeaders: string[]): types.HeadersArray { function toHeadersArray(rawHeaders: string[]): types.HeadersArray {

View file

@ -876,3 +876,17 @@ it('should throw when data passed for unsupported request', async function({ con
}).catch(e => e); }).catch(e => e);
expect(error.message).toContain(`Method GET does not accept post data`); expect(error.message).toContain(`Method GET does not accept post data`);
}); });
it('context request should export same storage state as context', async ({ context, page, server }) => {
server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['a=b', 'c=d']);
res.end();
});
await context._request.get(server.PREFIX + '/setcookie.html');
const contextState = await context.storageState();
expect(contextState.cookies.length).toBe(2);
const requestState = await context._request.storageState();
expect(requestState).toEqual(contextState);
const pageState = await page._request.storageState();
expect(pageState).toEqual(contextState);
});

View file

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import fs from 'fs';
import http from 'http'; import http from 'http';
import { FetchRequest } from '../index'; import { FetchRequest } from '../index';
import { expect, playwrightTest } from './config/browserTest'; import { expect, playwrightTest } from './config/browserTest';
@ -30,6 +31,9 @@ const it = playwrightTest.extend<GlobalFetchFixtures>({
}, },
}); });
type PromiseArg<T> = T extends Promise<infer R> ? R : never;
type StorageStateType = PromiseArg<ReturnType<FetchRequest['storageState']>>;
it.skip(({ mode }) => mode !== 'default'); it.skip(({ mode }) => mode !== 'default');
let prevAgent: http.Agent; let prevAgent: http.Agent;
@ -162,3 +166,153 @@ it('should remove expired cookies', async ({ request, server }) => {
expect(serverRequest.headers.cookie).toBe('a=v'); expect(serverRequest.headers.cookie).toBe('a=v');
}); });
it('should export cookies to storage state', async ({ request, server }) => {
const expires = new Date('12/31/2100 PST');
server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['a=b', `c=d; expires=${expires.toUTCString()}; domain=b.one.com; path=/input`, 'e=f; domain=b.one.com; path=/input/subfolder']);
res.end();
});
await request.get(`http://a.b.one.com:${server.PORT}/setcookie.html`);
const state = await request.storageState();
expect(state).toEqual({
'cookies': [
{
'name': 'a',
'value': 'b',
'domain': 'a.b.one.com',
'path': '/',
'expires': -1,
'httpOnly': false,
'secure': false,
'sameSite': 'Lax'
},
{
'name': 'c',
'value': 'd',
'domain': '.b.one.com',
'path': '/input',
'expires': +expires / 1000,
'httpOnly': false,
'secure': false,
'sameSite': 'Lax'
},
{
'name': 'e',
'value': 'f',
'domain': '.b.one.com',
'path': '/input/subfolder',
'expires': -1,
'httpOnly': false,
'secure': false,
'sameSite': 'Lax'
}
],
'origins': []
});
});
it('should preserve local storage on import/export of storage state', async ({ playwright, server }) => {
const storageState: StorageStateType = {
cookies: [
{
'name': 'a',
'value': 'b',
'domain': 'a.b.one.com',
'path': '/',
'expires': -1,
'httpOnly': false,
'secure': false,
'sameSite': 'Lax'
}
],
origins: [
{
origin: 'https://www.example.com',
localStorage: [{
name: 'name1',
value: 'value1'
}]
},
]
};
const request = await playwright._newRequest({ storageState });
await request.get(server.EMPTY_PAGE);
const exportedState = await request.storageState();
expect(exportedState).toEqual(storageState);
await request.dispose();
});
it('should send cookies from storage state', async ({ playwright, server }) => {
const expires = new Date('12/31/2099 PST');
const storageState: StorageStateType = {
'cookies': [
{
'name': 'a',
'value': 'b',
'domain': 'a.b.one.com',
'path': '/',
'expires': -1,
'httpOnly': false,
'secure': false,
'sameSite': 'Lax'
},
{
'name': 'c',
'value': 'd',
'domain': '.b.one.com',
'path': '/first/',
'expires': +expires / 1000,
'httpOnly': false,
'secure': false,
'sameSite': 'Lax'
},
{
'name': 'e',
'value': 'f',
'domain': '.b.one.com',
'path': '/first/second',
'expires': -1,
'httpOnly': false,
'secure': false,
'sameSite': 'Lax'
}
],
'origins': []
};
const request = await playwright._newRequest({ storageState });
const [serverRequest] = await Promise.all([
server.waitForRequest('/first/second/third/not_found.html'),
request.get(`http://www.a.b.one.com:${server.PORT}/first/second/third/not_found.html`)
]);
expect(serverRequest.headers.cookie).toBe('c=d; e=f');
});
it('storage state should round-trip through file', async ({ playwright, server }, testInfo) => {
const storageState: StorageStateType = {
'cookies': [
{
'name': 'a',
'value': 'b',
'domain': 'a.b.one.com',
'path': '/',
'expires': -1,
'httpOnly': false,
'secure': false,
'sameSite': 'Lax'
}
],
'origins': []
};
const request1 = await playwright._newRequest({ storageState });
const path = testInfo.outputPath('storage-state.json');
const state1 = await request1.storageState({ path });
expect(state1).toEqual(storageState);
const written = await fs.promises.readFile(path, 'utf8');
expect(JSON.stringify(state1, undefined, 2)).toBe(written);
const request2 = await playwright._newRequest({ storageState: path });
const state2 = await request2.storageState();
expect(state2).toEqual(storageState);
});

89
types/types.d.ts vendored
View file

@ -12797,6 +12797,50 @@ export interface FetchRequest {
*/ */
timeout?: number; timeout?: number;
}): Promise<FetchResponse>; }): Promise<FetchResponse>;
/**
* Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to
* the constructor.
* @param options
*/
storageState(options?: {
/**
* The file path to save the storage state to. If `path` is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
*/
path?: string;
}): Promise<{
cookies: Array<{
name: string;
value: string;
domain: string;
path: string;
/**
* Unix time in seconds.
*/
expires: number;
httpOnly: boolean;
secure: boolean;
sameSite: "Strict"|"Lax"|"None";
}>;
origins: Array<{
origin: string;
localStorage: Array<{
name: string;
value: string;
}>;
}>;
}>;
} }
/** /**
@ -13335,6 +13379,51 @@ export const _newRequest: (options?: {
password?: string; password?: string;
}; };
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via
* [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state)
* or
* [fetchRequest.storageState([options])](https://playwright.dev/docs/api/class-fetchrequest#fetch-request-storage-state).
* Either a path to the file with saved storage, or the value returned by one of
* [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state)
* or
* [fetchRequest.storageState([options])](https://playwright.dev/docs/api/class-fetchrequest#fetch-request-storage-state)
* methods.
*/
storageState?: string|{
cookies: Array<{
name: string;
value: string;
domain: string;
path: string;
/**
* Unix time in seconds.
*/
expires: number;
httpOnly: boolean;
secure: boolean;
sameSite: "Strict"|"Lax"|"None";
}>;
origins: Array<{
origin: string;
localStorage: Array<{
name: string;
value: string;
}>;
}>;
};
/** /**
* Maximum time in milliseconds to wait for the response. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. * Maximum time in milliseconds to wait for the response. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
*/ */