diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index b05b8bfe217c8..e9888e459bca2 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -19,6 +19,36 @@ const contents = win.webContents console.log(contents) ``` +## Navigation Events + +Several events can be used to monitor navigations as they occur within a `webContents`. + +### Document Navigations + +When a `webContents` navigates to another page (as opposed to an [in-page navigation](web-contents.md#in-page-navigation)), the following events will be fired. + +* [`did-start-navigation`](web-contents.md#event-did-start-navigation) +* [`will-frame-navigate`](web-contents.md#event-will-frame-navigate) +* [`will-navigate`](web-contents.md#event-will-navigate) (only fired when main frame navigates) +* [`will-redirect`](web-contents.md#event-will-redirect) (only fired when a redirect happens during navigation) +* [`did-redirect-navigation`](web-contents.md#event-did-redirect-navigation) (only fired when a redirect happens during navigation) +* [`did-frame-navigate`](web-contents.md#event-did-frame-navigate) +* [`did-navigate`](web-contents.md#event-did-navigate) (only fired when main frame navigates) + +Subsequent events will not fire if `event.preventDefault()` is called on any of the cancellable events. + +### In-page Navigation + +In-page navigations don't cause the page to reload, but instead navigate to a location within the current page. These events are not cancellable. For an in-page navigations, the following events will fire in this order: + +* [`did-start-navigation`](web-contents.md#event-did-start-navigation) +* [`did-navigate-in-page`](web-contents.md#event-did-navigate-in-page) + +### Frame Navigation + +The [`will-navigate`](web-contents.md#event-will-navigate) and [`did-navigate`](web-contents.md#event-did-navigate) events only fire when the [mainFrame](web-contents.md#contentsmainframe-readonly) navigates. +If you want to also observe navigations in ``); + }); + + it('is triggered when navigating from file: to http:', async () => { + await w.loadFile(path.join(fixtures, 'api', 'blank.html')); + w.webContents.executeJavaScript(`location.href = ${JSON.stringify(url)}`); + const navigatedTo = await new Promise(resolve => { + w.webContents.once('will-frame-navigate', (e) => { + e.preventDefault(); + resolve(e.url); + }); + }); + expect(navigatedTo).to.equal(url); + expect(w.webContents.getURL()).to.match(/^file:/); + }); + + it('is triggered when navigating from about:blank to http:', async () => { + await w.loadURL('about:blank'); + w.webContents.executeJavaScript(`location.href = ${JSON.stringify(url)}`); + const navigatedTo = await new Promise(resolve => { + w.webContents.once('will-frame-navigate', (e) => { + e.preventDefault(); + resolve(e.url); + }); + }); + expect(navigatedTo).to.equal(url); + expect(w.webContents.getURL()).to.equal('about:blank'); + }); + + it('is triggered when a cross-origin iframe navigates _top', async () => { + await w.loadURL(`data:text/html,`); + await setTimeout(1000); + + let willFrameNavigateEmitted = false; + let isMainFrameValue; + w.webContents.on('will-frame-navigate', (event) => { + willFrameNavigateEmitted = true; + isMainFrameValue = event.isMainFrame; + }); + const didNavigatePromise = once(w.webContents, 'did-navigate'); + + w.webContents.debugger.attach('1.1'); + const targets = await w.webContents.debugger.sendCommand('Target.getTargets'); + const iframeTarget = targets.targetInfos.find((t: any) => t.type === 'iframe'); + const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', { + targetId: iframeTarget.targetId, + flatten: true + }); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + + await didNavigatePromise; + + expect(willFrameNavigateEmitted).to.be.true(); + expect(isMainFrameValue).to.be.true(); + }); + + it('is triggered when a cross-origin iframe navigates itself', async () => { + await w.loadURL(`data:text/html,`); + await setTimeout(1000); + + let willNavigateEmitted = false; + let isMainFrameValue; + w.webContents.on('will-frame-navigate', (event) => { + willNavigateEmitted = true; + isMainFrameValue = event.isMainFrame; + }); + const didNavigatePromise = once(w.webContents, 'did-frame-navigate'); + + w.webContents.debugger.attach('1.1'); + const targets = await w.webContents.debugger.sendCommand('Target.getTargets'); + const iframeTarget = targets.targetInfos.find((t: any) => t.type === 'iframe'); + const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', { + targetId: iframeTarget.targetId, + flatten: true + }); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + + await didNavigatePromise; + + expect(willNavigateEmitted).to.be.true(); + expect(isMainFrameValue).to.be.false(); + }); + + it('can cancel when a cross-origin iframe navigates itself', async () => { + + }); + }); + describe('will-redirect event', () => { let server = null as unknown as http.Server; let url = null as unknown as string; @@ -678,6 +877,179 @@ describe('BrowserWindow module', () => { w.loadURL(`${url}/navigate-302`); }); }); + + describe('ordering', () => { + let server = null as unknown as http.Server; + let url = null as unknown as string; + const navigationEvents = [ + 'did-start-navigation', + 'did-navigate-in-page', + 'will-frame-navigate', + 'will-navigate', + 'will-redirect', + 'did-redirect-navigation', + 'did-frame-navigate', + 'did-navigate' + ]; + before((done) => { + server = http.createServer((req, res) => { + if (req.url === '/navigate') { + res.end('navigate'); + } else if (req.url === '/redirect') { + res.end('redirect'); + } else if (req.url === '/redirect2') { + res.statusCode = 302; + res.setHeader('location', url); + res.end(); + } else if (req.url === '/in-page') { + res.end('redirect
'); + } else { + res.end(''); + } + }); + server.listen(0, '127.0.0.1', () => { + url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`; + done(); + }); + }); + it('for initial navigation, event order is consistent', async () => { + const firedEvents: string[] = []; + const expectedEventOrder = [ + 'did-start-navigation', + 'did-frame-navigate', + 'did-navigate' + ]; + const allEvents = Promise.all(navigationEvents.map(event => + once(w.webContents, event).then(() => firedEvents.push(event)) + )); + const timeout = setTimeout(1000); + w.loadURL(url); + await Promise.race([allEvents, timeout]); + expect(firedEvents).to.deep.equal(expectedEventOrder); + }); + + it('for second navigation, event order is consistent', async () => { + const firedEvents: string[] = []; + const expectedEventOrder = [ + 'did-start-navigation', + 'will-frame-navigate', + 'will-navigate', + 'did-frame-navigate', + 'did-navigate' + ]; + w.loadURL(`${url}navigate`); + await once(w.webContents, 'did-navigate'); + await setTimeout(1000); + navigationEvents.forEach(event => + once(w.webContents, event).then(() => firedEvents.push(event)) + ); + const navigationFinished = once(w.webContents, 'did-navigate'); + w.webContents.debugger.attach('1.1'); + const targets = await w.webContents.debugger.sendCommand('Target.getTargets'); + const pageTarget = targets.targetInfos.find((t: any) => t.type === 'page'); + const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', { + targetId: pageTarget.targetId, + flatten: true + }); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await navigationFinished; + expect(firedEvents).to.deep.equal(expectedEventOrder); + }); + + it('when navigating with redirection, event order is consistent', async () => { + const firedEvents: string[] = []; + const expectedEventOrder = [ + 'did-start-navigation', + 'will-frame-navigate', + 'will-navigate', + 'will-redirect', + 'did-redirect-navigation', + 'did-frame-navigate', + 'did-navigate' + ]; + w.loadURL(`${url}redirect`); + await once(w.webContents, 'did-navigate'); + await setTimeout(1000); + navigationEvents.forEach(event => + once(w.webContents, event).then(() => firedEvents.push(event)) + ); + const navigationFinished = once(w.webContents, 'did-navigate'); + w.webContents.debugger.attach('1.1'); + const targets = await w.webContents.debugger.sendCommand('Target.getTargets'); + const pageTarget = targets.targetInfos.find((t: any) => t.type === 'page'); + const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', { + targetId: pageTarget.targetId, + flatten: true + }); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await navigationFinished; + expect(firedEvents).to.deep.equal(expectedEventOrder); + }); + + it('when navigating in-page, event order is consistent', async () => { + const firedEvents: string[] = []; + const expectedEventOrder = [ + 'did-start-navigation', + 'did-navigate-in-page' + ]; + w.loadURL(`${url}in-page`); + await once(w.webContents, 'did-navigate'); + await setTimeout(1000); + navigationEvents.forEach(event => + once(w.webContents, event).then(() => firedEvents.push(event)) + ); + const navigationFinished = once(w.webContents, 'did-navigate-in-page'); + w.webContents.debugger.attach('1.1'); + const targets = await w.webContents.debugger.sendCommand('Target.getTargets'); + const pageTarget = targets.targetInfos.find((t: any) => t.type === 'page'); + const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', { + targetId: pageTarget.targetId, + flatten: true + }); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await navigationFinished; + expect(firedEvents).to.deep.equal(expectedEventOrder); + }); + }); }); } diff --git a/spec/fixtures/pages/webview-will-navigate-in-frame.html b/spec/fixtures/pages/webview-will-navigate-in-frame.html new file mode 100644 index 0000000000000..63f921e724278 --- /dev/null +++ b/spec/fixtures/pages/webview-will-navigate-in-frame.html @@ -0,0 +1,12 @@ + + + + + diff --git a/spec/webview-spec.ts b/spec/webview-spec.ts index 2a30247057dad..10a868e950ef4 100644 --- a/spec/webview-spec.ts +++ b/spec/webview-spec.ts @@ -1481,7 +1481,7 @@ describe(' tag', function () { }); describe('will-navigate event', () => { - it('emits when a url that leads to outside of the page is clicked', async () => { + it('emits when a url that leads to outside of the page is loaded', async () => { const { url } = await loadWebViewAndWaitForEvent(w, { src: `file://${fixtures}/pages/webview-will-navigate.html` }, 'will-navigate'); @@ -1490,6 +1490,47 @@ describe(' tag', function () { }); }); + describe('will-frame-navigate event', () => { + it('emits when a link that leads to outside of the page is loaded', async () => { + const { url, isMainFrame } = await loadWebViewAndWaitForEvent(w, { + src: `file://${fixtures}/pages/webview-will-navigate.html` + }, 'will-frame-navigate'); + expect(url).to.equal('http://host/'); + expect(isMainFrame).to.be.true(); + }); + + it('emits when a link within an iframe, which leads to outside of the page, is loaded', async () => { + await loadWebView(w, { + src: `file://${fixtures}/pages/webview-will-navigate-in-frame.html`, + nodeIntegration: '' + }); + + const { url, frameProcessId, frameRoutingId } = await w.executeJavaScript(` + new Promise((resolve, reject) => { + let hasFrameNavigatedOnce = false; + const webview = document.getElementById('webview'); + webview.addEventListener('will-frame-navigate', ({url, isMainFrame, frameProcessId, frameRoutingId}) => { + if (isMainFrame) return; + if (hasFrameNavigatedOnce) resolve({ + url, + isMainFrame, + frameProcessId, + frameRoutingId, + }); + + // First navigation is the initial iframe load within the + hasFrameNavigatedOnce = true; + }); + webview.executeJavaScript('loadSubframe()'); + }); + `); + + expect(url).to.equal('http://host/'); + expect(frameProcessId).to.be.a('number'); + expect(frameRoutingId).to.be.a('number'); + }); + }); + describe('did-navigate event', () => { it('emits when a url that leads to outside of the page is clicked', async () => { const pageUrl = url.pathToFileURL(path.join(fixtures, 'pages', 'webview-will-navigate.html')).toString();