UNPKG

11.4 kBJavaScriptView Raw
1/**
2 * Copyright 2017 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17const { helper, assert } = require('./helper');
18const {Target} = require('./Target');
19const EventEmitter = require('events');
20const {TaskQueue} = require('./TaskQueue');
21const {Events} = require('./Events');
22
23class Browser extends EventEmitter {
24 /**
25 * @param {!Puppeteer.Connection} connection
26 * @param {!Array<string>} contextIds
27 * @param {boolean} ignoreHTTPSErrors
28 * @param {?Puppeteer.Viewport} defaultViewport
29 * @param {?Puppeteer.ChildProcess} process
30 * @param {function()=} closeCallback
31 */
32 static async create(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {
33 const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback);
34 await connection.send('Target.setDiscoverTargets', {discover: true});
35 return browser;
36 }
37
38 /**
39 * @param {!Puppeteer.Connection} connection
40 * @param {!Array<string>} contextIds
41 * @param {boolean} ignoreHTTPSErrors
42 * @param {?Puppeteer.Viewport} defaultViewport
43 * @param {?Puppeteer.ChildProcess} process
44 * @param {(function():Promise)=} closeCallback
45 */
46 constructor(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {
47 super();
48 this._ignoreHTTPSErrors = ignoreHTTPSErrors;
49 this._defaultViewport = defaultViewport;
50 this._process = process;
51 this._screenshotTaskQueue = new TaskQueue();
52 this._connection = connection;
53 this._closeCallback = closeCallback || new Function();
54
55 this._defaultContext = new BrowserContext(this._connection, this, null);
56 /** @type {Map<string, BrowserContext>} */
57 this._contexts = new Map();
58 for (const contextId of contextIds)
59 this._contexts.set(contextId, new BrowserContext(this._connection, this, contextId));
60
61 /** @type {Map<string, Target>} */
62 this._targets = new Map();
63 this._connection.on(Events.Connection.Disconnected, () => this.emit(Events.Browser.Disconnected));
64 this._connection.on('Target.targetCreated', this._targetCreated.bind(this));
65 this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
66 this._connection.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
67 }
68
69 /**
70 * @return {?Puppeteer.ChildProcess}
71 */
72 process() {
73 return this._process;
74 }
75
76 /**
77 * @return {!Promise<!BrowserContext>}
78 */
79 async createIncognitoBrowserContext() {
80 const {browserContextId} = await this._connection.send('Target.createBrowserContext');
81 const context = new BrowserContext(this._connection, this, browserContextId);
82 this._contexts.set(browserContextId, context);
83 return context;
84 }
85
86 /**
87 * @return {!Array<!BrowserContext>}
88 */
89 browserContexts() {
90 return [this._defaultContext, ...Array.from(this._contexts.values())];
91 }
92
93 /**
94 * @return {!BrowserContext}
95 */
96 defaultBrowserContext() {
97 return this._defaultContext;
98 }
99
100 /**
101 * @param {?string} contextId
102 */
103 async _disposeContext(contextId) {
104 await this._connection.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined});
105 this._contexts.delete(contextId);
106 }
107
108 /**
109 * @param {!Protocol.Target.targetCreatedPayload} event
110 */
111 async _targetCreated(event) {
112 const targetInfo = event.targetInfo;
113 const {browserContextId} = targetInfo;
114 const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext;
115
116 const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotTaskQueue);
117 assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
118 this._targets.set(event.targetInfo.targetId, target);
119
120 if (await target._initializedPromise) {
121 this.emit(Events.Browser.TargetCreated, target);
122 context.emit(Events.BrowserContext.TargetCreated, target);
123 }
124 }
125
126 /**
127 * @param {{targetId: string}} event
128 */
129 async _targetDestroyed(event) {
130 const target = this._targets.get(event.targetId);
131 target._initializedCallback(false);
132 this._targets.delete(event.targetId);
133 target._closedCallback();
134 if (await target._initializedPromise) {
135 this.emit(Events.Browser.TargetDestroyed, target);
136 target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target);
137 }
138 }
139
140 /**
141 * @param {!Protocol.Target.targetInfoChangedPayload} event
142 */
143 _targetInfoChanged(event) {
144 const target = this._targets.get(event.targetInfo.targetId);
145 assert(target, 'target should exist before targetInfoChanged');
146 const previousURL = target.url();
147 const wasInitialized = target._isInitialized;
148 target._targetInfoChanged(event.targetInfo);
149 if (wasInitialized && previousURL !== target.url()) {
150 this.emit(Events.Browser.TargetChanged, target);
151 target.browserContext().emit(Events.BrowserContext.TargetChanged, target);
152 }
153 }
154
155 /**
156 * @return {string}
157 */
158 wsEndpoint() {
159 return this._connection.url();
160 }
161
162 /**
163 * @return {!Promise<!Puppeteer.Page>}
164 */
165 async newPage() {
166 return this._defaultContext.newPage();
167 }
168
169 /**
170 * @param {?string} contextId
171 * @return {!Promise<!Puppeteer.Page>}
172 */
173 async _createPageInContext(contextId) {
174 const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank', browserContextId: contextId || undefined});
175 const target = await this._targets.get(targetId);
176 assert(await target._initializedPromise, 'Failed to create target for page');
177 const page = await target.page();
178 return page;
179 }
180
181 /**
182 * @return {!Array<!Target>}
183 */
184 targets() {
185 return Array.from(this._targets.values()).filter(target => target._isInitialized);
186 }
187
188 /**
189 * @return {!Target}
190 */
191 target() {
192 return this.targets().find(target => target.type() === 'browser');
193 }
194
195 /**
196 * @param {function(!Target):boolean} predicate
197 * @param {{timeout?: number}=} options
198 * @return {!Promise<!Target>}
199 */
200 async waitForTarget(predicate, options = {}) {
201 const {
202 timeout = 30000
203 } = options;
204 const existingTarget = this.targets().find(predicate);
205 if (existingTarget)
206 return existingTarget;
207 let resolve;
208 const targetPromise = new Promise(x => resolve = x);
209 this.on(Events.Browser.TargetCreated, check);
210 this.on(Events.Browser.TargetChanged, check);
211 try {
212 if (!timeout)
213 return await targetPromise;
214 return await helper.waitWithTimeout(targetPromise, 'target', timeout);
215 } finally {
216 this.removeListener(Events.Browser.TargetCreated, check);
217 this.removeListener(Events.Browser.TargetChanged, check);
218 }
219
220 /**
221 * @param {!Target} target
222 */
223 function check(target) {
224 if (predicate(target))
225 resolve(target);
226 }
227 }
228
229 /**
230 * @return {!Promise<!Array<!Puppeteer.Page>>}
231 */
232 async pages() {
233 const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
234 // Flatten array.
235 return contextPages.reduce((acc, x) => acc.concat(x), []);
236 }
237
238 /**
239 * @return {!Promise<string>}
240 */
241 async version() {
242 const version = await this._getVersion();
243 return version.product;
244 }
245
246 /**
247 * @return {!Promise<string>}
248 */
249 async userAgent() {
250 const version = await this._getVersion();
251 return version.userAgent;
252 }
253
254 async close() {
255 await this._closeCallback.call(null);
256 this.disconnect();
257 }
258
259 disconnect() {
260 this._connection.dispose();
261 }
262
263 /**
264 * @return {boolean}
265 */
266 isConnected() {
267 return !this._connection._closed;
268 }
269
270 /**
271 * @return {!Promise<!Object>}
272 */
273 _getVersion() {
274 return this._connection.send('Browser.getVersion');
275 }
276}
277
278class BrowserContext extends EventEmitter {
279 /**
280 * @param {!Puppeteer.Connection} connection
281 * @param {!Browser} browser
282 * @param {?string} contextId
283 */
284 constructor(connection, browser, contextId) {
285 super();
286 this._connection = connection;
287 this._browser = browser;
288 this._id = contextId;
289 }
290
291 /**
292 * @return {!Array<!Target>} target
293 */
294 targets() {
295 return this._browser.targets().filter(target => target.browserContext() === this);
296 }
297
298 /**
299 * @param {function(!Target):boolean} predicate
300 * @param {{timeout?: number}=} options
301 * @return {!Promise<!Target>}
302 */
303 waitForTarget(predicate, options) {
304 return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options);
305 }
306
307 /**
308 * @return {!Promise<!Array<!Puppeteer.Page>>}
309 */
310 async pages() {
311 const pages = await Promise.all(
312 this.targets()
313 .filter(target => target.type() === 'page')
314 .map(target => target.page())
315 );
316 return pages.filter(page => !!page);
317 }
318
319 /**
320 * @return {boolean}
321 */
322 isIncognito() {
323 return !!this._id;
324 }
325
326 /**
327 * @param {string} origin
328 * @param {!Array<string>} permissions
329 */
330 async overridePermissions(origin, permissions) {
331 const webPermissionToProtocol = new Map([
332 ['geolocation', 'geolocation'],
333 ['midi', 'midi'],
334 ['notifications', 'notifications'],
335 ['push', 'push'],
336 ['camera', 'videoCapture'],
337 ['microphone', 'audioCapture'],
338 ['background-sync', 'backgroundSync'],
339 ['ambient-light-sensor', 'sensors'],
340 ['accelerometer', 'sensors'],
341 ['gyroscope', 'sensors'],
342 ['magnetometer', 'sensors'],
343 ['accessibility-events', 'accessibilityEvents'],
344 ['clipboard-read', 'clipboardRead'],
345 ['clipboard-write', 'clipboardWrite'],
346 ['payment-handler', 'paymentHandler'],
347 // chrome-specific permissions we have.
348 ['midi-sysex', 'midiSysex'],
349 ]);
350 permissions = permissions.map(permission => {
351 const protocolPermission = webPermissionToProtocol.get(permission);
352 if (!protocolPermission)
353 throw new Error('Unknown permission: ' + permission);
354 return protocolPermission;
355 });
356 await this._connection.send('Browser.grantPermissions', {origin, browserContextId: this._id || undefined, permissions});
357 }
358
359 async clearPermissionOverrides() {
360 await this._connection.send('Browser.resetPermissions', {browserContextId: this._id || undefined});
361 }
362
363 /**
364 * @return {!Promise<!Puppeteer.Page>}
365 */
366 newPage() {
367 return this._browser._createPageInContext(this._id);
368 }
369
370 /**
371 * @return {!Browser}
372 */
373 browser() {
374 return this._browser;
375 }
376
377 async close() {
378 assert(this._id, 'Non-incognito profiles cannot be closed!');
379 await this._browser._disposeContext(this._id);
380 }
381}
382
383module.exports = {Browser, BrowserContext};