Skip to content

Commit

Permalink
feat(Browser): introduce Browser.pages() (#554)
Browse files Browse the repository at this point in the history
This patch:
- introduces Target class that represents any inspectable target, such as service worker or page
- emits events when targets come and go
- introduces target.page() to instantiate a page from a target

Fixes #386, fixes #443.
  • Loading branch information
JoelEinbinder authored and aslushnikov committed Oct 18, 2017
1 parent 273c733 commit 32398d1
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 15 deletions.
46 changes: 46 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@
* [puppeteer.executablePath()](#puppeteerexecutablepath)
* [puppeteer.launch([options])](#puppeteerlaunchoptions)
- [class: Browser](#class-browser)
* [event: 'targetchanged'](#event-targetchanged)
* [event: 'targetcreated'](#event-targetcreated)
* [event: 'targetdestroyed'](#event-targetdestroyed)
* [browser.close()](#browserclose)
* [browser.disconnect()](#browserdisconnect)
* [browser.newPage()](#browsernewpage)
* [browser.pages()](#browserpages)
* [browser.targets()](#browsertargets)
* [browser.version()](#browserversion)
* [browser.wsEndpoint()](#browserwsendpoint)
- [class: Page](#class-page)
Expand Down Expand Up @@ -175,6 +180,10 @@
* [response.status](#responsestatus)
* [response.text()](#responsetext)
* [response.url](#responseurl)
- [class: Target](#class-target)
* [target.page()](#targetpage)
* [target.type()](#targettype)
* [target.url()](#targeturl)

<!-- tocstop -->

Expand Down Expand Up @@ -278,6 +287,21 @@ puppeteer.launch().then(async browser => {
});
```

#### event: 'targetchanged'
- <[Target]>

Emitted when the url of a target changes.

#### event: 'targetcreated'
- <[Target]>

Emitted when a target is created, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browser.newPage`](#browsernewpage).

#### event: 'targetdestroyed'
- <[Target]>

Emitted when a target is destroyed, for example when a page is closed.

#### browser.close()
- returns: <[Promise]>

Expand All @@ -290,6 +314,12 @@ Disconnects Puppeteer from the browser, but leaves the Chromium process running.
#### browser.newPage()
- returns: <[Promise]<[Page]>> Promise which resolves to a new [Page] object.

#### browser.pages()
- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages.

#### browser.targets()
- returns: <[Array]<[Target]>> An array of all active targets.

#### browser.version()
- returns: <[Promise]<[string]>> For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is similar to `Chrome/61.0.3153.0`.

Expand Down Expand Up @@ -1896,6 +1926,21 @@ Contains the status code of the response (e.g., 200 for a success).

Contains the URL of the response.

### class: Target

#### target.page()
- returns: <[Promise]<[Page]>>

If the target is not of type `"page"`, returns `null`.

#### target.type()
- returns: <[string]>

Identifies what kind of target this is. Can be `"page"`, `"service_worker"`, or `"other"`.

#### target.url()
- returns: <[string]>

[Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array"
[boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean"
[Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer"
Expand Down Expand Up @@ -1927,3 +1972,4 @@ Contains the URL of the response.
[UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail"
[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable"
[Touchscreen]: #class-touchscreen "Touchscreen"
[Target]: #class-target "Target"
149 changes: 147 additions & 2 deletions lib/Browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,54 @@ class Browser extends EventEmitter {
this._screenshotTaskQueue = new TaskQueue();
this._connection = connection;
this._closeCallback = closeCallback || new Function();
/** @type {Map<string, Target>} */
this._targets = new Map();
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);
}

/**
Expand All @@ -45,8 +93,25 @@ class Browser extends EventEmitter {
*/
async newPage() {
const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank'});
const client = await this._connection.createSession(targetId);
return await Page.create(client, this._ignoreHTTPSErrors, this._appMode, this._screenshotTaskQueue);
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<!Target>}
*/
targets() {
return Array.from(this._targets.values()).filter(target => target._isInitialized);
}

/**
* @return {!Promise<!Array<!Page>>}
*/
async pages() {
const pages = await Promise.all(this.targets().map(target => target.page()));
return pages.filter(page => !!page);
}

/**
Expand All @@ -67,6 +132,13 @@ class Browser extends EventEmitter {
}
}

/** @enum {string} */
Browser.Events = {
TargetCreated: 'targetcreated',
TargetDestroyed: 'targetdestroyed',
TargetChanged: 'targetchanged'
};

helper.tracePublicAPI(Browser);

class TaskQueue {
Expand All @@ -84,4 +156,77 @@ class TaskQueue {
return result;
}
}

class Target {
/**
* @param {!Browser} browser
* @param {!Target.TargetInfo} targetInfo
*/
constructor(browser, targetInfo) {
this._browser = browser;
this._targetInfo = targetInfo;
/** @type {?Promise<!Page>} */
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<?Page>}
*/
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 };
19 changes: 18 additions & 1 deletion lib/FrameManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ const readFileAsync = helper.promisify(fs.readFile);
class FrameManager extends EventEmitter {
/**
* @param {!Puppeteer.Session} client
* @param {{frame: Object, childFrames: ?Array}} frameTree
* @param {!Puppeteer.Page} page
*/
constructor(client, page) {
constructor(client, frameTree, page) {
super();
this._client = client;
this._page = page;
Expand All @@ -40,6 +41,22 @@ class FrameManager extends EventEmitter {
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId));
this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context));

this._handleFrameTree(frameTree);
}

/**
* @param {{frame: Object, childFrames: ?Array}} frameTree
*/
_handleFrameTree(frameTree) {
if (frameTree.frame.parentId)
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId);
this._onFrameNavigated(frameTree.frame);
if (!frameTree.childFrames)
return;

for (const child of frameTree.childFrames)
this._handleFrameTree(child);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions lib/Launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class Launcher {
const connectionDelay = options.slowMo || 0;
const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, options.timeout || 30 * 1000);
connection = await Connection.create(browserWSEndpoint, connectionDelay);
return new Browser(connection, options, killChrome);
return Browser.create(connection, options, killChrome);
} catch (e) {
killChrome();
throw e;
Expand Down Expand Up @@ -179,7 +179,7 @@ class Launcher {
*/
static async connect(options = {}) {
const connection = await Connection.create(options.browserWSEndpoint);
return new Browser(connection, options, () => connection.send('Browser.close'));
return Browser.create(connection, options, () => connection.send('Browser.close'));
}
}

Expand Down
14 changes: 9 additions & 5 deletions lib/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,35 +37,39 @@ class Page extends EventEmitter {
* @return {!Promise<!Page>}
*/
static async create(client, ignoreHTTPSErrors, appMode, screenshotTaskQueue) {

await client.send('Page.enable');
const {frameTree} = await client.send('Page.getResourceTree');
const page = new Page(client, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);

await Promise.all([
client.send('Network.enable', {}),
client.send('Page.enable', {}),
client.send('Runtime.enable', {}),
client.send('Security.enable', {}),
client.send('Performance.enable', {})
]);
if (ignoreHTTPSErrors)
await client.send('Security.setOverrideCertificateErrors', {override: true});
const page = new Page(client, ignoreHTTPSErrors, screenshotTaskQueue);
await page.goto('about:blank');
// Initialize default page size.
if (!appMode)
await page.setViewport({width: 800, height: 600});

return page;
}

/**
* @param {!Puppeteer.Session} client
* @param {{frame: Object, childFrames: ?Array}} frameTree
* @param {boolean} ignoreHTTPSErrors
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
*/
constructor(client, ignoreHTTPSErrors, screenshotTaskQueue) {
constructor(client, frameTree, ignoreHTTPSErrors, screenshotTaskQueue) {
super();
this._client = client;
this._keyboard = new Keyboard(client);
this._mouse = new Mouse(client, this._keyboard);
this._touchscreen = new Touchscreen(client, this._keyboard);
this._frameManager = new FrameManager(client, this);
this._frameManager = new FrameManager(client, frameTree, this);
this._networkManager = new NetworkManager(client);
this._emulationManager = new EmulationManager(client);
this._tracing = new Tracing(client);
Expand Down
Empty file added test/assets/sw.js
Empty file.
5 changes: 5 additions & 0 deletions test/golden/reconnect-nested-frames.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
http://localhost:8907/frames/nested-frames.html
http://localhost:8907/frames/two-frames.html
http://localhost:8907/frames/frame.html
http://localhost:8907/frames/frame.html
http://localhost:8907/frames/frame.html
Loading

0 comments on commit 32398d1

Please sign in to comment.