mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
fix(Page): support anchor navigation (#2338)
This patch fixes puppeteer navigation primitives to work with same-document navigation. Same-document navigation happens when document's URL is changed, but document instance is not re-created. Some common scenarios for same-document navigation are: - History API - anchor navigation With this patch: - pptr starts dispatching `framenavigated` event when frame's URL gets changed due to same-document navigation - `page.waitForNavigation` now works with same-document navigation - `page.goBack()` and `page.goForward()` are handled correctly. Fixes #257.
This commit is contained in:
parent
2b95774af9
commit
fafd156d7b
@ -39,6 +39,7 @@ class FrameManager extends EventEmitter {
|
||||
|
||||
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
|
||||
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
|
||||
this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
|
||||
this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId));
|
||||
this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context));
|
||||
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
|
||||
@ -144,6 +145,19 @@ class FrameManager extends EventEmitter {
|
||||
this.emit(FrameManager.Events.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} frameId
|
||||
* @param {string} url
|
||||
*/
|
||||
_onFrameNavigatedWithinDocument(frameId, url) {
|
||||
const frame = this._frames.get(frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._navigatedWithinDocument(url);
|
||||
this.emit(FrameManager.Events.FrameNavigatedWithinDocument, frame);
|
||||
this.emit(FrameManager.Events.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} frameId
|
||||
*/
|
||||
@ -217,7 +231,8 @@ FrameManager.Events = {
|
||||
FrameAttached: 'frameattached',
|
||||
FrameNavigated: 'framenavigated',
|
||||
FrameDetached: 'framedetached',
|
||||
LifecycleEvent: 'lifecycleevent'
|
||||
LifecycleEvent: 'lifecycleevent',
|
||||
FrameNavigatedWithinDocument: 'framenavigatedwithindocument'
|
||||
};
|
||||
|
||||
/**
|
||||
@ -750,6 +765,13 @@ class Frame {
|
||||
this._url = framePayload.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
*/
|
||||
_navigatedWithinDocument(url) {
|
||||
this._url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} loaderId
|
||||
* @param {string} name
|
||||
|
@ -43,8 +43,10 @@ class NavigatorWatcher {
|
||||
this._frame = frame;
|
||||
this._initialLoaderId = frame._loaderId;
|
||||
this._timeout = timeout;
|
||||
this._hasSameDocumentNavigation = false;
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._frameManager, FrameManager.Events.LifecycleEvent, this._checkLifecycleComplete.bind(this)),
|
||||
helper.addEventListener(this._frameManager, FrameManager.Events.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)),
|
||||
helper.addEventListener(this._frameManager, FrameManager.Events.FrameDetached, this._checkLifecycleComplete.bind(this))
|
||||
];
|
||||
|
||||
@ -78,9 +80,19 @@ class NavigatorWatcher {
|
||||
return this._navigationPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Frame} frame
|
||||
*/
|
||||
_navigatedWithinDocument(frame) {
|
||||
if (frame !== this._frame)
|
||||
return;
|
||||
this._hasSameDocumentNavigation = true;
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_checkLifecycleComplete() {
|
||||
// We expect navigation to commit.
|
||||
if (this._frame._loaderId === this._initialLoaderId)
|
||||
if (this._frame._loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation)
|
||||
return;
|
||||
if (!checkLifecycle(this._frame, this._expectedLifecycle))
|
||||
return;
|
||||
|
@ -422,6 +422,14 @@ module.exports.addTests = function({testRunner, expect}) {
|
||||
expect(detachedFrames.length).toBe(1);
|
||||
expect(detachedFrames[0].isDetached()).toBe(true);
|
||||
});
|
||||
it('should send "framenavigated" when navigating on anchor URLs', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await Promise.all([
|
||||
page.goto(server.EMPTY_PAGE + '#foo'),
|
||||
utils.waitEvent(page, 'framenavigated')
|
||||
]);
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
|
||||
});
|
||||
it('should persist mainFrame on cross-process navigation', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const mainFrame = page.mainFrame();
|
||||
|
@ -613,6 +613,67 @@ module.exports.addTests = function({testRunner, expect, puppeteer, DeviceDescrip
|
||||
await bothFiredPromise;
|
||||
await navigationPromise;
|
||||
});
|
||||
it('should work with clicking on anchor links', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`<a href='#foobar'>foobar</a>`);
|
||||
await Promise.all([
|
||||
page.click('a'),
|
||||
page.waitForNavigation()
|
||||
]);
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar');
|
||||
});
|
||||
it('should work with history.pushState()', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<a onclick='javascript:pushState()'>SPA</a>
|
||||
<script>
|
||||
function pushState() { history.pushState({}, '', 'wow.html') }
|
||||
</script>
|
||||
`);
|
||||
await Promise.all([
|
||||
page.click('a'),
|
||||
page.waitForNavigation()
|
||||
]);
|
||||
expect(page.url()).toBe(server.PREFIX + '/wow.html');
|
||||
});
|
||||
it('should work with history.replaceState()', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<a onclick='javascript:replaceState()'>SPA</a>
|
||||
<script>
|
||||
function replaceState() { history.replaceState({}, '', '/replaced.html') }
|
||||
</script>
|
||||
`);
|
||||
await Promise.all([
|
||||
page.click('a'),
|
||||
page.waitForNavigation()
|
||||
]);
|
||||
expect(page.url()).toBe(server.PREFIX + '/replaced.html');
|
||||
});
|
||||
it('should work with DOM history.back()/history.forward()', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<a id=back onclick='javascript:goBack()'>back</a>
|
||||
<a id=forward onclick='javascript:goForward()'>forward</a>
|
||||
<script>
|
||||
function goBack() { history.back(); }
|
||||
function goForward() { history.forward(); }
|
||||
history.pushState({}, '', '/first.html');
|
||||
history.pushState({}, '', '/second.html');
|
||||
</script>
|
||||
`);
|
||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||
await Promise.all([
|
||||
page.click('a#back'),
|
||||
page.waitForNavigation()
|
||||
]);
|
||||
expect(page.url()).toBe(server.PREFIX + '/first.html');
|
||||
await Promise.all([
|
||||
page.click('a#forward'),
|
||||
page.waitForNavigation()
|
||||
]);
|
||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.goBack', function() {
|
||||
@ -631,6 +692,21 @@ module.exports.addTests = function({testRunner, expect, puppeteer, DeviceDescrip
|
||||
response = await page.goForward();
|
||||
expect(response).toBe(null);
|
||||
});
|
||||
it('should work with HistoryAPI', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.evaluate(() => {
|
||||
history.pushState({}, '', '/first.html');
|
||||
history.pushState({}, '', '/second.html');
|
||||
});
|
||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||
|
||||
await page.goBack();
|
||||
expect(page.url()).toBe(server.PREFIX + '/first.html');
|
||||
await page.goBack();
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||
await page.goForward();
|
||||
expect(page.url()).toBe(server.PREFIX + '/first.html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.exposeFunction', function() {
|
||||
|
@ -99,9 +99,6 @@ beforeEach(async({server, httpsServer}) => {
|
||||
httpsServer.reset();
|
||||
});
|
||||
|
||||
// Top-level tests that launch Browser themselves.
|
||||
require('./puppeteer.spec.js').addTests({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions});
|
||||
|
||||
describe('Page', function() {
|
||||
beforeAll(async state => {
|
||||
state.browser = await puppeteer.launch(defaultBrowserOptions);
|
||||
@ -136,6 +133,9 @@ describe('Page', function() {
|
||||
require('./tracing.spec.js').addTests({testRunner, expect});
|
||||
});
|
||||
|
||||
// Top-level tests that launch Browser themselves.
|
||||
require('./puppeteer.spec.js').addTests({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions});
|
||||
|
||||
if (process.env.COVERAGE) {
|
||||
describe('COVERAGE', function(){
|
||||
const coverage = helper.publicAPICoverage();
|
||||
|
Loading…
Reference in New Issue
Block a user