diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 83c73dc614..fc7aea4f4e 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1170,12 +1170,7 @@ Returns storage state for this browser context, contains current cookies and loc * langs: csharp, java - returns: <[string]> -### option: BrowserContext.storageState.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. +### option: BrowserContext.storageState.path = %%-storagestate-option-path-%% ## property: BrowserContext.tracing - type: <[Tracing]> diff --git a/docs/src/api/class-fetchrequest.md b/docs/src/api/class-fetchrequest.md index 07596a6bb9..98d6b773e5 100644 --- a/docs/src/api/class-fetchrequest.md +++ b/docs/src/api/class-fetchrequest.md @@ -134,3 +134,24 @@ Whether to throw on response codes other than 2xx and 3xx. By default response o for all status codes. ### 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-%% diff --git a/docs/src/api/class-playwright.md b/docs/src/api/class-playwright.md index 2676e9f5c8..9650b1fea9 100644 --- a/docs/src/api/class-playwright.md +++ b/docs/src/api/class-playwright.md @@ -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/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 - type: <[BrowserType]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 4a649c538e..37e02bea9f 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -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 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 - `acceptDownloads` <[boolean]> diff --git a/src/client/fetch.ts b/src/client/fetch.ts index 0ed45ee8fd..2186733eaa 100644 --- a/src/client/fetch.ts +++ b/src/client/fetch.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ReadStream } from 'fs'; +import fs from 'fs'; import path from 'path'; import * as mime from 'mime'; import { Serializable } from '../../types/structs'; @@ -22,11 +22,11 @@ import * as api from '../../types/types'; import { HeadersArray } from '../common/types'; import * as channels from '../protocol/channels'; 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 * as network from './network'; import { RawHeaders } from './network'; -import { FilePayload, Headers } from './types'; +import { FilePayload, Headers, StorageState } from './types'; export type FetchOptions = { params?: { [key: string]: string; }, @@ -110,8 +110,8 @@ export class FetchRequest extends ChannelOwner { + 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 { @@ -226,7 +237,7 @@ function filePayloadToJson(payload: FilePayload): ServerFilePayload { }; } -async function readStreamToJson(stream: ReadStream): Promise { +async function readStreamToJson(stream: fs.ReadStream): Promise { const buffer = await new Promise((resolve, reject) => { const chunks: Buffer[] = []; stream.on('data', chunk => chunks.push(chunk)); diff --git a/src/client/playwright.ts b/src/client/playwright.ts index a01c2b8e25..284e5b46cd 100644 --- a/src/client/playwright.ts +++ b/src/client/playwright.ts @@ -15,6 +15,7 @@ */ import dns from 'dns'; +import fs from 'fs'; import net from 'net'; import util from 'util'; import * as channels from '../protocol/channels'; @@ -72,9 +73,13 @@ export class Playwright extends ChannelOwner { 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({ ...options, - extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined + extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, + storageState, })).request); }); } diff --git a/src/client/types.ts b/src/client/types.ts index 7be13ff00a..8bae20de73 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -17,7 +17,7 @@ import * as channels from '../protocol/channels'; 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'; export interface Logger { @@ -58,7 +58,7 @@ export type BrowserContextOptions = Omit & { + extraHTTPHeaders?: Headers, + storageState?: string | StorageState, +}; diff --git a/src/common/types.ts b/src/common/types.ts index ff7f5f8e34..e179b8c4a1 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -21,21 +21,4 @@ export type Quad = [ Point, Point, Point, Point ]; export type URLMatch = string | RegExp | ((url: URL) => boolean); export type TimeoutOptions = { timeout?: number }; export type NameValue = { name: string, value: string }; -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; -}; \ No newline at end of file +export type HeadersArray = NameValue[]; \ No newline at end of file diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index e009eedd36..c771be46aa 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -163,7 +163,7 @@ export class BrowserContextDispatcher extends Dispatcher { - return await this._context.storageState(metadata); + return await this._context.storageState(); } async close(params: channels.BrowserContextCloseParams, metadata: CallMetadata): Promise { diff --git a/src/dispatchers/networkDispatchers.ts b/src/dispatchers/networkDispatchers.ts index e60400b9b9..52ebab2c1d 100644 --- a/src/dispatchers/networkDispatchers.ts +++ b/src/dispatchers/networkDispatchers.ts @@ -177,6 +177,10 @@ export class FetchRequestDispatcher extends Dispatcher { + return this._object.storageState(); + } + async dispose(params?: channels.FetchRequestDisposeParams): Promise { this._object.dispose(); } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index c4f4cf66a8..aeb413382d 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -162,6 +162,7 @@ export type FetchRequestInitializer = {}; export interface FetchRequestChannel extends Channel { fetch(params: FetchRequestFetchParams, metadata?: Metadata): Promise; fetchResponseBody(params: FetchRequestFetchResponseBodyParams, metadata?: Metadata): Promise; + storageState(params?: FetchRequestStorageStateParams, metadata?: Metadata): Promise; disposeFetchResponse(params: FetchRequestDisposeFetchResponseParams, metadata?: Metadata): Promise; dispose(params?: FetchRequestDisposeParams, metadata?: Metadata): Promise; } @@ -199,6 +200,12 @@ export type FetchRequestFetchResponseBodyOptions = { export type FetchRequestFetchResponseBodyResult = { binary?: Binary, }; +export type FetchRequestStorageStateParams = {}; +export type FetchRequestStorageStateOptions = {}; +export type FetchRequestStorageStateResult = { + cookies: NetworkCookie[], + origins: OriginStorage[], +}; export type FetchRequestDisposeFetchResponseParams = { fetchUid: string, }; @@ -346,6 +353,10 @@ export type PlaywrightNewRequestParams = { password?: string, }, timeout?: number, + storageState?: { + cookies: NetworkCookie[], + origins: OriginStorage[], + }, }; export type PlaywrightNewRequestOptions = { baseURL?: string, @@ -363,6 +374,10 @@ export type PlaywrightNewRequestOptions = { password?: string, }, timeout?: number, + storageState?: { + cookies: NetworkCookie[], + origins: OriginStorage[], + }, }; export type PlaywrightNewRequestResult = { request: FetchRequestChannel, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index cacb263cb3..71fc21a2e2 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -256,6 +256,15 @@ FetchRequest: returns: binary?: binary + storageState: + returns: + cookies: + type: array + items: NetworkCookie + origins: + type: array + items: OriginStorage + disposeFetchResponse: parameters: fetchUid: string @@ -484,6 +493,15 @@ Playwright: username: string? password: string? timeout: number? + storageState: + type: object? + properties: + cookies: + type: array + items: NetworkCookie + origins: + type: array + items: OriginStorage returns: request: FetchRequest diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 35d05098ba..97bffdc8f7 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -167,6 +167,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { scheme.FetchRequestFetchResponseBodyParams = tObject({ fetchUid: tString, }); + scheme.FetchRequestStorageStateParams = tOptional(tObject({})); scheme.FetchRequestDisposeFetchResponseParams = tObject({ fetchUid: tString, }); @@ -217,6 +218,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { password: tOptional(tString), })), timeout: tOptional(tNumber), + storageState: tOptional(tObject({ + cookies: tArray(tType('NetworkCookie')), + origins: tArray(tType('OriginStorage')), + })), }); scheme.SelectorsRegisterParams = tObject({ name: tString, diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 82f54eee1a..93b22509ce 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -325,7 +325,7 @@ export abstract class BrowserContext extends SdkObject { this._origins.add(origin); } - async storageState(metadata: CallMetadata): Promise { + async storageState(): Promise { const result: types.StorageState = { cookies: (await this.cookies()).filter(c => c.value !== ''), origins: [] diff --git a/src/server/cookieStore.ts b/src/server/cookieStore.ts index 5c9518986d..b1ed6ecaa2 100644 --- a/src/server/cookieStore.ts +++ b/src/server/cookieStore.ts @@ -68,13 +68,20 @@ export class CookieStore { cookies(url: URL): types.NetworkCookie[] { const result = []; - for (const cookie of this._allCookies()) { + for (const cookie of this._cookiesIterator()) { if (cookie.matches(url)) result.push(cookie.networkCookie()); } return result; } + allCookies(): types.NetworkCookie[] { + const result = []; + for (const cookie of this._cookiesIterator()) + result.push(cookie.networkCookie()); + return result; + } + private _addCookie(cookie: Cookie) { if (cookie.expired()) return; @@ -94,7 +101,7 @@ export class CookieStore { set.add(cookie); } - private *_allCookies(): IterableIterator { + private *_cookiesIterator(): IterableIterator { for (const [name, cookies] of this._nameToCookies) { CookieStore.pruneExpired(cookies); for (const cookie of cookies) diff --git a/src/server/fetch.ts b/src/server/fetch.ts index 1c02174d2a..e9a415eb08 100644 --- a/src/server/fetch.ts +++ b/src/server/fetch.ts @@ -21,7 +21,7 @@ import { pipeline, Readable, Transform } from 'stream'; import url from 'url'; import zlib from 'zlib'; import { HTTPCredentials } from '../../types/types'; -import { NameValue, NewRequestOptions } from '../common/types'; +import * as channels from '../protocol/channels'; import { TimeoutSettings } from '../utils/timeoutSettings'; import { assert, createGuid, getPlaywrightVersion, isFilePayload, monotonicTime } from '../utils/utils'; import { BrowserContext } from './browserContext'; @@ -76,6 +76,7 @@ export abstract class FetchRequest extends SdkObject { abstract _defaultOptions(): FetchRequestOptions; abstract _addCookies(cookies: types.NetworkCookie[]): Promise; abstract _cookies(url: URL): Promise; + abstract storageState(): Promise; private _storeResponseBody(body: Buffer): string { const uid = createGuid(); @@ -337,13 +338,19 @@ export class BrowserContextFetchRequest extends FetchRequest { async _cookies(url: URL): Promise { return await this._context.cookies(url.toString()); } + + override async storageState(): Promise { + return this._context.storageState(); + } } export class GlobalFetchRequest extends FetchRequest { private readonly _cookieStore: CookieStore = new CookieStore(); private readonly _options: FetchRequestOptions; - constructor(playwright: Playwright, options: Omit & { extraHTTPHeaders?: NameValue[] }) { + private readonly _origins: channels.OriginStorage[] | undefined; + + constructor(playwright: Playwright, options: channels.PlaywrightNewRequestOptions) { super(playwright); const timeoutSettings = new TimeoutSettings(); if (options.timeout !== undefined) @@ -355,6 +362,10 @@ export class GlobalFetchRequest extends FetchRequest { url = 'http://' + url; proxy.server = url; } + if (options.storageState) { + this._origins = options.storageState.origins; + this._cookieStore.addCookies(options.storageState.cookies); + } this._options = { baseURL: options.baseURL, userAgent: options.userAgent || `Playwright/${getPlaywrightVersion()}`, @@ -382,6 +393,13 @@ export class GlobalFetchRequest extends FetchRequest { async _cookies(url: URL): Promise { return this._cookieStore.cookies(url); } + + override async storageState(): Promise { + return { + cookies: this._cookieStore.allCookies(), + origins: this._origins || [] + }; + } } function toHeadersArray(rawHeaders: string[]): types.HeadersArray { diff --git a/tests/browsercontext-fetch.spec.ts b/tests/browsercontext-fetch.spec.ts index f44931d03c..2c5bbc016d 100644 --- a/tests/browsercontext-fetch.spec.ts +++ b/tests/browsercontext-fetch.spec.ts @@ -876,3 +876,17 @@ it('should throw when data passed for unsupported request', async function({ con }).catch(e => e); 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); +}); \ No newline at end of file diff --git a/tests/global-fetch-cookie.spec.ts b/tests/global-fetch-cookie.spec.ts index c1bbc847f1..409375ee19 100644 --- a/tests/global-fetch-cookie.spec.ts +++ b/tests/global-fetch-cookie.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import fs from 'fs'; import http from 'http'; import { FetchRequest } from '../index'; import { expect, playwrightTest } from './config/browserTest'; @@ -30,6 +31,9 @@ const it = playwrightTest.extend({ }, }); +type PromiseArg = T extends Promise ? R : never; +type StorageStateType = PromiseArg>; + it.skip(({ mode }) => mode !== 'default'); let prevAgent: http.Agent; @@ -162,3 +166,153 @@ it('should remove expired cookies', async ({ request, server }) => { 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); +}); diff --git a/types/types.d.ts b/types/types.d.ts index ee36b18743..20cca1fc54 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -12797,6 +12797,50 @@ export interface FetchRequest { */ timeout?: number; }): Promise; + + /** + * 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; }; + /** + * 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. */