Implement waitFor which survives navigation (#99)
This patch implements page.waitFor method which survives navigation. References #89.
This commit is contained in:
parent
2fb1d9afca
commit
a63a0198de
14
docs/api.md
14
docs/api.md
@ -560,6 +560,20 @@ This is a shortcut for [page.mainFrame().url()](#frameurl)
|
||||
- `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.
|
||||
|
||||
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)
|
||||
- `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.
|
||||
|
@ -177,7 +177,7 @@ class FrameManager extends EventEmitter {
|
||||
* @param {!Frame} frame
|
||||
* @return {!Promise<undefined>}
|
||||
*/
|
||||
async _waitForInFrame(selector, frame) {
|
||||
async _waitForSelector(selector, frame) {
|
||||
|
||||
function code(selector) {
|
||||
if (document.querySelector(selector))
|
||||
@ -193,7 +193,7 @@ class FrameManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
});
|
||||
mo.observe(document.documentElement, {
|
||||
mo.observe(document, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
@ -240,6 +240,9 @@ class Frame {
|
||||
this._parentFrame = parentFrame;
|
||||
this._url = '';
|
||||
this._id = frameId;
|
||||
/** @type {!Set<!AwaitedElement>} */
|
||||
this._awaitedElements = new Set();
|
||||
|
||||
this._adoptPayload(payload);
|
||||
|
||||
/** @type {!Set<!Frame>} */
|
||||
@ -301,10 +304,15 @@ class Frame {
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<undefined>}
|
||||
* @return {!Promise}
|
||||
*/
|
||||
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._url = framePayload.url;
|
||||
for (let awaitedElement of this._awaitedElements)
|
||||
awaitedElement.startWaiting();
|
||||
}
|
||||
|
||||
_detach() {
|
||||
for (let awaitedElement of this._awaitedElements)
|
||||
awaitedElement.terminate(new Error('waitForSelector failed: frame got detached.'));
|
||||
this._detached = true;
|
||||
if (this._parentFrame)
|
||||
this._parentFrame._childFrames.delete(this);
|
||||
@ -360,4 +372,44 @@ class 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;
|
||||
|
21
test/test.js
21
test/test.js
@ -239,6 +239,27 @@ describe('Puppeteer', 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() {
|
||||
|
@ -7,6 +7,7 @@ const Browser = require('../../lib/Browser');
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||
|
||||
let EXCLUDE_CLASSES = new Set([
|
||||
'AwaitedElement',
|
||||
'Connection',
|
||||
'EmulationManager',
|
||||
'FrameManager',
|
||||
|
Loading…
Reference in New Issue
Block a user