fix: html fullscreen transitions stacking (#32905)

* fix: html fullscreen transitions stacking

* spec: move webview test to spec-main
This commit is contained in:
Shelley Vohr 2022-06-07 18:59:50 +02:00 committed by GitHub
parent f44ecb7f03
commit 16db5a112e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 184 additions and 61 deletions

View file

@ -3673,7 +3673,12 @@ void WebContents::EnumerateDirectory(
bool WebContents::IsFullscreenForTabOrPending( bool WebContents::IsFullscreenForTabOrPending(
const content::WebContents* source) { const content::WebContents* source) {
return html_fullscreen_; bool transition_fs = owner_window()
? owner_window()->fullscreen_transition_state() !=
NativeWindow::FullScreenTransitionState::NONE
: false;
return html_fullscreen_ || transition_fs;
} }
bool WebContents::TakeFocus(content::WebContents* source, bool reverse) { bool WebContents::TakeFocus(content::WebContents* source, bool reverse) {
@ -3995,9 +4000,8 @@ void WebContents::SetHtmlApiFullscreen(bool enter_fullscreen) {
? !web_preferences->ShouldDisableHtmlFullscreenWindowResize() ? !web_preferences->ShouldDisableHtmlFullscreenWindowResize()
: true; : true;
if (html_fullscreenable) { if (html_fullscreenable)
owner_window_->SetFullScreen(enter_fullscreen); owner_window_->SetFullScreen(enter_fullscreen);
}
UpdateHtmlApiFullscreen(enter_fullscreen); UpdateHtmlApiFullscreen(enter_fullscreen);
native_fullscreen_ = false; native_fullscreen_ = false;

View file

@ -718,6 +718,15 @@ std::string NativeWindow::GetAccessibleTitle() {
return base::UTF16ToUTF8(accessible_title_); return base::UTF16ToUTF8(accessible_title_);
} }
void NativeWindow::HandlePendingFullscreenTransitions() {
if (pending_transitions_.empty())
return;
bool next_transition = pending_transitions_.front();
pending_transitions_.pop();
SetFullScreen(next_transition);
}
// static // static
int32_t NativeWindow::next_id_ = 0; int32_t NativeWindow::next_id_ = 0;

View file

@ -7,6 +7,7 @@
#include <list> #include <list>
#include <memory> #include <memory>
#include <queue>
#include <string> #include <string>
#include <vector> #include <vector>
@ -317,6 +318,17 @@ class NativeWindow : public base::SupportsUserData,
observers_.RemoveObserver(obs); observers_.RemoveObserver(obs);
} }
enum class FullScreenTransitionState { ENTERING, EXITING, NONE };
// Handle fullscreen transitions.
void HandlePendingFullscreenTransitions();
void set_fullscreen_transition_state(FullScreenTransitionState state) {
fullscreen_transition_state_ = state;
}
FullScreenTransitionState fullscreen_transition_state() const {
return fullscreen_transition_state_;
}
views::Widget* widget() const { return widget_.get(); } views::Widget* widget() const { return widget_.get(); }
views::View* content_view() const { return content_view_; } views::View* content_view() const { return content_view_; }
@ -375,6 +387,10 @@ class NativeWindow : public base::SupportsUserData,
// The "titleBarStyle" option. // The "titleBarStyle" option.
TitleBarStyle title_bar_style_ = TitleBarStyle::kNormal; TitleBarStyle title_bar_style_ = TitleBarStyle::kNormal;
std::queue<bool> pending_transitions_;
FullScreenTransitionState fullscreen_transition_state_ =
FullScreenTransitionState::NONE;
private: private:
std::unique_ptr<views::Widget> widget_; std::unique_ptr<views::Widget> widget_;

View file

@ -8,7 +8,6 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#include <memory> #include <memory>
#include <queue>
#include <string> #include <string>
#include <vector> #include <vector>
@ -172,11 +171,6 @@ class NativeWindowMac : public NativeWindow,
void SetCollectionBehavior(bool on, NSUInteger flag); void SetCollectionBehavior(bool on, NSUInteger flag);
void SetWindowLevel(int level); void SetWindowLevel(int level);
enum class FullScreenTransitionState { ENTERING, EXITING, NONE };
// Handle fullscreen transitions.
void SetFullScreenTransitionState(FullScreenTransitionState state);
void HandlePendingFullscreenTransitions();
bool HandleDeferredClose(); bool HandleDeferredClose();
void SetHasDeferredWindowClose(bool defer_close) { void SetHasDeferredWindowClose(bool defer_close) {
has_deferred_window_close_ = defer_close; has_deferred_window_close_ = defer_close;
@ -247,13 +241,6 @@ class NativeWindowMac : public NativeWindow,
bool zoom_to_page_width_ = false; bool zoom_to_page_width_ = false;
absl::optional<gfx::Point> traffic_light_position_; absl::optional<gfx::Point> traffic_light_position_;
std::queue<bool> pending_transitions_;
FullScreenTransitionState fullscreen_transition_state() const {
return fullscreen_transition_state_;
}
FullScreenTransitionState fullscreen_transition_state_ =
FullScreenTransitionState::NONE;
// Trying to close an NSWindow during a fullscreen transition will cause the // Trying to close an NSWindow during a fullscreen transition will cause the
// window to lock up. Use this to track if CloseWindow was called during a // window to lock up. Use this to track if CloseWindow was called during a
// fullscreen transition, to defer the -[NSWindow close] call until the // fullscreen transition, to defer the -[NSWindow close] call until the

View file

@ -582,11 +582,6 @@ bool NativeWindowMac::IsVisible() {
return [window_ isVisible] && !occluded && !IsMinimized(); return [window_ isVisible] && !occluded && !IsMinimized();
} }
void NativeWindowMac::SetFullScreenTransitionState(
FullScreenTransitionState state) {
fullscreen_transition_state_ = state;
}
bool NativeWindowMac::IsEnabled() { bool NativeWindowMac::IsEnabled() {
return [window_ attachedSheet] == nil; return [window_ attachedSheet] == nil;
} }
@ -670,15 +665,6 @@ bool NativeWindowMac::IsMinimized() {
return [window_ isMiniaturized]; return [window_ isMiniaturized];
} }
void NativeWindowMac::HandlePendingFullscreenTransitions() {
if (pending_transitions_.empty())
return;
bool next_transition = pending_transitions_.front();
pending_transitions_.pop();
SetFullScreen(next_transition);
}
bool NativeWindowMac::HandleDeferredClose() { bool NativeWindowMac::HandleDeferredClose() {
if (has_deferred_window_close_) { if (has_deferred_window_close_) {
SetHasDeferredWindowClose(false); SetHasDeferredWindowClose(false);

View file

@ -237,7 +237,7 @@ using FullScreenTransitionState =
// Store resizable mask so it can be restored after exiting fullscreen. // Store resizable mask so it can be restored after exiting fullscreen.
is_resizable_ = shell_->HasStyleMask(NSWindowStyleMaskResizable); is_resizable_ = shell_->HasStyleMask(NSWindowStyleMaskResizable);
shell_->SetFullScreenTransitionState(FullScreenTransitionState::ENTERING); shell_->set_fullscreen_transition_state(FullScreenTransitionState::ENTERING);
shell_->NotifyWindowWillEnterFullScreen(); shell_->NotifyWindowWillEnterFullScreen();
@ -246,7 +246,7 @@ using FullScreenTransitionState =
} }
- (void)windowDidEnterFullScreen:(NSNotification*)notification { - (void)windowDidEnterFullScreen:(NSNotification*)notification {
shell_->SetFullScreenTransitionState(FullScreenTransitionState::NONE); shell_->set_fullscreen_transition_state(FullScreenTransitionState::NONE);
shell_->NotifyWindowEnterFullScreen(); shell_->NotifyWindowEnterFullScreen();
@ -257,13 +257,13 @@ using FullScreenTransitionState =
} }
- (void)windowWillExitFullScreen:(NSNotification*)notification { - (void)windowWillExitFullScreen:(NSNotification*)notification {
shell_->SetFullScreenTransitionState(FullScreenTransitionState::EXITING); shell_->set_fullscreen_transition_state(FullScreenTransitionState::EXITING);
shell_->NotifyWindowWillLeaveFullScreen(); shell_->NotifyWindowWillLeaveFullScreen();
} }
- (void)windowDidExitFullScreen:(NSNotification*)notification { - (void)windowDidExitFullScreen:(NSNotification*)notification {
shell_->SetFullScreenTransitionState(FullScreenTransitionState::NONE); shell_->set_fullscreen_transition_state(FullScreenTransitionState::NONE);
shell_->SetResizable(is_resizable_); shell_->SetResizable(is_resizable_);
shell_->NotifyWindowLeaveFullScreen(); shell_->NotifyWindowLeaveFullScreen();

View file

@ -4731,19 +4731,80 @@ describe('BrowserWindow module', () => {
expect(w.isFullScreen()).to.be.false('is fullscreen'); expect(w.isFullScreen()).to.be.false('is fullscreen');
}); });
it('handles several HTML fullscreen transitions', async () => {
const w = new BrowserWindow();
await w.loadFile(path.join(fixtures, 'pages', 'a.html'));
expect(w.isFullScreen()).to.be.false('is fullscreen');
const enterFullScreen = emittedOnce(w, 'enter-full-screen');
const leaveFullScreen = emittedOnce(w, 'leave-full-screen');
await w.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
await enterFullScreen;
await w.webContents.executeJavaScript('document.exitFullscreen()', true);
await leaveFullScreen;
expect(w.isFullScreen()).to.be.false('is fullscreen');
await delay();
await w.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
await enterFullScreen;
await w.webContents.executeJavaScript('document.exitFullscreen()', true);
await leaveFullScreen;
expect(w.isFullScreen()).to.be.false('is fullscreen');
});
it('handles several transitions in close proximity', async () => { it('handles several transitions in close proximity', async () => {
const w = new BrowserWindow(); const w = new BrowserWindow();
expect(w.isFullScreen()).to.be.false('is fullscreen'); expect(w.isFullScreen()).to.be.false('is fullscreen');
const enterFS = emittedNTimes(w, 'enter-full-screen', 2);
const leaveFS = emittedNTimes(w, 'leave-full-screen', 2);
w.setFullScreen(true); w.setFullScreen(true);
w.setFullScreen(false); w.setFullScreen(false);
w.setFullScreen(true); w.setFullScreen(true);
w.setFullScreen(false);
const enterFullScreen = emittedNTimes(w, 'enter-full-screen', 2); await Promise.all([enterFS, leaveFS]);
await enterFullScreen;
expect(w.isFullScreen()).to.be.true('not fullscreen'); expect(w.isFullScreen()).to.be.false('not fullscreen');
});
it('handles several chromium-initiated transitions in close proximity', async () => {
const w = new BrowserWindow();
await w.loadFile(path.join(fixtures, 'pages', 'a.html'));
expect(w.isFullScreen()).to.be.false('is fullscreen');
let enterCount = 0;
let exitCount = 0;
const done = new Promise<void>(resolve => {
const checkDone = () => {
if (enterCount === 2 && exitCount === 2) resolve();
};
w.webContents.on('enter-html-full-screen', () => {
enterCount++;
checkDone();
});
w.webContents.on('leave-html-full-screen', () => {
exitCount++;
checkDone();
});
});
await w.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
await w.webContents.executeJavaScript('document.exitFullscreen()');
await w.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
await w.webContents.executeJavaScript('document.exitFullscreen()');
await done;
}); });
it('does not crash when exiting simpleFullScreen (properties)', async () => { it('does not crash when exiting simpleFullScreen (properties)', async () => {

View file

@ -10,7 +10,7 @@ import * as url from 'url';
import * as ChildProcess from 'child_process'; import * as ChildProcess from 'child_process';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { promisify } from 'util'; import { promisify } from 'util';
import { ifit, ifdescribe, delay, defer } from './spec-helpers'; import { ifit, ifdescribe, defer, delay } from './spec-helpers';
import { AddressInfo } from 'net'; import { AddressInfo } from 'net';
import { PipeTransport } from './pipe-transport'; import { PipeTransport } from './pipe-transport';
@ -1590,7 +1590,7 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', ()
server.close(); server.close();
}); });
it('can fullscreen from out-of-process iframes (OOPIFs)', async () => { ifit(process.platform !== 'darwin')('can fullscreen from out-of-process iframes (non-macOS)', async () => {
const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange'); const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
const html = const html =
'<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>'; '<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>';
@ -1614,8 +1614,37 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', ()
expect(width).to.equal(0); expect(width).to.equal(0);
}); });
ifit(process.platform === 'darwin')('can fullscreen from out-of-process iframes (macOS)', async () => {
await emittedOnce(w, 'enter-full-screen');
const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
const html =
'<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>';
w.loadURL(`data:text/html,${html}`);
await fullscreenChange;
const fullscreenWidth = await w.webContents.executeJavaScript(
"document.querySelector('iframe').offsetWidth"
);
expect(fullscreenWidth > 0).to.be.true();
await w.webContents.executeJavaScript(
"document.querySelector('iframe').contentWindow.postMessage('exitFullscreen', '*')"
);
await emittedOnce(w.webContents, 'leave-html-full-screen');
const width = await w.webContents.executeJavaScript(
"document.querySelector('iframe').offsetWidth"
);
expect(width).to.equal(0);
w.setFullScreen(false);
await emittedOnce(w, 'leave-full-screen');
});
// TODO(jkleinsc) fix this flaky test on WOA // TODO(jkleinsc) fix this flaky test on WOA
ifit(process.platform !== 'win32' || process.arch !== 'arm64')('can fullscreen from in-process iframes', async () => { ifit(process.platform !== 'win32' || process.arch !== 'arm64')('can fullscreen from in-process iframes', async () => {
if (process.platform === 'darwin') await emittedOnce(w, 'enter-full-screen');
const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange'); const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
w.loadFile(path.join(fixturesPath, 'pages', 'fullscreen-ipif.html')); w.loadFile(path.join(fixturesPath, 'pages', 'fullscreen-ipif.html'));
await fullscreenChange; await fullscreenChange;

View file

@ -2,6 +2,7 @@
<div id="div"> <div id="div">
WebView WebView
</div> </div>
<video></video>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
const {ipcRenderer} = require('electron') const {ipcRenderer} = require('electron')
ipcRenderer.send('webview-ready') ipcRenderer.send('webview-ready')

View file

@ -426,11 +426,16 @@ describe('<webview> tag', function () {
contextIsolation: false contextIsolation: false
} }
}); });
const attachPromise = emittedOnce(w.webContents, 'did-attach-webview'); const attachPromise = emittedOnce(w.webContents, 'did-attach-webview');
const loadPromise = emittedOnce(w.webContents, 'did-finish-load');
const readyPromise = emittedOnce(ipcMain, 'webview-ready'); const readyPromise = emittedOnce(ipcMain, 'webview-ready');
w.loadFile(path.join(__dirname, 'fixtures', 'webview', 'fullscreen', 'main.html')); w.loadFile(path.join(__dirname, 'fixtures', 'webview', 'fullscreen', 'main.html'));
const [, webview] = await attachPromise; const [, webview] = await attachPromise;
await readyPromise; await Promise.all([readyPromise, loadPromise]);
return [w, webview]; return [w, webview];
}; };
@ -442,17 +447,38 @@ describe('<webview> tag', function () {
closeAllWindows(); closeAllWindows();
}); });
it('should make parent frame element fullscreen too', async () => { ifit(process.platform !== 'darwin')('should make parent frame element fullscreen too (non-macOS)', async () => {
const [w, webview] = await loadWebViewWindow(); const [w, webview] = await loadWebViewWindow();
expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false(); expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange'); const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange');
await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true); await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
await parentFullscreen; await parentFullscreen;
expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true(); expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
const close = emittedOnce(w, 'closed');
w.close(); w.close();
await emittedOnce(w, 'closed'); await close;
});
ifit(process.platform === 'darwin')('should make parent frame element fullscreen too (macOS)', async () => {
const [w, webview] = await loadWebViewWindow();
expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange');
const enterHTMLFS = emittedOnce(w.webContents, 'enter-html-full-screen');
const leaveHTMLFS = emittedOnce(w.webContents, 'leave-html-full-screen');
await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
await webview.executeJavaScript('document.exitFullscreen()');
await Promise.all([enterHTMLFS, leaveHTMLFS, parentFullscreen]);
const close = emittedOnce(w, 'closed');
w.close();
await close;
}); });
// FIXME(zcbenz): Fullscreen events do not work on Linux. // FIXME(zcbenz): Fullscreen events do not work on Linux.
@ -468,8 +494,9 @@ describe('<webview> tag', function () {
await delay(0); await delay(0);
expect(w.isFullScreen()).to.be.false(); expect(w.isFullScreen()).to.be.false();
const close = emittedOnce(w, 'closed');
w.close(); w.close();
await emittedOnce(w, 'closed'); await close;
}); });
// Sending ESC via sendInputEvent only works on Windows. // Sending ESC via sendInputEvent only works on Windows.
@ -485,8 +512,9 @@ describe('<webview> tag', function () {
await delay(0); await delay(0);
expect(w.isFullScreen()).to.be.false(); expect(w.isFullScreen()).to.be.false();
const close = emittedOnce(w, 'closed');
w.close(); w.close();
await emittedOnce(w, 'closed'); await close;
}); });
it('pressing ESC should emit the leave-html-full-screen event', async () => { it('pressing ESC should emit the leave-html-full-screen event', async () => {
@ -513,11 +541,27 @@ describe('<webview> tag', function () {
const leaveFSWindow = emittedOnce(w, 'leave-html-full-screen'); const leaveFSWindow = emittedOnce(w, 'leave-html-full-screen');
const leaveFSWebview = emittedOnce(webContents, 'leave-html-full-screen'); const leaveFSWebview = emittedOnce(webContents, 'leave-html-full-screen');
webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' }); webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
await leaveFSWindow;
await leaveFSWebview; await leaveFSWebview;
await leaveFSWindow;
const close = emittedOnce(w, 'closed');
w.close(); w.close();
await emittedOnce(w, 'closed'); await close;
});
it('should support user gesture', async () => {
const [w, webview] = await loadWebViewWindow();
const waitForEnterHtmlFullScreen = emittedOnce(webview, 'enter-html-full-screen');
const jsScript = "document.querySelector('video').webkitRequestFullscreen()";
webview.executeJavaScript(jsScript, true);
await waitForEnterHtmlFullScreen;
const close = emittedOnce(w, 'closed');
w.close();
await close;
}); });
}); });

View file

@ -1 +1 @@
<video></video> <video></video>

View file

@ -910,20 +910,6 @@ describe('<webview> tag', function () {
}); });
describe('executeJavaScript', () => { describe('executeJavaScript', () => {
it('should support user gesture', async () => {
await loadWebView(webview, {
src: `file://${fixtures}/pages/fullscreen.html`
});
// Event handler has to be added before js execution.
const waitForEnterHtmlFullScreen = waitForEvent(webview, 'enter-html-full-screen');
const jsScript = "document.querySelector('video').webkitRequestFullscreen()";
webview.executeJavaScript(jsScript, true);
return waitForEnterHtmlFullScreen;
});
it('can return the result of the executed script', async () => { it('can return the result of the executed script', async () => {
await loadWebView(webview, { await loadWebView(webview, {
src: 'about:blank' src: 'about:blank'