blob: 3a2b94a484c4d66c7a11f6fc7309c97646ccd182 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/display_cutout/display_cutout_constants.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/mojom/page/display_cutout.mojom.h"
namespace content {
namespace {
#if defined(OS_ANDROID)
// These inset and flags simulate when we are not extending into the cutout.
const gfx::Insets kNoCutoutInsets = gfx::Insets();
const int kNoCutoutInsetsExpectedFlags = DisplayCutoutSafeArea::kEmpty;
// These inset and flags simulate when the we are extending into the cutout.
const gfx::Insets kCutoutInsets = gfx::Insets(1, 0, 1, 0);
const int kCutoutInsetsExpectedFlags =
DisplayCutoutSafeArea::kTop | DisplayCutoutSafeArea::kBottom;
// These inset and flags simulate when we are extending into the cutout and have
// rotated the device so that the cutout is on the other sides.
const gfx::Insets kRotatedCutoutInsets = gfx::Insets(0, 1, 0, 1);
const int kRotatedCutoutInsetsExpectedFlags =
DisplayCutoutSafeArea::kLeft | DisplayCutoutSafeArea::kRight;
#endif
class TestWebContentsObserver : public WebContentsObserver {
public:
explicit TestWebContentsObserver(content::WebContents* web_contents)
: WebContentsObserver(web_contents) {}
// WebContentsObserver override.
void ViewportFitChanged(blink::mojom::ViewportFit value) override {
value_ = value;
if (value_ == wanted_value_)
run_loop_.Quit();
}
bool has_value() const { return value_.has_value(); }
void WaitForWantedValue(blink::mojom::ViewportFit wanted_value) {
if (value_.has_value()) {
EXPECT_EQ(wanted_value, value_);
return;
}
wanted_value_ = wanted_value;
run_loop_.Run();
}
private:
base::RunLoop run_loop_;
base::Optional<blink::mojom::ViewportFit> value_;
blink::mojom::ViewportFit wanted_value_ = blink::mojom::ViewportFit::kAuto;
DISALLOW_COPY_AND_ASSIGN(TestWebContentsObserver);
};
// Used for forcing a specific |blink::WebDisplayMode| during a test.
class DisplayCutoutWebContentsDelegate : public WebContentsDelegate {
public:
blink::WebDisplayMode GetDisplayMode(
const WebContents* web_contents) override {
return display_mode_;
}
void SetDisplayMode(blink::WebDisplayMode display_mode) {
display_mode_ = display_mode;
}
private:
blink::WebDisplayMode display_mode_ =
blink::WebDisplayMode::kWebDisplayModeBrowser;
};
const char kTestHTML[] =
"<!DOCTYPE html>"
"<style>"
" #target {"
" margin-top: env(safe-area-inset-top);"
" margin-left: env(safe-area-inset-left);"
" margin-bottom: env(safe-area-inset-bottom);"
" margin-right: env(safe-area-inset-right);"
" }"
"</style>"
"<div id=target></div>";
} // namespace
class DisplayCutoutBrowserTest : public ContentBrowserTest {
public:
DisplayCutoutBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII("enable-blink-features",
"DisplayCutoutAPI");
}
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
embedded_test_server()->ServeFilesFromDirectory(temp_dir_.GetPath());
ASSERT_TRUE(embedded_test_server()->Start());
ContentBrowserTest::SetUp();
}
void LoadTestPageWithViewportFitFromMeta(const std::string& value) {
LoadTestPageWithData(
"<!DOCTYPE html>"
"<meta name='viewport' content='viewport-fit=" +
value + "'><iframe></iframe>");
}
void LoadSubFrameWithViewportFitMetaValue(const std::string& value) {
const std::string data =
"data:text/html;charset=utf-8,<!DOCTYPE html>"
"<meta name='viewport' content='viewport-fit=" +
value + "'>";
FrameTreeNode* root = web_contents_impl()->GetFrameTree()->root();
FrameTreeNode* child = root->child_at(0);
ASSERT_TRUE(NavigateToURLFromRenderer(child, GURL(data)));
web_contents_impl()->Focus();
}
bool ClearViewportFitTag() {
return ExecuteScript(
web_contents_impl(),
"document.getElementsByTagName('meta')[0].content = ''");
}
void SendSafeAreaToFrame(int top, int left, int bottom, int right) {
blink::mojom::DisplayCutoutClientAssociatedPtr client;
MainFrame()->GetRemoteAssociatedInterfaces()->GetInterface(&client);
client->SetSafeArea(
blink::mojom::DisplayCutoutSafeArea::New(top, left, bottom, right));
}
std::string GetCurrentSafeAreaValue(const std::string& name) {
std::string value;
EXPECT_TRUE(ExecuteScriptAndExtractString(
MainFrame(),
"(() => {"
"const e = document.getElementById('target');"
"const style = window.getComputedStyle(e, null);"
"window.domAutomationController.send("
" style.getPropertyValue('margin-" +
name +
"'));"
"})();",
&value));
return value;
}
void LoadTestPageWithData(const std::string& data) {
// Write |data| to a temporary file that can be later reached at
// http://127.0.0.1/test_file_*.html.
static int s_test_file_number = 1;
base::FilePath file_path = temp_dir_.GetPath().AppendASCII(
base::StringPrintf("test_file_%d.html", s_test_file_number++));
{
base::ScopedAllowBlockingForTesting allow_temp_file_writing;
ASSERT_EQ(static_cast<int>(data.length()),
base::WriteFile(file_path, data.c_str(), data.length()));
}
GURL url = embedded_test_server()->GetURL(
"/" + file_path.BaseName().AsUTF8Unsafe());
// Reset UKM and navigate to the html file created above.
ResetUKM();
ASSERT_TRUE(NavigateToURL(shell(), url));
}
void SimulateFullscreenStateChanged(RenderFrameHost* frame,
bool is_fullscreen) {
web_contents_impl()->FullscreenStateChanged(frame, is_fullscreen);
}
void SimulateFullscreenExit() {
web_contents_impl()->ExitFullscreenMode(true);
}
RenderFrameHost* MainFrame() { return web_contents_impl()->GetMainFrame(); }
RenderFrameHost* ChildFrame() {
FrameTreeNode* root = web_contents_impl()->GetFrameTree()->root();
return root->child_at(0)->current_frame_host();
}
WebContentsImpl* web_contents_impl() {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
unsigned GetUKMEntryCount() const {
using Entry = ukm::builders::Layout_DisplayCutout_StateChanged;
auto ukm_entries = test_ukm_recorder_->GetEntriesByName(Entry::kEntryName);
return ukm_entries.size();
}
void ExpectUKMEntry(int index,
ukm::SourceId source_id,
bool is_main_frame,
blink::mojom::ViewportFit applied_value,
blink::mojom::ViewportFit supplied_value,
int ignored_reason,
int safe_areas_present) {
using Entry = ukm::builders::Layout_DisplayCutout_StateChanged;
auto ukm_entries = test_ukm_recorder_->GetEntriesByName(Entry::kEntryName);
EXPECT_EQ(source_id, ukm_entries[index]->source_id);
EXPECT_EQ(is_main_frame, *test_ukm_recorder_->GetEntryMetric(
ukm_entries[index], Entry::kIsMainFrameName));
EXPECT_EQ(static_cast<int>(applied_value),
*test_ukm_recorder_->GetEntryMetric(
ukm_entries[index], Entry::kViewportFit_AppliedName));
EXPECT_EQ(static_cast<int>(supplied_value),
*test_ukm_recorder_->GetEntryMetric(
ukm_entries[index], Entry::kViewportFit_SuppliedName));
EXPECT_EQ(ignored_reason,
*test_ukm_recorder_->GetEntryMetric(
ukm_entries[index], Entry::kViewportFit_IgnoredReasonName));
EXPECT_EQ(safe_areas_present,
*test_ukm_recorder_->GetEntryMetric(
ukm_entries[index], Entry::kSafeAreasPresentName));
}
private:
void ResetUKM() {
test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
}
base::ScopedTempDir temp_dir_;
std::unique_ptr<ukm::TestUkmRecorder> test_ukm_recorder_;
DISALLOW_COPY_AND_ASSIGN(DisplayCutoutBrowserTest);
};
// The viewport meta tag is only enabled on Android.
#if defined(OS_ANDROID)
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, ViewportFit_Fullscreen) {
LoadTestPageWithViewportFitFromMeta("cover");
LoadSubFrameWithViewportFitMetaValue("contain");
{
TestWebContentsObserver observer(web_contents_impl());
SimulateFullscreenStateChanged(MainFrame(), true);
observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
web_contents_impl()->SetDisplayCutoutSafeArea(kCutoutInsets);
}
{
TestWebContentsObserver observer(web_contents_impl());
SimulateFullscreenStateChanged(ChildFrame(), true);
observer.WaitForWantedValue(blink::mojom::ViewportFit::kContain);
web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
}
{
TestWebContentsObserver observer(web_contents_impl());
SimulateFullscreenStateChanged(ChildFrame(), false);
observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
// This simulates the user rotating the device.
web_contents_impl()->SetDisplayCutoutSafeArea(kCutoutInsets);
web_contents_impl()->SetDisplayCutoutSafeArea(kRotatedCutoutInsets);
}
{
TestWebContentsObserver observer(web_contents_impl());
SimulateFullscreenStateChanged(MainFrame(), false);
SimulateFullscreenExit();
observer.WaitForWantedValue(blink::mojom::ViewportFit::kAuto);
web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
}
// Get the source id for the page and close the |shell|. This will flush any
// unrecorded UKM metrics.
ukm::SourceId source_id =
web_contents_impl()->GetUkmSourceIdForLastCommittedSource();
shell()->Close();
// Check UKM metrics are recorded. The first two entries are from loading the
// frame and the subframe with a viewport fit attribute.
EXPECT_EQ(5u, GetUKMEntryCount());
ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
blink::mojom::ViewportFit::kCover,
DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
kNoCutoutInsetsExpectedFlags);
ExpectUKMEntry(1, source_id, false, blink::mojom::ViewportFit::kAuto,
blink::mojom::ViewportFit::kContain,
DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
kNoCutoutInsetsExpectedFlags);
// This is when we take the main frame fullscreen.
ExpectUKMEntry(2, source_id, true, blink::mojom::ViewportFit::kCover,
blink::mojom::ViewportFit::kCover,
DisplayCutoutIgnoredReason::kAllowed,
kCutoutInsetsExpectedFlags);
// This is when we take the subframe fullscreen.
ExpectUKMEntry(3, source_id, false, blink::mojom::ViewportFit::kContain,
blink::mojom::ViewportFit::kContain,
DisplayCutoutIgnoredReason::kAllowed,
kNoCutoutInsetsExpectedFlags);
// These is when the subframe exits fullscreen.
ExpectUKMEntry(4, source_id, true, blink::mojom::ViewportFit::kCover,
blink::mojom::ViewportFit::kCover,
DisplayCutoutIgnoredReason::kAllowed,
kRotatedCutoutInsetsExpectedFlags);
}
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,
ViewportFit_Fullscreen_Update) {
LoadTestPageWithViewportFitFromMeta("cover");
{
TestWebContentsObserver observer(web_contents_impl());
SimulateFullscreenStateChanged(MainFrame(), true);
observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
}
{
TestWebContentsObserver observer(web_contents_impl());
EXPECT_TRUE(ClearViewportFitTag());
observer.WaitForWantedValue(blink::mojom::ViewportFit::kAuto);
web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
}
// Get the source id for the page and close the |shell|. This will flush any
// unrecorded UKM metrics.
ukm::SourceId source_id =
web_contents_impl()->GetUkmSourceIdForLastCommittedSource();
shell()->Close();
// Check UKM metrics are recorded.
EXPECT_EQ(2u, GetUKMEntryCount());
ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
blink::mojom::ViewportFit::kCover,
DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
kNoCutoutInsetsExpectedFlags);
ExpectUKMEntry(1, source_id, true, blink::mojom::ViewportFit::kCover,
blink::mojom::ViewportFit::kCover,
DisplayCutoutIgnoredReason::kAllowed,
kNoCutoutInsetsExpectedFlags);
}
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, ViewportFit_Noop_Navigate) {
{
TestWebContentsObserver observer(web_contents_impl());
LoadTestPageWithViewportFitFromMeta("cover");
EXPECT_FALSE(observer.has_value());
}
ukm::SourceId source_id =
web_contents_impl()->GetUkmSourceIdForLastCommittedSource();
LoadTestPageWithData("");
// Check UKM metrics are recorded.
EXPECT_EQ(1u, GetUKMEntryCount());
ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
blink::mojom::ViewportFit::kCover,
DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
kNoCutoutInsetsExpectedFlags);
}
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,
ViewportFit_Noop_WebContentsDestroyed) {
{
TestWebContentsObserver observer(web_contents_impl());
LoadTestPageWithViewportFitFromMeta("cover");
EXPECT_FALSE(observer.has_value());
}
ukm::SourceId source_id =
web_contents_impl()->GetUkmSourceIdForLastCommittedSource();
shell()->Close();
// Check UKM metrics are recorded.
EXPECT_EQ(1u, GetUKMEntryCount());
ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
blink::mojom::ViewportFit::kCover,
DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
kNoCutoutInsetsExpectedFlags);
}
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode) {
// Inject the custom delegate used for this test.
std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
new DisplayCutoutWebContentsDelegate());
web_contents_impl()->SetDelegate(delegate.get());
EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());
{
TestWebContentsObserver observer(web_contents_impl());
LoadTestPageWithViewportFitFromMeta("cover");
EXPECT_FALSE(observer.has_value());
}
}
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode_Fullscreen) {
// Inject the custom delegate used for this test.
std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
new DisplayCutoutWebContentsDelegate());
delegate->SetDisplayMode(blink::WebDisplayMode::kWebDisplayModeFullscreen);
web_contents_impl()->SetDelegate(delegate.get());
EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());
{
TestWebContentsObserver observer(web_contents_impl());
LoadTestPageWithViewportFitFromMeta("cover");
observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
}
}
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode_Standalone) {
// Inject the custom delegate used for this test.
std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
new DisplayCutoutWebContentsDelegate());
delegate->SetDisplayMode(blink::WebDisplayMode::kWebDisplayModeStandalone);
web_contents_impl()->SetDelegate(delegate.get());
EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());
{
TestWebContentsObserver observer(web_contents_impl());
LoadTestPageWithViewportFitFromMeta("cover");
EXPECT_FALSE(observer.has_value());
}
}
#endif
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, PublishSafeAreaVariables) {
LoadTestPageWithData(kTestHTML);
// Make sure all the safe areas are currently zero.
EXPECT_EQ("0px", GetCurrentSafeAreaValue("top"));
EXPECT_EQ("0px", GetCurrentSafeAreaValue("left"));
EXPECT_EQ("0px", GetCurrentSafeAreaValue("bottom"));
EXPECT_EQ("0px", GetCurrentSafeAreaValue("right"));
SendSafeAreaToFrame(1, 2, 3, 4);
// Make sure all the safe ares are correctly set.
EXPECT_EQ("1px", GetCurrentSafeAreaValue("top"));
EXPECT_EQ("2px", GetCurrentSafeAreaValue("left"));
EXPECT_EQ("3px", GetCurrentSafeAreaValue("bottom"));
EXPECT_EQ("4px", GetCurrentSafeAreaValue("right"));
}
} // namespace content