Skip to content

Commit 4d7fb28

Browse files
authored
cherrypick(release-1.7): accept path in save/load storage apis (#4717)
Cherry-pick: PR #4714 SHA 355a58e
1 parent e6c206e commit 4d7fb28

File tree

11 files changed

+91
-20
lines changed

11 files changed

+91
-20
lines changed

docs-src/api-body.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,11 @@ Whether to emulate network being offline for the browser context.
545545

546546
Returns storage state for this browser context, contains current cookies and local storage snapshot.
547547

548+
### option: BrowserContext.storageState.path
549+
- `path` <[string]>
550+
551+
The file path to save the storage state to. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, storage state is still returned, but won't be saved to the disk.
552+
548553
## async method: BrowserContext.unroute
549554

550555
Removes a route created with [browserContext.route()](). When `handler` is not specified, removes all routes for the

docs-src/api-params.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Defaults to `'visible'`. Can be either:
115115

116116
## context-option-storage-state
117117

118-
- `storageState` <[Object]>
118+
- `storageState` <[string]|[Object]>
119119
- `cookies` <[Array]<[Object]>> Optional cookies to set for context
120120
- `name` <[string]> **required**
121121
- `value` <[string]> **required**
@@ -133,7 +133,7 @@ Defaults to `'visible'`. Can be either:
133133
- `value` <[string]>
134134

135135
Populates context with given storage state. This method can be used to initialize context with logged-in information
136-
obtained via [browserContext.storageState()]().
136+
obtained via [browserContext.storageState()](). Either a path to the file with saved storage, or an object with the following fields:
137137

138138
## context-option-acceptdownloads
139139

docs/api.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ Indicates that the browser is connected.
246246
- `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
247247
- `username` <[string]> Optional username to use if HTTP proxy requires authentication.
248248
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
249-
- `storageState` <[Object]> Populates context with given storage state. This method can be used to initialize context with logged-in information obtained via [browserContext.storageState()](#browsercontextstoragestate).
249+
- `storageState` <[string]|[Object]> Populates context with given storage state. This method can be used to initialize context with logged-in information obtained via [browserContext.storageState([options])](#browsercontextstoragestateoptions). Either a path to the file with saved storage, or an object with the following fields:
250250
- `cookies` <[Array]<[Object]>> Optional cookies to set for context
251251
- `name` <[string]> **required**
252252
- `value` <[string]> **required**
@@ -321,7 +321,7 @@ Creates a new browser context. It won't share cookies/cache with other browser c
321321
- `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
322322
- `username` <[string]> Optional username to use if HTTP proxy requires authentication.
323323
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
324-
- `storageState` <[Object]> Populates context with given storage state. This method can be used to initialize context with logged-in information obtained via [browserContext.storageState()](#browsercontextstoragestate).
324+
- `storageState` <[string]|[Object]> Populates context with given storage state. This method can be used to initialize context with logged-in information obtained via [browserContext.storageState([options])](#browsercontextstoragestateoptions). Either a path to the file with saved storage, or an object with the following fields:
325325
- `cookies` <[Array]<[Object]>> Optional cookies to set for context
326326
- `name` <[string]> **required**
327327
- `value` <[string]> **required**
@@ -393,7 +393,7 @@ await context.close();
393393
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
394394
- [browserContext.setHTTPCredentials(httpCredentials)](#browsercontextsethttpcredentialshttpcredentials)
395395
- [browserContext.setOffline(offline)](#browsercontextsetofflineoffline)
396-
- [browserContext.storageState()](#browsercontextstoragestate)
396+
- [browserContext.storageState([options])](#browsercontextstoragestateoptions)
397397
- [browserContext.unroute(url[, handler])](#browsercontextunrouteurl-handler)
398398
- [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate)
399399
<!-- GEN:stop -->
@@ -745,7 +745,9 @@ instead.
745745
- `offline` <[boolean]> Whether to emulate network being offline for the browser context.
746746
- returns: <[Promise]>
747747

748-
#### browserContext.storageState()
748+
#### browserContext.storageState([options])
749+
- `options` <[Object]>
750+
- `path` <[string]> The file path to save the storage state to. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, storage state is still returned, but won't be saved to the disk.
749751
- returns: <[Promise]<[Object]>>
750752
- `cookies` <[Array]<[Object]>>
751753
- `name` <[string]>
@@ -5185,7 +5187,7 @@ const backgroundPage = await context.waitForEvent('backgroundpage');
51855187
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
51865188
- [browserContext.setHTTPCredentials(httpCredentials)](#browsercontextsethttpcredentialshttpcredentials)
51875189
- [browserContext.setOffline(offline)](#browsercontextsetofflineoffline)
5188-
- [browserContext.storageState()](#browsercontextstoragestate)
5190+
- [browserContext.storageState([options])](#browsercontextstoragestateoptions)
51895191
- [browserContext.unroute(url[, handler])](#browsercontextunrouteurl-handler)
51905192
- [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate)
51915193
<!-- GEN:stop -->

src/client/android.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import * as util from 'util';
1919
import { isString } from '../utils/utils';
2020
import * as channels from '../protocol/channels';
2121
import { Events } from './events';
22-
import { BrowserContext, validateBrowserContextOptions } from './browserContext';
22+
import { BrowserContext, prepareBrowserContextOptions } from './browserContext';
2323
import { ChannelOwner } from './channelOwner';
2424
import * as apiInternal from '../../android-types-internal';
2525
import * as types from './types';
@@ -235,7 +235,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
235235

236236
async launchBrowser(options: types.BrowserContextOptions & { packageName?: string } = {}): Promise<BrowserContext> {
237237
return this._wrapApiCall('androidDevice.launchBrowser', async () => {
238-
const contextOptions = validateBrowserContextOptions(options);
238+
const contextOptions = await prepareBrowserContextOptions(options);
239239
const { context } = await this._channel.launchBrowser(contextOptions);
240240
return BrowserContext.from(context);
241241
});

src/client/browser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import * as channels from '../protocol/channels';
18-
import { BrowserContext, validateBrowserContextOptions } from './browserContext';
18+
import { BrowserContext, prepareBrowserContextOptions } from './browserContext';
1919
import { Page } from './page';
2020
import { ChannelOwner } from './channelOwner';
2121
import { Events } from './events';
@@ -46,7 +46,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel, channels.Brow
4646
return this._wrapApiCall('browser.newContext', async () => {
4747
if (this._isRemote && options._tracePath)
4848
throw new Error(`"_tracePath" is not supported in connected browser`);
49-
const contextOptions = validateBrowserContextOptions(options);
49+
const contextOptions = await prepareBrowserContextOptions(options);
5050
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
5151
context._options = contextOptions;
5252
this._contexts.add(context);

src/client/browserContext.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,21 @@
1818
import { Page, BindingCall, FunctionWithSource } from './page';
1919
import * as network from './network';
2020
import * as channels from '../protocol/channels';
21+
import * as util from 'util';
22+
import * as fs from 'fs';
2123
import { ChannelOwner } from './channelOwner';
2224
import { deprecate, evaluationScript, urlMatches } from './clientHelper';
2325
import { Browser } from './browser';
2426
import { Events } from './events';
2527
import { TimeoutSettings } from '../utils/timeoutSettings';
2628
import { Waiter } from './waiter';
2729
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState } from './types';
28-
import { isUnderTest, headersObjectToArray } from '../utils/utils';
30+
import { isUnderTest, headersObjectToArray, mkdirIfNeeded } from '../utils/utils';
2931
import { isSafeCloseError } from '../utils/errors';
3032

33+
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
34+
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
35+
3136
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> {
3237
_pages = new Set<Page>();
3338
private _routes: { url: URLMatch, handler: network.RouteHandler }[] = [];
@@ -219,9 +224,14 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
219224
return result;
220225
}
221226

222-
async storageState(): Promise<StorageState> {
227+
async storageState(options: { path?: string } = {}): Promise<StorageState> {
223228
return await this._wrapApiCall('browserContext.storageState', async () => {
224-
return await this._channel.storageState();
229+
const state = await this._channel.storageState();
230+
if (options.path) {
231+
await mkdirIfNeeded(options.path);
232+
await fsWriteFileAsync(options.path, JSON.stringify(state), 'utf8');
233+
}
234+
return state;
225235
});
226236
}
227237

@@ -245,7 +255,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
245255
}
246256
}
247257

248-
export function validateBrowserContextOptions(options: BrowserContextOptions): channels.BrowserNewContextOptions {
258+
export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise<channels.BrowserNewContextOptions> {
249259
if (options.videoSize && !options.videosPath)
250260
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
251261
if (options.extraHTTPHeaders)
@@ -255,6 +265,7 @@ export function validateBrowserContextOptions(options: BrowserContextOptions): c
255265
viewport: options.viewport === null ? undefined : options.viewport,
256266
noDefaultViewport: options.viewport === null,
257267
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
268+
storageState: typeof options.storageState === 'string' ? JSON.parse(await fsReadFileAsync(options.storageState, 'utf8')) : options.storageState,
258269
};
259270
if (!contextOptions.recordVideo && options.videosPath) {
260271
contextOptions.recordVideo = {

src/client/browserType.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import * as channels from '../protocol/channels';
1818
import { Browser } from './browser';
19-
import { BrowserContext, validateBrowserContextOptions } from './browserContext';
19+
import { BrowserContext, prepareBrowserContextOptions } from './browserContext';
2020
import { ChannelOwner } from './channelOwner';
2121
import { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions } from './types';
2222
import * as WebSocket from 'ws';
@@ -92,7 +92,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
9292
async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> {
9393
return this._wrapApiCall('browserType.launchPersistentContext', async () => {
9494
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
95-
const contextOptions = validateBrowserContextOptions(options);
95+
const contextOptions = await prepareBrowserContextOptions(options);
9696
const persistentOptions: channels.BrowserTypeLaunchPersistentContextParams = {
9797
...contextOptions,
9898
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,

src/client/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,13 @@ export type SetStorageState = {
4949
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle';
5050
export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle']);
5151

52-
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders'> & {
52+
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'storageState'> & {
5353
viewport?: Size | null,
5454
extraHTTPHeaders?: Headers,
5555
logger?: Logger,
5656
videosPath?: string,
5757
videoSize?: Size,
58+
storageState?: string | channels.BrowserNewContextOptions['storageState'],
5859
};
5960

6061
type LaunchOverrides = {

src/server/firefox/ffBrowser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,11 @@ export class FFBrowserContext extends BrowserContext {
242242
}
243243

244244
async addCookies(cookies: types.SetNetworkCookieParam[]) {
245-
await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId, cookies: network.rewriteCookies(cookies) });
245+
const cc = network.rewriteCookies(cookies).map(c => ({
246+
...c,
247+
expires: c.expires && c.expires !== -1 ? c.expires : undefined,
248+
}));
249+
await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId, cookies: cc });
246250
}
247251

248252
async clearCookies() {

test/browsercontext-add-cookies.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ it('should work', async ({context, page, server}) => {
2727
expect(await page.evaluate(() => document.cookie)).toEqual('password=123456');
2828
});
2929

30+
it('should work with expires=-1', async ({context, page}) => {
31+
await context.addCookies([{
32+
name: 'username',
33+
value: 'John Doe',
34+
domain: 'www.example.com',
35+
path: '/',
36+
expires: -1,
37+
httpOnly: false,
38+
secure: false,
39+
sameSite: 'None',
40+
}]);
41+
await page.route('**/*', route => {
42+
route.fulfill({ body: '<html></html>' }).catch(() => {});
43+
});
44+
await page.goto('https://www.example.com');
45+
expect(await page.evaluate(() => document.cookie)).toEqual('username=John Doe');
46+
});
47+
3048
it('should roundtrip cookie', async ({context, page, server}) => {
3149
await page.goto(server.EMPTY_PAGE);
3250
// @see https://en.wikipedia.org/wiki/Year_2038_problem

0 commit comments

Comments
 (0)