feat(fetch): import/export storageState (#9244)
This commit is contained in:
parent
a1d0878fa1
commit
4e372dccb5
|
|
@ -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]>
|
||||||
|
|
|
||||||
|
|
@ -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-%%
|
||||||
|
|
|
||||||
|
|
@ -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]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: []
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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
89
types/types.d.ts
vendored
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue