/** * Copyright 2017 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const {helper} = require('./helper'); const Page = require('./Page'); const EventEmitter = require('events'); class Browser extends EventEmitter { /** * @param {!Puppeteer.Connection} connection * @param {!Object=} options * @param {(function():Promise)=} closeCallback */ constructor(connection, options = {}, closeCallback) { super(); this._ignoreHTTPSErrors = !!options.ignoreHTTPSErrors; this._appMode = !!options.appMode; this._screenshotTaskQueue = new TaskQueue(); this._connection = connection; this._closeCallback = closeCallback || new Function(); /** @type {Map} */ this._targets = new Map(); this._connection.setClosedCallback(() => { this.emit(Browser.Events.Disconnected); }); this._connection.on('Target.targetCreated', this._targetCreated.bind(this)); this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); this._connection.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); } /** * @param {!Puppeteer.Connection} connection * @param {boolean} ignoreHTTPSErrors * @param {function()=} closeCallback */ static async create(connection, ignoreHTTPSErrors, closeCallback) { const browser = new Browser(connection, ignoreHTTPSErrors, closeCallback); await connection.send('Target.setDiscoverTargets', {discover: true}); return browser; } /** * @param {{targetInfo: !Target.TargetInfo}} event */ async _targetCreated(event) { const target = new Target(this, event.targetInfo); console.assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated'); this._targets.set(event.targetInfo.targetId, target); if (await target._initializedPromise) this.emit(Browser.Events.TargetCreated, target); } /** * @param {{targetId: string}} event */ async _targetDestroyed(event) { const target = this._targets.get(event.targetId); target._initializedCallback(false); this._targets.delete(event.targetId); if (await target._initializedPromise) this.emit(Browser.Events.TargetDestroyed, target); } /** * @param {{targetInfo: !Target.TargetInfo}} event */ _targetInfoChanged(event) { const target = this._targets.get(event.targetInfo.targetId); console.assert(target, 'target should exist before targetInfoChanged'); target._targetInfoChanged(event.targetInfo); } /** * @return {string} */ wsEndpoint() { return this._connection.url(); } /** * @return {!Promise} */ async newPage() { const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank'}); const target = await this._targets.get(targetId); console.assert(await target._initializedPromise, 'Failed to create target for page'); const page = await target.page(); return page; } /** * @return {!Array} */ targets() { return Array.from(this._targets.values()).filter(target => target._isInitialized); } /** * @return {!Promise>} */ async pages() { const pages = await Promise.all(this.targets().map(target => target.page())); return pages.filter(page => !!page); } /** * @return {!Promise} */ async version() { const version = await this._connection.send('Browser.getVersion'); return version.product; } async close() { await this._closeCallback.call(null); this.disconnect(); } disconnect() { this._connection.dispose(); } } /** @enum {string} */ Browser.Events = { TargetCreated: 'targetcreated', TargetDestroyed: 'targetdestroyed', TargetChanged: 'targetchanged', Disconnected: 'disconnected' }; helper.tracePublicAPI(Browser); class TaskQueue { constructor() { this._chain = Promise.resolve(); } /** * @param {function()} task * @return {!Promise} */ postTask(task) { const result = this._chain.then(task); this._chain = result.catch(() => {}); return result; } } class Target { /** * @param {!Browser} browser * @param {!Target.TargetInfo} targetInfo */ constructor(browser, targetInfo) { this._browser = browser; this._targetInfo = targetInfo; /** @type {?Promise} */ this._pagePromise = null; this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill); this._isInitialized = this._targetInfo.type !== 'page' || this._targetInfo.url !== ''; if (this._isInitialized) this._initializedCallback(true); } /** * @return {!Promise} */ async page() { if (this._targetInfo.type === 'page' && !this._pagePromise) { this._pagePromise = this._browser._connection.createSession(this._targetInfo.targetId) .then(client => Page.create(client, this._browser._ignoreHTTPSErrors, this._browser._appMode, this._browser._screenshotTaskQueue)); } return this._pagePromise; } /** * @return {string} */ url() { return this._targetInfo.url; } /** * @return {"page"|"service_worker"|"other"} */ type() { const type = this._targetInfo.type; if (type === 'page' || type === 'service_worker') return type; return 'other'; } /** * @param {!Target.TargetInfo} targetInfo */ _targetInfoChanged(targetInfo) { const previousURL = this._targetInfo.url; this._targetInfo = targetInfo; if (!this._isInitialized && (this._targetInfo.type !== 'page' || this._targetInfo.url !== '')) { this._isInitialized = true; this._initializedCallback(true); return; } if (previousURL !== targetInfo.url) this._browser.emit(Browser.Events.TargetChanged, this); } } helper.tracePublicAPI(Target); /** * @typedef {Object} Target.TargetInfo * @property {string} type * @property {string} targetId * @property {string} title * @property {string} url * @property {boolean} attached */ module.exports = { Browser, TaskQueue };