// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/web_test/browser/web_test_shell_platform_delegate.h"
#import "base/apple/foundation_util.h"
#include "base/containers/contains.h"
#include "content/browser/renderer_host/render_widget_host_view_mac.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/shell/browser/shell.h"
namespace content {
// On mac, the WebTestShellPlatformDelegate replaces behaviour in the base class
// ShellPlatformDelegate when in headless mode. Otherwise it mostly defers to
// the base class.
struct WebTestShellPlatformDelegate::WebTestShellData {
gfx::Size initial_size;
};
struct WebTestShellPlatformDelegate::WebTestPlatformData {};
WebTestShellPlatformDelegate::WebTestShellPlatformDelegate() = default;
WebTestShellPlatformDelegate::~WebTestShellPlatformDelegate() = default;
void WebTestShellPlatformDelegate::Initialize(
const gfx::Size& default_window_size) {
ShellPlatformDelegate::Initialize(default_window_size);
}
void WebTestShellPlatformDelegate::CreatePlatformWindow(
Shell* shell,
const gfx::Size& initial_size) {
if (!IsHeadless()) {
ShellPlatformDelegate::CreatePlatformWindow(shell, initial_size);
return;
}
DCHECK(!base::Contains(web_test_shell_data_map_, shell));
WebTestShellData& shell_data = web_test_shell_data_map_[shell];
shell_data.initial_size = initial_size;
}
gfx::NativeWindow WebTestShellPlatformDelegate::GetNativeWindow(Shell* shell) {
if (!IsHeadless())
return ShellPlatformDelegate::GetNativeWindow(shell);
NOTREACHED_IN_MIGRATION();
return {};
}
void WebTestShellPlatformDelegate::CleanUp(Shell* shell) {
if (!IsHeadless()) {
ShellPlatformDelegate::CleanUp(shell);
return;
}
DCHECK(base::Contains(web_test_shell_data_map_, shell));
web_test_shell_data_map_.erase(shell);
if (shell == activated_headless_shell_)
activated_headless_shell_ = nullptr;
}
void WebTestShellPlatformDelegate::SetContents(Shell* shell) {
if (!IsHeadless()) {
ShellPlatformDelegate::SetContents(shell);
return;
}
}
void WebTestShellPlatformDelegate::EnableUIControl(Shell* shell,
UIControl control,
bool is_enabled) {
if (!IsHeadless()) {
ShellPlatformDelegate::EnableUIControl(shell, control, is_enabled);
return;
}
}
void WebTestShellPlatformDelegate::SetAddressBarURL(Shell* shell,
const GURL& url) {
if (!IsHeadless()) {
ShellPlatformDelegate::SetAddressBarURL(shell, url);
return;
}
}
void WebTestShellPlatformDelegate::SetTitle(Shell* shell,
const std::u16string& title) {
if (!IsHeadless()) {
ShellPlatformDelegate::SetTitle(shell, title);
return;
}
}
void WebTestShellPlatformDelegate::MainFrameCreated(Shell* shell) {
if (!IsHeadless()) {
ShellPlatformDelegate::MainFrameCreated(shell);
return;
}
DCHECK(base::Contains(web_test_shell_data_map_, shell));
WebTestShellData& shell_data = web_test_shell_data_map_[shell];
// In mac headless mode, the OS view for the WebContents is not attached to a
// window so the usual notifications from the OS about the bounds of the web
// contents do not occur. We need to make sure the renderer knows its bounds,
// and to do this we force a resize to happen on the WebContents. However, the
// WebContents can not be fully resized until after the RenderWidgetHostView
// is created, which may not be not done until the first navigation starts.
// Failing to do this resize *after* the navigation causes the
// RenderWidgetHostView to be created would leave the WebContents with invalid
// sizes (such as the window screen rect).
//
// We use the signal that the `blink::WebView` has been created in the
// renderer as a proxy for knowing when the top level RenderWidgetHostView is
// created, since they are created at the same time.
DCHECK(shell->web_contents()->GetPrimaryMainFrame()->GetView());
ResizeWebContent(shell, shell_data.initial_size);
}
bool WebTestShellPlatformDelegate::DestroyShell(Shell* shell) {
if (IsHeadless())
return false; // Shell destroys itself.
return ShellPlatformDelegate::DestroyShell(shell);
}
void WebTestShellPlatformDelegate::ResizeWebContent(
Shell* shell,
const gfx::Size& content_size) {
if (!IsHeadless()) {
ShellPlatformDelegate::ResizeWebContent(shell, content_size);
return;
}
NSView* web_view = shell->web_contents()->GetNativeView().GetNativeNSView();
web_view.frame =
NSMakeRect(0, 0, content_size.width(), content_size.height());
// The above code changes the RenderWidgetHostView's size, but does not change
// the widget's screen rects, since the RenderWidgetHostView is not attached
// to a window in headless mode. So this call causes them to be updated so
// they are not left as 0x0.
auto* rwhv_mac = shell->web_contents()->GetPrimaryMainFrame()->GetView();
if (rwhv_mac)
rwhv_mac->SetWindowFrameInScreen(gfx::Rect(content_size));
}
void WebTestShellPlatformDelegate::ActivateContents(Shell* shell,
WebContents* top_contents) {
if (!IsHeadless()) {
ShellPlatformDelegate::ActivateContents(shell, top_contents);
return;
}
// In headless mode, there are no system windows, so we can't go down the
// normal path which relies on calling the OS to move focus/active states.
// Instead we fake it out by just informing the RenderWidgetHost directly.
// For all windows other than this one, blur them.
for (Shell* window : Shell::windows()) {
if (window != shell) {
WebContents* other_top_contents = window->web_contents();
auto* other_rwhv_mac = static_cast<RenderWidgetHostViewMac*>(
other_top_contents->GetPrimaryMainFrame()->GetView());
other_rwhv_mac->OnFirstResponderChanged(false);
other_rwhv_mac->GetRenderWidgetHost()->SetActive(false);
}
}
auto* top_rwhv_mac = static_cast<RenderWidgetHostViewMac*>(
top_contents->GetPrimaryMainFrame()->GetView());
top_rwhv_mac->OnFirstResponderChanged(true);
top_rwhv_mac->GetRenderWidgetHost()->SetActive(true);
activated_headless_shell_ = shell;
}
void WebTestShellPlatformDelegate::DidNavigatePrimaryMainFramePostCommit(
Shell* shell,
WebContents* contents) {
if (!IsHeadless()) {
ShellPlatformDelegate::DidNavigatePrimaryMainFramePostCommit(shell,
contents);
return;
}
// Normally RenderFrameHostManager::CommitPending() transfers focus status to
// the new RenderWidgetHostView when a navigation creates a new view, but that
// doesn't work in Mac headless mode because RenderWidgetHostView depends on
// the native window (which doesn't exist in headless mode) to manage focus
// status. Instead we manually set focus status of the new RenderWidgetHost.
if (shell == activated_headless_shell_)
ActivateContents(shell, contents);
}
bool WebTestShellPlatformDelegate::HandleKeyboardEvent(
Shell* shell,
WebContents* source,
const input::NativeWebKeyboardEvent& event) {
if (IsHeadless())
return false;
return ShellPlatformDelegate::HandleKeyboardEvent(shell, source, event);
}
} // namespace content