UNPKG

16.5 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');
21
22class Browser extends EventEmitter {
23 /**
24 * @param {!Puppeteer.Connection} connection
25 * @param {!Array<string>} contextIds
26 * @param {boolean} ignoreHTTPSErrors
27 * @param {?Puppeteer.Viewport} defaultViewport
28 * @param {?Puppeteer.ChildProcess} process
29 * @param {(function():Promise)=} closeCallback
30 */
31 constructor(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {
32 super();
33 this._ignoreHTTPSErrors = ignoreHTTPSErrors;
34 this._defaultViewport = defaultViewport;
35 this._process = process;
36 this._screenshotTaskQueue = new TaskQueue();
37 this._connection = connection;
38 this._closeCallback = closeCallback || new Function();
39
40 this._defaultContext = new BrowserContext(this, null);
41 /** @type {Map<string, BrowserContext>} */
42 this._contexts = new Map();
43 for (const contextId of contextIds)
44 this._contexts.set(contextId, new BrowserContext(this, contextId));
45
46 /** @type {Map<string, Target>} */
47 this._targets = new Map();
48 this._connection.setClosedCallback(() => {
49 this.emit(Browser.Events.Disconnected);
50 });
51 this._connection.on('Target.targetCreated', this._targetCreated.bind(this));
52 this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
53 this._connection.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
54 }
55
56 /**
57 * @return {?Puppeteer.ChildProcess}
58 */
59 process() {
60 return this._process;
61 }
62
63 /**
64 * @return {!Promise<!BrowserContext>}
65 */
66 /* async */ createIncognitoBrowserContext() {return (fn => {
67 const gen = fn.call(this);
68 return new Promise((resolve, reject) => {
69 function step(key, arg) {
70 let info, value;
71 try {
72 info = gen[key](arg);
73 value = info.value;
74 } catch (error) {
75 reject(error);
76 return;
77 }
78 if (info.done) {
79 resolve(value);
80 } else {
81 return Promise.resolve(value).then(
82 value => {
83 step('next', value);
84 },
85 err => {
86 step('throw', err);
87 });
88 }
89 }
90 return step('next');
91 });
92})(function*(){
93 const {browserContextId} = (yield this._connection.send('Target.createBrowserContext'));
94 const context = new BrowserContext(this, browserContextId);
95 this._contexts.set(browserContextId, context);
96 return context;
97 });}
98
99 /**
100 * @return {!Array<!BrowserContext>}
101 */
102 browserContexts() {
103 return [this._defaultContext, ...Array.from(this._contexts.values())];
104 }
105
106 /**
107 * @param {?string} contextId
108 */
109 /* async */ _disposeContext(contextId) {return (fn => {
110 const gen = fn.call(this);
111 return new Promise((resolve, reject) => {
112 function step(key, arg) {
113 let info, value;
114 try {
115 info = gen[key](arg);
116 value = info.value;
117 } catch (error) {
118 reject(error);
119 return;
120 }
121 if (info.done) {
122 resolve(value);
123 } else {
124 return Promise.resolve(value).then(
125 value => {
126 step('next', value);
127 },
128 err => {
129 step('throw', err);
130 });
131 }
132 }
133 return step('next');
134 });
135})(function*(){
136 (yield this._connection.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined}));
137 this._contexts.delete(contextId);
138 });}
139
140 /**
141 * @param {!Puppeteer.Connection} connection
142 * @param {!Array<string>} contextIds
143 * @param {boolean} ignoreHTTPSErrors
144 * @param {?Puppeteer.Viewport} defaultViewport
145 * @param {?Puppeteer.ChildProcess} process
146 * @param {function()=} closeCallback
147 */
148 static /* async */ create(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {return (fn => {
149 const gen = fn.call(this);
150 return new Promise((resolve, reject) => {
151 function step(key, arg) {
152 let info, value;
153 try {
154 info = gen[key](arg);
155 value = info.value;
156 } catch (error) {
157 reject(error);
158 return;
159 }
160 if (info.done) {
161 resolve(value);
162 } else {
163 return Promise.resolve(value).then(
164 value => {
165 step('next', value);
166 },
167 err => {
168 step('throw', err);
169 });
170 }
171 }
172 return step('next');
173 });
174})(function*(){
175 const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback);
176 (yield connection.send('Target.setDiscoverTargets', {discover: true}));
177 return browser;
178 });}
179
180 /**
181 * @param {!Protocol.Target.targetCreatedPayload} event
182 */
183 /* async */ _targetCreated(event) {return (fn => {
184 const gen = fn.call(this);
185 return new Promise((resolve, reject) => {
186 function step(key, arg) {
187 let info, value;
188 try {
189 info = gen[key](arg);
190 value = info.value;
191 } catch (error) {
192 reject(error);
193 return;
194 }
195 if (info.done) {
196 resolve(value);
197 } else {
198 return Promise.resolve(value).then(
199 value => {
200 step('next', value);
201 },
202 err => {
203 step('throw', err);
204 });
205 }
206 }
207 return step('next');
208 });
209})(function*(){
210 const targetInfo = event.targetInfo;
211 const {browserContextId} = targetInfo;
212 const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext;
213
214 const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotTaskQueue);
215 assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
216 this._targets.set(event.targetInfo.targetId, target);
217
218 if ((yield target._initializedPromise)) {
219 this.emit(Browser.Events.TargetCreated, target);
220 context.emit(BrowserContext.Events.TargetCreated, target);
221 }
222 });}
223
224 /**
225 * @param {{targetId: string}} event
226 */
227 /* async */ _targetDestroyed(event) {return (fn => {
228 const gen = fn.call(this);
229 return new Promise((resolve, reject) => {
230 function step(key, arg) {
231 let info, value;
232 try {
233 info = gen[key](arg);
234 value = info.value;
235 } catch (error) {
236 reject(error);
237 return;
238 }
239 if (info.done) {
240 resolve(value);
241 } else {
242 return Promise.resolve(value).then(
243 value => {
244 step('next', value);
245 },
246 err => {
247 step('throw', err);
248 });
249 }
250 }
251 return step('next');
252 });
253})(function*(){
254 const target = this._targets.get(event.targetId);
255 target._initializedCallback(false);
256 this._targets.delete(event.targetId);
257 target._closedCallback();
258 if ((yield target._initializedPromise)) {
259 this.emit(Browser.Events.TargetDestroyed, target);
260 target.browserContext().emit(BrowserContext.Events.TargetDestroyed, target);
261 }
262 });}
263
264 /**
265 * @param {!Protocol.Target.targetInfoChangedPayload} event
266 */
267 _targetInfoChanged(event) {
268 const target = this._targets.get(event.targetInfo.targetId);
269 assert(target, 'target should exist before targetInfoChanged');
270 const previousURL = target.url();
271 const wasInitialized = target._isInitialized;
272 target._targetInfoChanged(event.targetInfo);
273 if (wasInitialized && previousURL !== target.url()) {
274 this.emit(Browser.Events.TargetChanged, target);
275 target.browserContext().emit(BrowserContext.Events.TargetChanged, target);
276 }
277 }
278
279 /**
280 * @return {string}
281 */
282 wsEndpoint() {
283 return this._connection.url();
284 }
285
286 /**
287 * @return {!Promise<!Puppeteer.Page>}
288 */
289 /* async */ newPage() {return (fn => {
290 const gen = fn.call(this);
291 return new Promise((resolve, reject) => {
292 function step(key, arg) {
293 let info, value;
294 try {
295 info = gen[key](arg);
296 value = info.value;
297 } catch (error) {
298 reject(error);
299 return;
300 }
301 if (info.done) {
302 resolve(value);
303 } else {
304 return Promise.resolve(value).then(
305 value => {
306 step('next', value);
307 },
308 err => {
309 step('throw', err);
310 });
311 }
312 }
313 return step('next');
314 });
315})(function*(){
316 return this._defaultContext.newPage();
317 });}
318
319 /**
320 * @param {string} contextId
321 * @return {!Promise<!Puppeteer.Page>}
322 */
323 /* async */ _createPageInContext(contextId) {return (fn => {
324 const gen = fn.call(this);
325 return new Promise((resolve, reject) => {
326 function step(key, arg) {
327 let info, value;
328 try {
329 info = gen[key](arg);
330 value = info.value;
331 } catch (error) {
332 reject(error);
333 return;
334 }
335 if (info.done) {
336 resolve(value);
337 } else {
338 return Promise.resolve(value).then(
339 value => {
340 step('next', value);
341 },
342 err => {
343 step('throw', err);
344 });
345 }
346 }
347 return step('next');
348 });
349})(function*(){
350 const {targetId} = (yield this._connection.send('Target.createTarget', {url: 'about:blank', browserContextId: contextId || undefined}));
351 const target = (yield this._targets.get(targetId));
352 assert((yield target._initializedPromise), 'Failed to create target for page');
353 const page = (yield target.page());
354 return page;
355 });}
356
357 /**
358 * @return {!Array<!Target>}
359 */
360 targets() {
361 return Array.from(this._targets.values()).filter(target => target._isInitialized);
362 }
363
364 /**
365 * @return {!Promise<!Array<!Puppeteer.Page>>}
366 */
367 /* async */ pages() {return (fn => {
368 const gen = fn.call(this);
369 return new Promise((resolve, reject) => {
370 function step(key, arg) {
371 let info, value;
372 try {
373 info = gen[key](arg);
374 value = info.value;
375 } catch (error) {
376 reject(error);
377 return;
378 }
379 if (info.done) {
380 resolve(value);
381 } else {
382 return Promise.resolve(value).then(
383 value => {
384 step('next', value);
385 },
386 err => {
387 step('throw', err);
388 });
389 }
390 }
391 return step('next');
392 });
393})(function*(){
394 const contextPages = (yield Promise.all(this.browserContexts().map(context => context.pages())));
395 // Flatten array.
396 return contextPages.reduce((acc, x) => acc.concat(x), []);
397 });}
398
399 /**
400 * @return {!Promise<string>}
401 */
402 /* async */ version() {return (fn => {
403 const gen = fn.call(this);
404 return new Promise((resolve, reject) => {
405 function step(key, arg) {
406 let info, value;
407 try {
408 info = gen[key](arg);
409 value = info.value;
410 } catch (error) {
411 reject(error);
412 return;
413 }
414 if (info.done) {
415 resolve(value);
416 } else {
417 return Promise.resolve(value).then(
418 value => {
419 step('next', value);
420 },
421 err => {
422 step('throw', err);
423 });
424 }
425 }
426 return step('next');
427 });
428})(function*(){
429 const version = (yield this._getVersion());
430 return version.product;
431 });}
432
433 /**
434 * @return {!Promise<string>}
435 */
436 /* async */ userAgent() {return (fn => {
437 const gen = fn.call(this);
438 return new Promise((resolve, reject) => {
439 function step(key, arg) {
440 let info, value;
441 try {
442 info = gen[key](arg);
443 value = info.value;
444 } catch (error) {
445 reject(error);
446 return;
447 }
448 if (info.done) {
449 resolve(value);
450 } else {
451 return Promise.resolve(value).then(
452 value => {
453 step('next', value);
454 },
455 err => {
456 step('throw', err);
457 });
458 }
459 }
460 return step('next');
461 });
462})(function*(){
463 const version = (yield this._getVersion());
464 return version.userAgent;
465 });}
466
467 /* async */ close() {return (fn => {
468 const gen = fn.call(this);
469 return new Promise((resolve, reject) => {
470 function step(key, arg) {
471 let info, value;
472 try {
473 info = gen[key](arg);
474 value = info.value;
475 } catch (error) {
476 reject(error);
477 return;
478 }
479 if (info.done) {
480 resolve(value);
481 } else {
482 return Promise.resolve(value).then(
483 value => {
484 step('next', value);
485 },
486 err => {
487 step('throw', err);
488 });
489 }
490 }
491 return step('next');
492 });
493})(function*(){
494 (yield this._closeCallback.call(null));
495 this.disconnect();
496 });}
497
498 disconnect() {
499 this._connection.dispose();
500 }
501
502 /**
503 * @return {!Promise<!Object>}
504 */
505 _getVersion() {
506 return this._connection.send('Browser.getVersion');
507 }
508}
509
510/** @enum {string} */
511Browser.Events = {
512 TargetCreated: 'targetcreated',
513 TargetDestroyed: 'targetdestroyed',
514 TargetChanged: 'targetchanged',
515 Disconnected: 'disconnected'
516};
517
518class BrowserContext extends EventEmitter {
519 /**
520 * @param {!Browser} browser
521 * @param {?string} contextId
522 */
523 constructor(browser, contextId) {
524 super();
525 this._browser = browser;
526 this._id = contextId;
527 }
528
529 /**
530 * @return {!Array<!Target>} target
531 */
532 targets() {
533 return this._browser.targets().filter(target => target.browserContext() === this);
534 }
535
536 /**
537 * @return {!Promise<!Array<!Puppeteer.Page>>}
538 */
539 /* async */ pages() {return (fn => {
540 const gen = fn.call(this);
541 return new Promise((resolve, reject) => {
542 function step(key, arg) {
543 let info, value;
544 try {
545 info = gen[key](arg);
546 value = info.value;
547 } catch (error) {
548 reject(error);
549 return;
550 }
551 if (info.done) {
552 resolve(value);
553 } else {
554 return Promise.resolve(value).then(
555 value => {
556 step('next', value);
557 },
558 err => {
559 step('throw', err);
560 });
561 }
562 }
563 return step('next');
564 });
565})(function*(){
566 const pages = (yield Promise.all(
567 this.targets()
568 .filter(target => target.type() === 'page')
569 .map(target => target.page())
570 ));
571 return pages.filter(page => !!page);
572 });}
573
574 /**
575 * @return {boolean}
576 */
577 isIncognito() {
578 return !!this._id;
579 }
580
581 /**
582 * @return {!Promise<!Puppeteer.Page>}
583 */
584 newPage() {
585 return this._browser._createPageInContext(this._id);
586 }
587
588 /**
589 * @return {!Browser}
590 */
591 browser() {
592 return this._browser;
593 }
594
595 /* async */ close() {return (fn => {
596 const gen = fn.call(this);
597 return new Promise((resolve, reject) => {
598 function step(key, arg) {
599 let info, value;
600 try {
601 info = gen[key](arg);
602 value = info.value;
603 } catch (error) {
604 reject(error);
605 return;
606 }
607 if (info.done) {
608 resolve(value);
609 } else {
610 return Promise.resolve(value).then(
611 value => {
612 step('next', value);
613 },
614 err => {
615 step('throw', err);
616 });
617 }
618 }
619 return step('next');
620 });
621})(function*(){
622 assert(this._id, 'Non-incognito profiles cannot be closed!');
623 (yield this._browser._disposeContext(this._id));
624 });}
625}
626
627/** @enum {string} */
628BrowserContext.Events = {
629 TargetCreated: 'targetcreated',
630 TargetDestroyed: 'targetdestroyed',
631 TargetChanged: 'targetchanged',
632};
633
634helper.tracePublicAPI(BrowserContext);
635helper.tracePublicAPI(Browser);
636
637module.exports = {Browser, BrowserContext};