1 | /**
|
2 | * @license
|
3 | * Copyright 2017 Google Inc.
|
4 | * SPDX-License-Identifier: Apache-2.0
|
5 | */
|
6 |
|
7 | import {
|
8 | firstValueFrom,
|
9 | from,
|
10 | merge,
|
11 | raceWith,
|
12 | } from '../../third_party/rxjs/rxjs.js';
|
13 | import type {Cookie, CookieData} from '../common/Cookie.js';
|
14 | import {EventEmitter, type EventType} from '../common/EventEmitter.js';
|
15 | import {
|
16 | debugError,
|
17 | fromEmitterEvent,
|
18 | filterAsync,
|
19 | timeout,
|
20 | } from '../common/util.js';
|
21 | import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
|
22 | import {Mutex} from '../util/Mutex.js';
|
23 |
|
24 | import type {Browser, Permission, WaitForTargetOptions} from './Browser.js';
|
25 | import type {Page} from './Page.js';
|
26 | import type {Target} from './Target.js';
|
27 |
|
28 | /**
|
29 | * @public
|
30 | */
|
31 | export const enum BrowserContextEvent {
|
32 | /**
|
33 | * Emitted when the url of a target inside the browser context changes.
|
34 | * Contains a {@link Target} instance.
|
35 | */
|
36 | TargetChanged = 'targetchanged',
|
37 |
|
38 | /**
|
39 | * Emitted when a target is created within the browser context, for example
|
40 | * when a new page is opened by
|
41 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
|
42 | * or by {@link BrowserContext.newPage | browserContext.newPage}
|
43 | *
|
44 | * Contains a {@link Target} instance.
|
45 | */
|
46 | TargetCreated = 'targetcreated',
|
47 | /**
|
48 | * Emitted when a target is destroyed within the browser context, for example
|
49 | * when a page is closed. Contains a {@link Target} instance.
|
50 | */
|
51 | TargetDestroyed = 'targetdestroyed',
|
52 | }
|
53 |
|
54 | /**
|
55 | * @public
|
56 | */
|
57 | export interface BrowserContextEvents extends Record<EventType, unknown> {
|
58 | [BrowserContextEvent.TargetChanged]: Target;
|
59 | [BrowserContextEvent.TargetCreated]: Target;
|
60 | [BrowserContextEvent.TargetDestroyed]: Target;
|
61 | }
|
62 |
|
63 | /**
|
64 | * {@link BrowserContext} represents individual user contexts within a
|
65 | * {@link Browser | browser}.
|
66 | *
|
67 | * When a {@link Browser | browser} is launched, it has at least one default
|
68 | * {@link BrowserContext | browser context}. Others can be created
|
69 | * using {@link Browser.createBrowserContext}. Each context has isolated storage
|
70 | * (cookies/localStorage/etc.)
|
71 | *
|
72 | * {@link BrowserContext} {@link EventEmitter | emits} various events which are
|
73 | * documented in the {@link BrowserContextEvent} enum.
|
74 | *
|
75 | * If a {@link Page | page} opens another {@link Page | page}, e.g. using
|
76 | * `window.open`, the popup will belong to the parent {@link Page.browserContext
|
77 | * | page's browser context}.
|
78 | *
|
79 | * @example Creating a new {@link BrowserContext | browser context}:
|
80 | *
|
81 | * ```ts
|
82 | * // Create a new browser context
|
83 | * const context = await browser.createBrowserContext();
|
84 | * // Create a new page inside context.
|
85 | * const page = await context.newPage();
|
86 | * // ... do stuff with page ...
|
87 | * await page.goto('https://example.com');
|
88 | * // Dispose context once it's no longer needed.
|
89 | * await context.close();
|
90 | * ```
|
91 | *
|
92 | * @remarks
|
93 | *
|
94 | * In Chrome all non-default contexts are incognito,
|
95 | * and {@link Browser.defaultBrowserContext | default browser context}
|
96 | * might be incognito if you provide the `--incognito` argument when launching
|
97 | * the browser.
|
98 | *
|
99 | * @public
|
100 | */
|
101 |
|
102 | export abstract class BrowserContext extends EventEmitter<BrowserContextEvents> {
|
103 | /**
|
104 | * @internal
|
105 | */
|
106 | constructor() {
|
107 | super();
|
108 | }
|
109 |
|
110 | /**
|
111 | * Gets all active {@link Target | targets} inside this
|
112 | * {@link BrowserContext | browser context}.
|
113 | */
|
114 | abstract targets(): Target[];
|
115 |
|
116 | /**
|
117 | * If defined, indicates an ongoing screenshot opereation.
|
118 | */
|
119 | #pageScreenshotMutex?: Mutex;
|
120 | #screenshotOperationsCount = 0;
|
121 |
|
122 | /**
|
123 | * @internal
|
124 | */
|
125 | startScreenshot(): Promise<InstanceType<typeof Mutex.Guard>> {
|
126 | const mutex = this.#pageScreenshotMutex || new Mutex();
|
127 | this.#pageScreenshotMutex = mutex;
|
128 | this.#screenshotOperationsCount++;
|
129 | return mutex.acquire(() => {
|
130 | this.#screenshotOperationsCount--;
|
131 | if (this.#screenshotOperationsCount === 0) {
|
132 | // Remove the mutex to indicate no ongoing screenshot operation.
|
133 | this.#pageScreenshotMutex = undefined;
|
134 | }
|
135 | });
|
136 | }
|
137 |
|
138 | /**
|
139 | * @internal
|
140 | */
|
141 | waitForScreenshotOperations():
|
142 | | Promise<InstanceType<typeof Mutex.Guard>>
|
143 | | undefined {
|
144 | return this.#pageScreenshotMutex?.acquire();
|
145 | }
|
146 |
|
147 | /**
|
148 | * Waits until a {@link Target | target} matching the given `predicate`
|
149 | * appears and returns it.
|
150 | *
|
151 | * This will look all open {@link BrowserContext | browser contexts}.
|
152 | *
|
153 | * @example Finding a target for a page opened via `window.open`:
|
154 | *
|
155 | * ```ts
|
156 | * await page.evaluate(() => window.open('https://www.example.com/'));
|
157 | * const newWindowTarget = await browserContext.waitForTarget(
|
158 | * target => target.url() === 'https://www.example.com/',
|
159 | * );
|
160 | * ```
|
161 | */
|
162 | async waitForTarget(
|
163 | predicate: (x: Target) => boolean | Promise<boolean>,
|
164 | options: WaitForTargetOptions = {},
|
165 | ): Promise<Target> {
|
166 | const {timeout: ms = 30000} = options;
|
167 | return await firstValueFrom(
|
168 | merge(
|
169 | fromEmitterEvent(this, BrowserContextEvent.TargetCreated),
|
170 | fromEmitterEvent(this, BrowserContextEvent.TargetChanged),
|
171 | from(this.targets()),
|
172 | ).pipe(filterAsync(predicate), raceWith(timeout(ms))),
|
173 | );
|
174 | }
|
175 |
|
176 | /**
|
177 | * Gets a list of all open {@link Page | pages} inside this
|
178 | * {@link BrowserContext | browser context}.
|
179 | *
|
180 | * @remarks Non-visible {@link Page | pages}, such as `"background_page"`,
|
181 | * will not be listed here. You can find them using {@link Target.page}.
|
182 | */
|
183 | abstract pages(): Promise<Page[]>;
|
184 |
|
185 | /**
|
186 | * Grants this {@link BrowserContext | browser context} the given
|
187 | * `permissions` within the given `origin`.
|
188 | *
|
189 | * @example Overriding permissions in the
|
190 | * {@link Browser.defaultBrowserContext | default browser context}:
|
191 | *
|
192 | * ```ts
|
193 | * const context = browser.defaultBrowserContext();
|
194 | * await context.overridePermissions('https://html5demos.com', [
|
195 | * 'geolocation',
|
196 | * ]);
|
197 | * ```
|
198 | *
|
199 | * @param origin - The origin to grant permissions to, e.g.
|
200 | * "https://example.com".
|
201 | * @param permissions - An array of permissions to grant. All permissions that
|
202 | * are not listed here will be automatically denied.
|
203 | */
|
204 | abstract overridePermissions(
|
205 | origin: string,
|
206 | permissions: Permission[],
|
207 | ): Promise<void>;
|
208 |
|
209 | /**
|
210 | * Clears all permission overrides for this
|
211 | * {@link BrowserContext | browser context}.
|
212 | *
|
213 | * @example Clearing overridden permissions in the
|
214 | * {@link Browser.defaultBrowserContext | default browser context}:
|
215 | *
|
216 | * ```ts
|
217 | * const context = browser.defaultBrowserContext();
|
218 | * context.overridePermissions('https://example.com', ['clipboard-read']);
|
219 | * // do stuff ..
|
220 | * context.clearPermissionOverrides();
|
221 | * ```
|
222 | */
|
223 | abstract clearPermissionOverrides(): Promise<void>;
|
224 |
|
225 | /**
|
226 | * Creates a new {@link Page | page} in this
|
227 | * {@link BrowserContext | browser context}.
|
228 | */
|
229 | abstract newPage(): Promise<Page>;
|
230 |
|
231 | /**
|
232 | * Gets the {@link Browser | browser} associated with this
|
233 | * {@link BrowserContext | browser context}.
|
234 | */
|
235 | abstract browser(): Browser;
|
236 |
|
237 | /**
|
238 | * Closes this {@link BrowserContext | browser context} and all associated
|
239 | * {@link Page | pages}.
|
240 | *
|
241 | * @remarks The
|
242 | * {@link Browser.defaultBrowserContext | default browser context} cannot be
|
243 | * closed.
|
244 | */
|
245 | abstract close(): Promise<void>;
|
246 |
|
247 | /**
|
248 | * Gets all cookies in the browser context.
|
249 | */
|
250 | abstract cookies(): Promise<Cookie[]>;
|
251 |
|
252 | /**
|
253 | * Sets a cookie in the browser context.
|
254 | */
|
255 | abstract setCookie(...cookies: CookieData[]): Promise<void>;
|
256 |
|
257 | /**
|
258 | * Removes cookie in the browser context
|
259 | * @param cookies - {@link Cookie | cookie} to remove
|
260 | */
|
261 | async deleteCookie(...cookies: Cookie[]): Promise<void> {
|
262 | return await this.setCookie(
|
263 | ...cookies.map(cookie => {
|
264 | return {
|
265 | ...cookie,
|
266 | expires: 1,
|
267 | };
|
268 | }),
|
269 | );
|
270 | }
|
271 |
|
272 | /**
|
273 | * Whether this {@link BrowserContext | browser context} is closed.
|
274 | */
|
275 | get closed(): boolean {
|
276 | return !this.browser().browserContexts().includes(this);
|
277 | }
|
278 |
|
279 | /**
|
280 | * Identifier for this {@link BrowserContext | browser context}.
|
281 | */
|
282 | get id(): string | undefined {
|
283 | return undefined;
|
284 | }
|
285 |
|
286 | /** @internal */
|
287 | override [disposeSymbol](): void {
|
288 | return void this.close().catch(debugError);
|
289 | }
|
290 |
|
291 | /** @internal */
|
292 | [asyncDisposeSymbol](): Promise<void> {
|
293 | return this.close();
|
294 | }
|
295 | }
|