Merge pull request #9178 from electron/visibilitystate

Let Chromium manage `document.visibilityState` and `document.hidden`
This commit is contained in:
Kevin Sawicki 2017-06-06 17:32:49 -07:00 committed by GitHub
commit 915f6a6f54
11 changed files with 230 additions and 91 deletions

View file

@ -186,24 +186,23 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches(
command_line->AppendSwitchASCII(::switches::kDisableBlinkFeatures, command_line->AppendSwitchASCII(::switches::kDisableBlinkFeatures,
disable_blink_features); disable_blink_features);
// The initial visibility state. if (guest_instance_id) {
NativeWindow* window = NativeWindow::FromWebContents(web_contents); // Webview `document.visibilityState` tracks window visibility so we need
// to let it know if the window happens to be hidden right now.
// Use embedder window for webviews
if (guest_instance_id && !window) {
auto manager = WebViewManager::GetWebViewManager(web_contents); auto manager = WebViewManager::GetWebViewManager(web_contents);
if (manager) { if (manager) {
auto embedder = manager->GetEmbedder(guest_instance_id); auto embedder = manager->GetEmbedder(guest_instance_id);
if (embedder) if (embedder) {
window = NativeWindow::FromWebContents(embedder); auto window = NativeWindow::FromWebContents(embedder);
if (window) {
const bool visible = window->IsVisible() && !window->IsMinimized();
if (!visible) {
command_line->AppendSwitch(switches::kHiddenPage);
}
}
}
} }
} }
if (window) {
bool visible = window->IsVisible() && !window->IsMinimized();
if (!visible) // Default state is visible.
command_line->AppendSwitch(switches::kHiddenPage);
}
} }
bool WebContentsPreferences::IsPreferenceEnabled( bool WebContentsPreferences::IsPreferenceEnabled(

View file

@ -97,6 +97,25 @@ child.once('ready-to-show', () => {
}) })
``` ```
### Page visibility
The [Page Visibility API][page-visibility-api] works as follows:
* On all platforms, the visibility state tracks whether the window is
hidden/minimized or not.
* Additionally, on macOS, the visibility state also tracks the window
occlusion state. If the window is occluded (i.e. fully covered) by another
window, the visibility state will be `hidden`. On other platforms, the
visibility state will be `hidden` only when the window is minimized or
explicitly hidden with `win.hide()`.
* If a `BrowserWindow` is created with `show: false`, the initial visibility
state will be `visible` despite the window actually being hidden.
* If `backgroundThrottling` is disabled, the visibility state will remain
`visible` even if the window is minimized, occluded, or hidden.
It is recommended that you pause expensive operations when the visibility
state is `hidden` in order to minimize power consumption.
### Platform notices ### Platform notices
* On macOS modal windows will be displayed as sheets attached to the parent window. * On macOS modal windows will be displayed as sheets attached to the parent window.
@ -294,7 +313,8 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
* `minimumFontSize` Integer (optional) - Defaults to `0`. * `minimumFontSize` Integer (optional) - Defaults to `0`.
* `defaultEncoding` String (optional) - Defaults to `ISO-8859-1`. * `defaultEncoding` String (optional) - Defaults to `ISO-8859-1`.
* `backgroundThrottling` Boolean (optional) - Whether to throttle animations and timers * `backgroundThrottling` Boolean (optional) - Whether to throttle animations and timers
when the page becomes background. Defaults to `true`. when the page becomes background. This also affects the
[Page Visibility API][#page-visibility]. Defaults to `true`.
* `offscreen` Boolean (optional) - Whether to enable offscreen rendering for the browser * `offscreen` Boolean (optional) - Whether to enable offscreen rendering for the browser
window. Defaults to `false`. See the window. Defaults to `false`. See the
[offscreen rendering tutorial](../tutorial/offscreen-rendering.md) for [offscreen rendering tutorial](../tutorial/offscreen-rendering.md) for
@ -1326,6 +1346,7 @@ removed in future Electron releases.
removed in future Electron releases. removed in future Electron releases.
[blink-feature-string]: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5?l=62 [blink-feature-string]: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5?l=62
[page-visibility-api]: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
[quick-look]: https://en.wikipedia.org/wiki/Quick_Look [quick-look]: https://en.wikipedia.org/wiki/Quick_Look
[vibrancy-docs]: https://developer.apple.com/reference/appkit/nsvisualeffectview?language=objc [vibrancy-docs]: https://developer.apple.com/reference/appkit/nsvisualeffectview?language=objc
[window-levels]: https://developer.apple.com/reference/appkit/nswindow/1664726-window_levels [window-levels]: https://developer.apple.com/reference/appkit/nswindow/1664726-window_levels

View file

@ -60,12 +60,11 @@ container when used with traditional and flexbox layouts (since v0.36.11). Pleas
do not overwrite the default `display:flex;` CSS property, unless specifying do not overwrite the default `display:flex;` CSS property, unless specifying
`display:inline-flex;` for inline layout. `display:inline-flex;` for inline layout.
`webview` has issues being hidden using the `hidden` attribute or using `display: none;`. `webview` has issues being hidden using the `hidden` attribute or using
It can cause unusual rendering behaviour within its child `browserplugin` object `display: none;`. It can cause unusual rendering behaviour within its child
and the web page is reloaded, when the `webview` is un-hidden, as opposed to just `browserplugin` object and the web page is reloaded when the `webview` is
becoming visible again. The recommended approach is to hide the `webview` using un-hidden. The recommended approach is to hide the `webview` using
CSS by zeroing the `width` & `height` and allowing the element to shrink to the 0px `visibility: hidden`.
dimensions via `flex`.
```html ```html
<style> <style>
@ -75,9 +74,7 @@ dimensions via `flex`.
height:480px; height:480px;
} }
webview.hide { webview.hide {
flex: 0 1; visibility: hidden;
width: 0px;
height: 0px;
} }
</style> </style>
``` ```

View file

@ -109,7 +109,6 @@ BrowserWindow.prototype._init = function () {
if (isVisible !== newState) { if (isVisible !== newState) {
isVisible = newState isVisible = newState
const visibilityState = isVisible ? 'visible' : 'hidden' const visibilityState = isVisible ? 'visible' : 'hidden'
this.webContents.send('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', visibilityState)
this.webContents.emit('-window-visibility-change', visibilityState) this.webContents.emit('-window-visibility-change', visibilityState)
} }
} }

View file

@ -281,7 +281,7 @@ const watchEmbedder = function (embedder) {
for (const guestInstanceId of Object.keys(guestInstances)) { for (const guestInstanceId of Object.keys(guestInstances)) {
const guestInstance = guestInstances[guestInstanceId] const guestInstance = guestInstances[guestInstanceId]
if (guestInstance.embedder === embedder) { if (guestInstance.embedder === embedder) {
guestInstance.guest.send('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', visibilityState) guestInstance.guest.send('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', visibilityState)
} }
} }
} }

View file

@ -175,27 +175,35 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNative
} }
}) })
// The initial visibilityState. if (guestInstanceId != null) {
let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible' // Webview `document.visibilityState` tracks window visibility (and ignores
// the actual <webview> element visibility) for backwards compatibility.
// See discussion in #9178.
//
// Note that this results in duplicate visibilitychange events (since
// Chromium also fires them) and potentially incorrect visibility change.
// We should reconsider this decision for Electron 2.0.
let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible'
// Subscribe to visibilityState changes. // Subscribe to visibilityState changes.
ipcRenderer.on('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', function (event, visibilityState) { ipcRenderer.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (event, visibilityState) {
if (cachedVisibilityState !== visibilityState) { if (cachedVisibilityState !== visibilityState) {
cachedVisibilityState = visibilityState cachedVisibilityState = visibilityState
document.dispatchEvent(new Event('visibilitychange')) document.dispatchEvent(new Event('visibilitychange'))
} }
}) })
// Make document.hidden and document.visibilityState return the correct value. // Make document.hidden and document.visibilityState return the correct value.
defineProperty(document, 'hidden', { defineProperty(document, 'hidden', {
get: function () { get: function () {
return cachedVisibilityState !== 'visible' return cachedVisibilityState !== 'visible'
} }
}) })
defineProperty(document, 'visibilityState', { defineProperty(document, 'visibilityState', {
get: function () { get: function () {
return cachedVisibilityState return cachedVisibilityState
} }
}) })
}
} }

View file

@ -1412,6 +1412,164 @@ describe('BrowserWindow module', function () {
}) })
}) })
describe('document.visibilityState/hidden', function () {
beforeEach(function () {
w.destroy()
})
function onVisibilityChange (callback) {
ipcMain.on('pong', function (event, visibilityState, hidden) {
if (event.sender.id === w.webContents.id) {
callback(visibilityState, hidden)
}
})
}
function onNextVisibilityChange (callback) {
ipcMain.once('pong', function (event, visibilityState, hidden) {
if (event.sender.id === w.webContents.id) {
callback(visibilityState, hidden)
}
})
}
afterEach(function () {
ipcMain.removeAllListeners('pong')
})
it('visibilityState is initially visible despite window being hidden', function (done) {
w = new BrowserWindow({ show: false, width: 100, height: 100 })
let readyToShow = false
w.once('ready-to-show', function () {
readyToShow = true
})
onNextVisibilityChange(function (visibilityState, hidden) {
assert.equal(readyToShow, false)
assert.equal(visibilityState, 'visible')
assert.equal(hidden, false)
done()
})
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
})
it('visibilityState changes when window is hidden', function (done) {
w = new BrowserWindow({width: 100, height: 100})
onNextVisibilityChange(function (visibilityState, hidden) {
assert.equal(visibilityState, 'visible')
assert.equal(hidden, false)
onNextVisibilityChange(function (visibilityState, hidden) {
assert.equal(visibilityState, 'hidden')
assert.equal(hidden, true)
done()
})
w.hide()
})
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
})
it('visibilityState changes when window is shown', function (done) {
w = new BrowserWindow({width: 100, height: 100})
onNextVisibilityChange(function (visibilityState, hidden) {
onVisibilityChange(function (visibilityState, hidden) {
if (!hidden) {
assert.equal(visibilityState, 'visible')
done()
}
})
w.hide()
w.show()
})
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
})
it('visibilityState changes when window is shown inactive', function (done) {
if (isCI && process.platform === 'win32') return done()
w = new BrowserWindow({width: 100, height: 100})
onNextVisibilityChange(function (visibilityState, hidden) {
onVisibilityChange(function (visibilityState, hidden) {
if (!hidden) {
assert.equal(visibilityState, 'visible')
done()
}
})
w.hide()
w.showInactive()
})
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
})
it('visibilityState changes when window is minimized', function (done) {
if (isCI && process.platform === 'linux') return done()
w = new BrowserWindow({width: 100, height: 100})
onNextVisibilityChange(function (visibilityState, hidden) {
assert.equal(visibilityState, 'visible')
assert.equal(hidden, false)
onNextVisibilityChange(function (visibilityState, hidden) {
assert.equal(visibilityState, 'hidden')
assert.equal(hidden, true)
done()
})
w.minimize()
})
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
})
it('visibilityState remains visible if backgroundThrottling is disabled', function (done) {
w = new BrowserWindow({
show: false,
width: 100,
height: 100,
webPreferences: {
backgroundThrottling: false
}
})
onNextVisibilityChange(function (visibilityState, hidden) {
assert.equal(visibilityState, 'visible')
assert.equal(hidden, false)
onNextVisibilityChange(function (visibilityState, hidden) {
done(new Error(`Unexpected visibility change event. visibilityState: ${visibilityState} hidden: ${hidden}`))
})
})
w.once('show', () => {
w.once('hide', () => {
w.once('show', () => {
done()
})
w.show()
})
w.hide()
})
w.show()
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
})
})
describe('new-window event', function () { describe('new-window event', function () {
if (isCI && process.platform === 'darwin') { if (isCI && process.platform === 'darwin') {
return return
@ -2318,9 +2476,7 @@ describe('BrowserWindow module', function () {
typeofArrayPush: 'number', typeofArrayPush: 'number',
typeofFunctionApply: 'boolean', typeofFunctionApply: 'boolean',
typeofPreloadExecuteJavaScriptProperty: 'number', typeofPreloadExecuteJavaScriptProperty: 'number',
typeofOpenedWindow: 'object', typeofOpenedWindow: 'object'
documentHidden: true,
documentVisibilityState: 'hidden'
} }
} }

View file

@ -48,35 +48,6 @@ describe('chromium feature', function () {
}) })
}) })
describe('document.hidden', function () {
var url = 'file://' + fixtures + '/pages/document-hidden.html'
it('is set correctly when window is not shown', function (done) {
w = new BrowserWindow({
show: false
})
w.webContents.once('ipc-message', function (event, args) {
assert.deepEqual(args, ['hidden', true])
done()
})
w.loadURL(url)
})
it('is set correctly when window is inactive', function (done) {
if (isCI && process.platform === 'win32') return done()
w = new BrowserWindow({
show: false
})
w.webContents.once('ipc-message', function (event, args) {
assert.deepEqual(args, ['hidden', false])
done()
})
w.showInactive()
w.loadURL(url)
})
})
xdescribe('navigator.webkitGetUserMedia', function () { xdescribe('navigator.webkitGetUserMedia', function () {
it('calls its callbacks', function (done) { it('calls its callbacks', function (done) {
navigator.webkitGetUserMedia({ navigator.webkitGetUserMedia({

View file

@ -19,9 +19,7 @@
typeofArrayPush: typeof Array.prototype.push, typeofArrayPush: typeof Array.prototype.push,
typeofFunctionApply: typeof Function.prototype.apply, typeofFunctionApply: typeof Function.prototype.apply,
typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty, typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty,
typeofOpenedWindow: typeof opened, typeofOpenedWindow: typeof opened
documentHidden: document.hidden,
documentVisibilityState: document.visibilityState
}, '*') }, '*')
</script> </script>
</head> </head>

View file

@ -1,7 +0,0 @@
<html>
<body>
<script type="text/javascript" charset="utf-8">
require('electron').ipcRenderer.send('hidden', document.hidden);
</script>
</body>
</html>

View file

@ -6,7 +6,6 @@ const {ipcRenderer, remote} = require('electron')
const {app, session, getGuestWebContents, ipcMain, BrowserWindow, webContents} = remote const {app, session, getGuestWebContents, ipcMain, BrowserWindow, webContents} = remote
const {closeWindow} = require('./window-helpers') const {closeWindow} = require('./window-helpers')
const isCI = remote.getGlobal('isCi')
const nativeModulesEnabled = remote.getGlobal('nativeModulesEnabled') const nativeModulesEnabled = remote.getGlobal('nativeModulesEnabled')
describe('<webview> tag', function () { describe('<webview> tag', function () {
@ -485,9 +484,7 @@ describe('<webview> tag', function () {
typeofArrayPush: 'number', typeofArrayPush: 'number',
typeofFunctionApply: 'boolean', typeofFunctionApply: 'boolean',
typeofPreloadExecuteJavaScriptProperty: 'number', typeofPreloadExecuteJavaScriptProperty: 'number',
typeofOpenedWindow: 'object', typeofOpenedWindow: 'object'
documentHidden: isCI,
documentVisibilityState: isCI ? 'hidden' : 'visible'
} }
}) })
done() done()