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.
|
- `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.
|
||||||
|
@ -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;
|
||||||
|
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');
|
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() {
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user