Implement waitFor which survives navigation (#99)

This patch implements page.waitFor method which survives navigation.

References #89.
This commit is contained in:
Andrey Lushnikov 2017-07-19 19:04:51 -07:00 committed by GitHub
parent 2fb1d9afca
commit a63a0198de
4 changed files with 92 additions and 4 deletions

View File

@ -560,6 +560,20 @@ This is a shortcut for [page.mainFrame().url()](#frameurl)
- `selector` <[string]> A query selector to wait for on the page. - `selector` <[string]> A query selector to wait for on the page.
- returns: <[Promise]> Promise which resolves when the element matching `selector` appears in the page. - returns: <[Promise]> Promise which resolves when the element matching `selector` appears in the page.
The `page.waitFor` successfully survives page navigations:
```js
const {Browser} = new require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page => {
let currentURL;
page.waitFor('img').then(() => console.log('First URL with image: ' + currentURL));
for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
await page.navigate(currentURL);
browser.close();
});
```
#### page.waitForNavigation(options) #### page.waitForNavigation(options)
- `options` <[Object]> Navigation parameters, same as in [page.navigate](#pagenavigateurl-options). - `options` <[Object]> Navigation parameters, same as in [page.navigate](#pagenavigateurl-options).
- returns: <[Promise]<[Response]>> Promise which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect. - returns: <[Promise]<[Response]>> Promise which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect.

View File

@ -177,7 +177,7 @@ class FrameManager extends EventEmitter {
* @param {!Frame} frame * @param {!Frame} frame
* @return {!Promise<undefined>} * @return {!Promise<undefined>}
*/ */
async _waitForInFrame(selector, frame) { async _waitForSelector(selector, frame) {
function code(selector) { function code(selector) {
if (document.querySelector(selector)) if (document.querySelector(selector))
@ -193,7 +193,7 @@ class FrameManager extends EventEmitter {
return; return;
} }
}); });
mo.observe(document.documentElement, { mo.observe(document, {
childList: true, childList: true,
subtree: true subtree: true
}); });
@ -240,6 +240,9 @@ class Frame {
this._parentFrame = parentFrame; this._parentFrame = parentFrame;
this._url = ''; this._url = '';
this._id = frameId; this._id = frameId;
/** @type {!Set<!AwaitedElement>} */
this._awaitedElements = new Set();
this._adoptPayload(payload); this._adoptPayload(payload);
/** @type {!Set<!Frame>} */ /** @type {!Set<!Frame>} */
@ -301,10 +304,15 @@ class Frame {
/** /**
* @param {string} selector * @param {string} selector
* @return {!Promise<undefined>} * @return {!Promise}
*/ */
async waitFor(selector) { async waitFor(selector) {
await this._frameManager._waitForInFrame(selector, this); const awaitedElement = new AwaitedElement(() => this._frameManager._waitForSelector(selector, this));
this._awaitedElements.add(awaitedElement);
let cleanup = () => this._awaitedElements.delete(awaitedElement);
awaitedElement.promise.then(cleanup, cleanup);
return awaitedElement.promise;
} }
/** /**
@ -349,9 +357,13 @@ class Frame {
}; };
this._name = framePayload.name; this._name = framePayload.name;
this._url = framePayload.url; this._url = framePayload.url;
for (let awaitedElement of this._awaitedElements)
awaitedElement.startWaiting();
} }
_detach() { _detach() {
for (let awaitedElement of this._awaitedElements)
awaitedElement.terminate(new Error('waitForSelector failed: frame got detached.'));
this._detached = true; this._detached = true;
if (this._parentFrame) if (this._parentFrame)
this._parentFrame._childFrames.delete(this); this._parentFrame._childFrames.delete(this);
@ -360,4 +372,44 @@ class Frame {
} }
helper.tracePublicAPI(Frame); helper.tracePublicAPI(Frame);
class AwaitedElement {
/**
* @param {function():!Promise} waitInPageCallback
*/
constructor(waitInPageCallback) {
this.promise = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
this._waitInPageCallback = waitInPageCallback;
this._waitPromise = null;
this.startWaiting();
}
/**
* @param {!Error} error
*/
terminate(error) {
this._reject(error);
this._waitTaskPromise = null;
}
startWaiting() {
let waitPromise = this._waitInPageCallback.call(null).then(finish.bind(this), finish.bind(this));
this._waitPromise = waitPromise;
/**
* @param {?Error} error
*/
function finish(error) {
if (this._waitPromise !== waitPromise)
return;
if (error)
this._reject(error);
else
this._resolve();
}
}
}
module.exports = FrameManager; module.exports = FrameManager;

View File

@ -239,6 +239,27 @@ describe('Puppeteer', function() {
expect(e.message).toContain('Evaluation failed: document.querySelector is not a function'); expect(e.message).toContain('Evaluation failed: document.querySelector is not a function');
} }
})); }));
it('should throw when frame is detached', SX(async function() {
await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE);
let frame = page.frames()[1];
let waitError = null;
let waitPromise = frame.waitFor('.box').catch(e => waitError = e);
await FrameUtils.detachFrame(page, 'frame1');
await waitPromise;
expect(waitError).toBeTruthy();
expect(waitError.message).toContain('waitForSelector failed: frame got detached.');
}));
it('should survive navigation', SX(async function() {
let boxFound = false;
let waitFor = page.waitFor('.box').then(() => boxFound = true);
await page.navigate(EMPTY_PAGE);
expect(boxFound).toBe(false);
await page.reload();
expect(boxFound).toBe(false);
await page.navigate(PREFIX + '/grid.html');
await waitFor;
expect(boxFound).toBe(true);
}));
}); });
describe('Page.Events.Console', function() { describe('Page.Events.Console', function() {

View File

@ -7,6 +7,7 @@ const Browser = require('../../lib/Browser');
const PROJECT_DIR = path.join(__dirname, '..', '..'); const PROJECT_DIR = path.join(__dirname, '..', '..');
let EXCLUDE_CLASSES = new Set([ let EXCLUDE_CLASSES = new Set([
'AwaitedElement',
'Connection', 'Connection',
'EmulationManager', 'EmulationManager',
'FrameManager', 'FrameManager',