Skip to content

Commit

Permalink
feat(page): introduce Page.ensureNavigation
Browse files Browse the repository at this point in the history
This patch adds a new method - `Page.ensureNavigation` - that
completes once the last running navigation hits the conditions
defined in `waitUntil` clause.

This comes handy when awaiting loading of a pages spawned as a result
of ctrl-click and other events.

References puppeteer#3083
  • Loading branch information
aslushnikov committed Dec 12, 2018
1 parent c90392b commit 7004854
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 4 deletions.
40 changes: 36 additions & 4 deletions lib/FrameManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ class FrameManager extends EventEmitter {
return watcher.navigationResponse();
}

/**
* @param {!Puppeteer.Frame} frame
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
* @return {!Promise<?Puppeteer.Response>}
*/
async ensureFrameNavigation(frame, options = {}) {
assertNoLegacyNavigationOptions(options);
const {
waitUntil = ['load'],
timeout = this._defaultNavigationTimeout,
} = options;
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
watcher.sameDocumentNavigationPromise(),
watcher.newDocumentNavigationPromise()
]);
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
}

/**
* @param {!Protocol.Page.lifecycleEventPayload} event
*/
Expand Down Expand Up @@ -532,17 +555,25 @@ class Frame {
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
*/
async setContent(html, options = {}) {
const {
waitUntil = ['load'],
timeout = 30000,
} = options;
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
await this.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
await this.ensureNavigation(options);
}

/**
* @param {string} html
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
*/
async ensureNavigation(options = {}) {
const {
waitUntil = ['load'],
timeout = 30000,
} = options;
const watcher = new LifecycleWatcher(this._frameManager, this, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
Expand Down Expand Up @@ -1201,6 +1232,7 @@ class LifecycleWatcher {
this._terminationPromise = new Promise(fulfill => {
this._terminationCallback = fulfill;
});
this._checkLifecycleComplete();
}

/**
Expand Down
7 changes: 7 additions & 0 deletions lib/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,13 @@ class Page extends EventEmitter {
return await this._frameManager.mainFrame().waitForNavigation(options);
}

/**
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
*/
async ensureNavigation(options = {}) {
await this._frameManager.mainFrame().ensureNavigation(options);
}

/**
* @param {(string|Function)} urlOrPredicate
* @param {!{timeout?: number}=} options
Expand Down
54 changes: 54 additions & 0 deletions test/navigation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,58 @@ module.exports.addTests = function({testRunner, expect}) {
await navigationPromise;
});
});

describe('Page.ensureNavigation', function() {
it('should work', async({page, server}) => {
// Hold on one-style.css to prevent load event from happenning.
let serverResponse = null;
server.setRoute('/one-style.css', (req, res) => serverResponse = res);
await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'});

// Make sure ensureNavigation fulfills once one-style.css is dispatched.
let complete = false;
const promise = page.ensureNavigation().then(() => complete = true);
expect(complete).toBe(false);
serverResponse.end('');
await promise;
expect(complete).toBe(true);
});
it('should work when opening a new page', async({page, context, server}) => {
// Hold on one-style.css to prevent load event from happenning.
let serverResponse = null;
server.setRoute('/one-style.css', (req, res) => serverResponse = res);

// Setup a link that opens a new tab
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<a target=_blank href='/one-style.html'>onestyle</a>`);
const [target] = await Promise.all([
new Promise(resolve => context.once('targetcreated', resolve)),
page.click('a'),
]);
const newPage = await target.page();
// Make sure the new tab is loaded up to 'domcontentloaded'
await newPage.ensureNavigation({waitUntil: 'domcontentloaded'});

// Make sure ensureNavigation fulfills once one-style.css is dispatched.
let complete = false;
const promise = page.ensureNavigation().then(() => complete = true);
expect(complete).toBe(false);
serverResponse.end('');
await promise;
expect(complete).toBe(true);
});
it('should return if navigation completed', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
await page.ensureNavigation({waitUntil: 'load'});
});
it('should fail when exceeding maximum navigation timeout', async({page, server}) => {
// Hang for request to the empty.html
server.setRoute('/one-style.css', (req, res) => { });
await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'});
let error = null;
await page.ensureNavigation({timeout: 1}).catch(e => error = e);
expect(error.message).toContain('Navigation Timeout Exceeded: 1ms');
expect(error).toBeInstanceOf(TimeoutError);
});
});
};

0 comments on commit 7004854

Please sign in to comment.