Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: webContents.loadURL returns a promise #15855

Merged
merged 7 commits into from Dec 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/api/browser-window.md
Expand Up @@ -1236,7 +1236,11 @@ Captures a snapshot of the page within `rect`. Omitting `rect` will capture the
* `postData` ([UploadRawData[]](structures/upload-raw-data.md) | [UploadFile[]](structures/upload-file.md) | [UploadBlob[]](structures/upload-blob.md)) (optional)
* `baseURLForDataURL` String (optional) - Base url (with trailing path separator) for files to be loaded by the data url. This is needed only if the specified `url` is a data url and needs to load other files.

Same as `webContents.loadURL(url[, options])`.
Returns `Promise<void>` - the promise will resolve when the page has finished loading
(see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects
if the page fails to load (see [`did-fail-load`](web-contents.md#event-did-fail-load)).

Same as [`webContents.loadURL(url[, options])`](web-contents.md#contentsloadurlurl-options).

The `url` can be a remote address (e.g. `http://`) or a path to a local
HTML file using the `file://` protocol.
Expand Down Expand Up @@ -1276,6 +1280,10 @@ win.loadURL('http://localhost:8000/post', {
* `search` String (optional) - Passed to `url.format()`.
* `hash` String (optional) - Passed to `url.format()`.

Returns `Promise<void>` - the promise will resolve when the page has finished loading
(see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects
if the page fails to load (see [`did-fail-load`](web-contents.md#event-did-fail-load)).

Same as `webContents.loadFile`, `filePath` should be a path to an HTML
file relative to the root of your application. See the `webContents` docs
for more information.
Expand Down
9 changes: 9 additions & 0 deletions docs/api/web-contents.md
Expand Up @@ -697,6 +697,11 @@ Custom value can be returned by setting `event.returnValue`.
* `postData` ([UploadRawData[]](structures/upload-raw-data.md) | [UploadFile[]](structures/upload-file.md) | [UploadBlob[]](structures/upload-blob.md)) (optional)
* `baseURLForDataURL` String (optional) - Base url (with trailing path separator) for files to be loaded by the data url. This is needed only if the specified `url` is a data url and needs to load other files.

Returns `Promise<void>` - the promise will resolve when the page has finished loading
(see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects
if the page fails to load (see
[`did-fail-load`](web-contents.md#event-did-fail-load)).

Loads the `url` in the window. The `url` must contain the protocol prefix,
e.g. the `http://` or `file://`. If the load should bypass http cache then
use the `pragma` header to achieve it.
Expand All @@ -715,6 +720,10 @@ webContents.loadURL('https://github.com', options)
* `search` String (optional) - Passed to `url.format()`.
* `hash` String (optional) - Passed to `url.format()`.

Returns `Promise<void>` - the promise will resolve when the page has finished loading
(see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects
if the page fails to load (see [`did-fail-load`](web-contents.md#event-did-fail-load)).

Loads the given file in the window, `filePath` should be a path to
an HTML file relative to the root of your application. For instance
an app structure like this:
Expand Down
59 changes: 58 additions & 1 deletion lib/browser/navigation-controller.js
Expand Up @@ -55,9 +55,66 @@ const NavigationController = (function () {
if (options == null) {
options = {}
}
const p = new Promise((resolve, reject) => {
const resolveAndCleanup = () => {
removeListeners()
resolve()
}
const rejectAndCleanup = (errorCode, errorDescription, url) => {
const err = new Error(`${errorDescription} (${errorCode}) loading '${url}'`)
Object.assign(err, { errno: errorCode, code: errorDescription, url })
removeListeners()
reject(err)
}
const finishListener = () => {
resolveAndCleanup()
}
const failListener = (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
if (isMainFrame) {
rejectAndCleanup(errorCode, errorDescription, validatedURL)
}
}

let navigationStarted = false
const navigationListener = (event, url, isSameDocument, isMainFrame, frameProcessId, frameRoutingId, navigationId) => {
if (isMainFrame) {
if (navigationStarted) {
// the webcontents has started another unrelated navigation in the
// main frame (probably from the app calling `loadURL` again); reject
// the promise
return rejectAndCleanup(-3, 'ERR_ABORTED', url)
}
navigationStarted = true
}
}
const stopLoadingListener = () => {
// By the time we get here, either 'finish' or 'fail' should have fired
// if the navigation occurred. However, in some situations (e.g. when
// attempting to load a page with a bad scheme), loading will stop
// without emitting finish or fail. In this case, we reject the promise
// with a generic failure.
// TODO(jeremy): enumerate all the cases in which this can happen. If
// the only one is with a bad scheme, perhaps ERR_INVALID_ARGUMENT
// would be more appropriate.
rejectAndCleanup(-2, 'ERR_FAILED', url)
}
const removeListeners = () => {
this.webContents.removeListener('did-finish-load', finishListener)
this.webContents.removeListener('did-fail-load', failListener)
this.webContents.removeListener('did-start-navigation', navigationListener)
this.webContents.removeListener('did-stop-loading', stopLoadingListener)
}
this.webContents.on('did-finish-load', finishListener)
this.webContents.on('did-fail-load', failListener)
this.webContents.on('did-start-navigation', navigationListener)
this.webContents.on('did-stop-loading', stopLoadingListener)
})
// Add a no-op rejection handler to silence the unhandled rejection error.
p.catch(() => {})
this.pendingIndex = -1
this.webContents._loadURL(url, options)
return this.webContents.emit('load-url', url, options)
this.webContents.emit('load-url', url, options)
return p
}

NavigationController.prototype.getURL = function () {
Expand Down
5 changes: 1 addition & 4 deletions spec/api-ipc-renderer-spec.js
Expand Up @@ -5,7 +5,6 @@ const dirtyChai = require('dirty-chai')
const http = require('http')
const path = require('path')
const { closeWindow } = require('./window-helpers')
const { emittedOnce } = require('./events-helpers')

const { expect } = chai
chai.use(dirtyChai)
Expand Down Expand Up @@ -229,9 +228,7 @@ describe('ipc renderer module', () => {
describe('ipcRenderer.on', () => {
it('is not used for internals', async () => {
w = new BrowserWindow({ show: false })
w.loadURL('about:blank')

await emittedOnce(w.webContents, 'did-finish-load')
await w.loadURL('about:blank')

const script = `require('electron').ipcRenderer.eventNames()`
const result = await w.webContents.executeJavaScript(script)
Expand Down
3 changes: 2 additions & 1 deletion spec/api-remote-spec.js
Expand Up @@ -429,9 +429,10 @@ describe('remote module', () => {
})

it('emits unhandled rejection events in the renderer process', (done) => {
window.addEventListener('unhandledrejection', function (event) {
window.addEventListener('unhandledrejection', function handler (event) {
event.preventDefault()
assert.strictEqual(event.reason.message, 'rejected')
window.removeEventListener('unhandledrejection', handler)
done()
})

Expand Down
107 changes: 101 additions & 6 deletions spec/api-web-contents-spec.js
Expand Up @@ -39,6 +39,105 @@ describe('webContents module', () => {

afterEach(() => closeWindow(w).then(() => { w = null }))

describe('loadURL() promise API', () => {
it('resolves when done loading', async () => {
await expect(w.loadURL('about:blank')).to.eventually.be.fulfilled
})

it('resolves when done loading a file URL', async () => {
await expect(w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))).to.eventually.be.fulfilled
})

it('rejects when failing to load a file URL', async () => {
await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected
.and.have.property('code', 'ERR_FILE_NOT_FOUND')
})

it('rejects when loading fails due to DNS not resolved', async () => {
await expect(w.loadURL('https://err.name.not.resolved')).to.eventually.be.rejected
.and.have.property('code', 'ERR_NAME_NOT_RESOLVED')
})

it('rejects when navigation is cancelled due to a bad scheme', async () => {
await expect(w.loadURL('bad-scheme://foo')).to.eventually.be.rejected
.and.have.property('code', 'ERR_FAILED')
})

it('sets appropriate error information on rejection', async () => {
let err
try {
await w.loadURL('file:non-existent')
} catch (e) {
err = e
}
expect(err).not.to.be.null()
expect(err.code).to.eql('ERR_FILE_NOT_FOUND')
expect(err.errno).to.eql(-6)
expect(err.url).to.eql(process.platform === 'win32' ? 'file://non-existent/' : 'file:///non-existent')
})

it('rejects if the load is aborted', async () => {
const s = http.createServer((req, res) => { /* never complete the request */ })
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address()
const p = expect(w.loadURL(`http://127.0.0.1:${port}`)).to.eventually.be.rejectedWith(Error, /ERR_ABORTED/)
// load a different file before the first load completes, causing the
// first load to be aborted.
await w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
await p
s.close()
})

it("doesn't reject when a subframe fails to load", async () => {
let resp = null
const s = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.write('<iframe src="http://err.name.not.resolved"></iframe>')
resp = res
// don't end the response yet
})
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address()
const p = new Promise(resolve => {
w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
if (!isMainFrame) {
resolve()
}
})
})
const main = w.loadURL(`http://127.0.0.1:${port}`)
await p
resp.end()
await main
s.close()
})

it("doesn't resolve when a subframe loads", async () => {
let resp = null
const s = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.write('<iframe src="data:text/html,hi"></iframe>')
resp = res
// don't end the response yet
})
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address()
const p = new Promise(resolve => {
w.webContents.on('did-frame-finish-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
if (!isMainFrame) {
resolve()
}
})
})
const main = w.loadURL(`http://127.0.0.1:${port}`)
await p
resp.destroy() // cause the main request to fail
await expect(main).to.eventually.be.rejected
.and.have.property('errno', -355) // ERR_INCOMPLETE_CHUNKED_ENCODING
s.close()
})
})

describe('getAllWebContents() API', () => {
it('returns an array of web contents', (done) => {
w.webContents.on('devtools-opened', () => {
Expand Down Expand Up @@ -899,9 +998,7 @@ describe('webContents module', () => {
}
})

const p = emittedOnce(w.webContents, 'did-finish-load')
w.loadURL('about:blank')
await p
await w.loadURL('about:blank')

const filePath = path.join(remote.app.getPath('temp'), 'test.heapsnapshot')

Expand Down Expand Up @@ -931,9 +1028,7 @@ describe('webContents module', () => {
}
})

const p = emittedOnce(w.webContents, 'did-finish-load')
w.loadURL('about:blank')
await p
await w.loadURL('about:blank')

const promise = w.webContents.takeHeapSnapshot('')
return expect(promise).to.be.eventually.rejectedWith(Error, 'takeHeapSnapshot failed')
Expand Down