UNPKG

11.7 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 {Connection} = require('./Connection');
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():Promise)=} closeCallback
31 */
32 constructor(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {
33 super();
34 this._ignoreHTTPSErrors = ignoreHTTPSErrors;
35 this._defaultViewport = defaultViewport;
36 this._process = process;
37 this._screenshotTaskQueue = new TaskQueue();
38 this._connection = connection;
39 this._closeCallback = closeCallback || new Function();
40
41 this._defaultContext = new BrowserContext(this._connection, this, null);
42 /** @type {Map<string, BrowserContext>} */
43 this._contexts = new Map();
44 for (const contextId of contextIds)
45 this._contexts.set(contextId, new BrowserContext(this._connection, this, contextId));
46
47 /** @type {Map<string, Target>} */
48 this._targets = new Map();
49 this._connection.on(Connection.Events.Disconnected, () => this.emit(Browser.Events.Disconnected));
50 this._connection.on('Target.targetCreated', this._targetCreated.bind(this));
51 this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
52 this._connection.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
53 }
54
55 /**
56 * @return {?Puppeteer.ChildProcess}
57 */
58 process() {
59 return this._process;
60 }
61
62 /**
63 * @return {!Promise<!BrowserContext>}
64 */
65 async createIncognitoBrowserContext() {
66 const {browserContextId} = await this._connection.send('Target.createBrowserContext');
67 const context = new BrowserContext(this._connection, this, browserContextId);
68 this._contexts.set(browserContextId, context);
69 return context;
70 }
71
72 /**
73 * @return {!Array<!BrowserContext>}
74 */
75 browserContexts() {
76 return [this._defaultContext, ...Array.from(this._contexts.values())];
77 }
78
79 /**
80 * @return {!BrowserContext}
81 */
82 defaultBrowserContext() {
83 return this._defaultContext;
84 }
85
86 /**
87 * @param {?string} contextId
88 */
89 async _disposeContext(contextId) {
90 await this._connection.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined});
91 this._contexts.delete(contextId);
92 }
93
94 /**
95 * @param {!Puppeteer.Connection} connection
96 * @param {!Array<string>} contextIds
97 * @param {boolean} ignoreHTTPSErrors
98 * @param {?Puppeteer.Viewport} defaultViewport
99 * @param {?Puppeteer.ChildProcess} process
100 * @param {function()=} closeCallback
101 */
102 static async create(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {
103 const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback);
104 await connection.send('Target.setDiscoverTargets', {discover: true});
105 return browser;
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(Browser.Events.TargetCreated, target);
122 context.emit(BrowserContext.Events.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(Browser.Events.TargetDestroyed, target);
136 target.browserContext().emit(BrowserContext.Events.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(Browser.Events.TargetChanged, target);
151 target.browserContext().emit(BrowserContext.Events.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 */
199 async waitForTarget(predicate, options = {}) {
200 const {
201 timeout = 30000
202 } = options;
203 const existingTarget = this.targets().find(predicate);
204 if (existingTarget)
205 return existingTarget;
206 let resolve;
207 const targetPromise = new Promise(x => resolve = x);
208 this.on(Browser.Events.TargetCreated, check);
209 this.on(Browser.Events.TargetChanged, check);
210 try {
211 if (!timeout)
212 return await targetPromise;
213 return await helper.waitWithTimeout(targetPromise, 'target', timeout);
214 } finally {
215 this.removeListener(Browser.Events.TargetCreated, check);
216 this.removeListener(Browser.Events.TargetChanged, check);
217 }
218
219 /**
220 * @param {!Target} target
221 */
222 function check(target) {
223 if (predicate(target))
224 resolve(target);
225 }
226 }
227
228 /**
229 * @return {!Promise<!Array<!Puppeteer.Page>>}
230 */
231 async pages() {
232 const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
233 // Flatten array.
234 return contextPages.reduce((acc, x) => acc.concat(x), []);
235 }
236
237 /**
238 * @return {!Promise<string>}
239 */
240 async version() {
241 const version = await this._getVersion();
242 return version.product;
243 }
244
245 /**
246 * @return {!Promise<string>}
247 */
248 async userAgent() {
249 const version = await this._getVersion();
250 return version.userAgent;
251 }
252
253 async close() {
254 await this._closeCallback.call(null);
255 this.disconnect();
256 }
257
258 disconnect() {
259 this._connection.dispose();
260 }
261
262 /**
263 * @return {!Promise<!Object>}
264 */
265 _getVersion() {
266 return this._connection.send('Browser.getVersion');
267 }
268}
269
270/** @enum {string} */
271Browser.Events = {
272 TargetCreated: 'targetcreated',
273 TargetDestroyed: 'targetdestroyed',
274 TargetChanged: 'targetchanged',
275 Disconnected: 'disconnected'
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 */
302 waitForTarget(predicate, options) {
303 return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options);
304 }
305
306 /**
307 * @return {!Promise<!Array<!Puppeteer.Page>>}
308 */
309 async pages() {
310 const pages = await Promise.all(
311 this.targets()
312 .filter(target => target.type() === 'page')
313 .map(target => target.page())
314 );
315 return pages.filter(page => !!page);
316 }
317
318 /**
319 * @return {boolean}
320 */
321 isIncognito() {
322 return !!this._id;
323 }
324
325 /**
326 * @param {string} origin
327 * @param {!Array<string>} permissions
328 */
329 async overridePermissions(origin, permissions) {
330 const webPermissionToProtocol = new Map([
331 ['geolocation', 'geolocation'],
332 ['midi', 'midi'],
333 ['notifications', 'notifications'],
334 ['push', 'push'],
335 ['camera', 'videoCapture'],
336 ['microphone', 'audioCapture'],
337 ['background-sync', 'backgroundSync'],
338 ['ambient-light-sensor', 'sensors'],
339 ['accelerometer', 'sensors'],
340 ['gyroscope', 'sensors'],
341 ['magnetometer', 'sensors'],
342 ['accessibility-events', 'accessibilityEvents'],
343 ['clipboard-read', 'clipboardRead'],
344 ['clipboard-write', 'clipboardWrite'],
345 ['payment-handler', 'paymentHandler'],
346 // chrome-specific permissions we have.
347 ['midi-sysex', 'midiSysex'],
348 ]);
349 permissions = permissions.map(permission => {
350 const protocolPermission = webPermissionToProtocol.get(permission);
351 if (!protocolPermission)
352 throw new Error('Unknown permission: ' + permission);
353 return protocolPermission;
354 });
355 await this._connection.send('Browser.grantPermissions', {origin, browserContextId: this._id || undefined, permissions});
356 }
357
358 async clearPermissionOverrides() {
359 await this._connection.send('Browser.resetPermissions', {browserContextId: this._id || undefined});
360 }
361
362 /**
363 * @return {!Promise<!Puppeteer.Page>}
364 */
365 newPage() {
366 return this._browser._createPageInContext(this._id);
367 }
368
369 /**
370 * @return {!Browser}
371 */
372 browser() {
373 return this._browser;
374 }
375
376 async close() {
377 assert(this._id, 'Non-incognito profiles cannot be closed!');
378 await this._browser._disposeContext(this._id);
379 }
380}
381
382/** @enum {string} */
383BrowserContext.Events = {
384 TargetCreated: 'targetcreated',
385 TargetDestroyed: 'targetdestroyed',
386 TargetChanged: 'targetchanged',
387};
388
389helper.tracePublicAPI(BrowserContext);
390helper.tracePublicAPI(Browser);
391
392module.exports = {Browser, BrowserContext};