1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "base/files/file_util.h"
6 #include "base/files/scoped_temp_dir.h"
7 #include "base/run_loop.h"
8 #include "base/threading/thread_restrictions.h"
9 #include "build/build_config.h"
10 #include "components/ukm/test_ukm_recorder.h"
11 #include "content/browser/display_cutout/display_cutout_constants.h"
12 #include "content/browser/renderer_host/frame_tree_node.h"
13 #include "content/browser/web_contents/web_contents_impl.h"
14 #include "content/public/browser/render_view_host.h"
15 #include "content/public/browser/web_contents_delegate.h"
16 #include "content/public/browser/web_contents_observer.h"
17 #include "content/public/common/content_switches.h"
18 #include "content/public/test/browser_test.h"
19 #include "content/public/test/browser_test_utils.h"
20 #include "content/public/test/content_browser_test.h"
21 #include "content/public/test/content_browser_test_utils.h"
22 #include "content/public/test/test_frame_navigation_observer.h"
23 #include "content/public/test/test_navigation_observer.h"
24 #include "content/public/test/test_utils.h"
25 #include "content/shell/browser/shell.h"
26 #include "content/test/content_browser_test_utils_internal.h"
27 #include "mojo/public/cpp/bindings/associated_remote.h"
28 #include "net/test/embedded_test_server/embedded_test_server.h"
29 #include "services/metrics/public/cpp/ukm_builders.h"
30 #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
31 #include "third_party/blink/public/mojom/page/display_cutout.mojom.h"
32
33 namespace content {
34
35 namespace {
36
37 #if defined(OS_ANDROID)
38
39 // These inset and flags simulate when we are not extending into the cutout.
40 const gfx::Insets kNoCutoutInsets = gfx::Insets();
41 const int kNoCutoutInsetsExpectedFlags = DisplayCutoutSafeArea::kEmpty;
42
43 // These inset and flags simulate when the we are extending into the cutout.
44 const gfx::Insets kCutoutInsets = gfx::Insets(1, 0, 1, 0);
45 const int kCutoutInsetsExpectedFlags =
46 DisplayCutoutSafeArea::kTop | DisplayCutoutSafeArea::kBottom;
47
48 // These inset and flags simulate when we are extending into the cutout and have
49 // rotated the device so that the cutout is on the other sides.
50 const gfx::Insets kRotatedCutoutInsets = gfx::Insets(0, 1, 0, 1);
51 const int kRotatedCutoutInsetsExpectedFlags =
52 DisplayCutoutSafeArea::kLeft | DisplayCutoutSafeArea::kRight;
53
54 #endif
55
56 class TestWebContentsObserver : public WebContentsObserver {
57 public:
TestWebContentsObserver(content::WebContents * web_contents)58 explicit TestWebContentsObserver(content::WebContents* web_contents)
59 : WebContentsObserver(web_contents) {}
60
61 // WebContentsObserver override.
ViewportFitChanged(blink::mojom::ViewportFit value)62 void ViewportFitChanged(blink::mojom::ViewportFit value) override {
63 value_ = value;
64
65 if (value_ == wanted_value_)
66 run_loop_.Quit();
67 }
68
has_value() const69 bool has_value() const { return value_.has_value(); }
70
WaitForWantedValue(blink::mojom::ViewportFit wanted_value)71 void WaitForWantedValue(blink::mojom::ViewportFit wanted_value) {
72 if (value_.has_value()) {
73 EXPECT_EQ(wanted_value, value_);
74 return;
75 }
76
77 wanted_value_ = wanted_value;
78 run_loop_.Run();
79 }
80
81 private:
82 base::RunLoop run_loop_;
83 base::Optional<blink::mojom::ViewportFit> value_;
84 blink::mojom::ViewportFit wanted_value_ = blink::mojom::ViewportFit::kAuto;
85
86 DISALLOW_COPY_AND_ASSIGN(TestWebContentsObserver);
87 };
88
89 // Used for forcing a specific |blink::mojom::DisplayMode| during a test.
90 class DisplayCutoutWebContentsDelegate : public WebContentsDelegate {
91 public:
GetDisplayMode(const WebContents * web_contents)92 blink::mojom::DisplayMode GetDisplayMode(
93 const WebContents* web_contents) override {
94 return display_mode_;
95 }
96
SetDisplayMode(blink::mojom::DisplayMode display_mode)97 void SetDisplayMode(blink::mojom::DisplayMode display_mode) {
98 display_mode_ = display_mode;
99 }
100
101 private:
102 blink::mojom::DisplayMode display_mode_ = blink::mojom::DisplayMode::kBrowser;
103 };
104
105 const char kTestHTML[] =
106 "<!DOCTYPE html>"
107 "<style>"
108 " #target {"
109 " margin-top: env(safe-area-inset-top);"
110 " margin-left: env(safe-area-inset-left);"
111 " margin-bottom: env(safe-area-inset-bottom);"
112 " margin-right: env(safe-area-inset-right);"
113 " }"
114 "</style>"
115 "<div id=target></div>";
116
117 } // namespace
118
119 class DisplayCutoutBrowserTest : public ContentBrowserTest {
120 public:
121 DisplayCutoutBrowserTest() = default;
122
SetUpCommandLine(base::CommandLine * command_line)123 void SetUpCommandLine(base::CommandLine* command_line) override {
124 command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
125 "DisplayCutoutAPI");
126 }
127
SetUp()128 void SetUp() override {
129 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
130
131 embedded_test_server()->ServeFilesFromDirectory(temp_dir_.GetPath());
132 ASSERT_TRUE(embedded_test_server()->Start());
133
134 ContentBrowserTest::SetUp();
135 }
136
LoadTestPageWithViewportFitFromMeta(const std::string & value)137 void LoadTestPageWithViewportFitFromMeta(const std::string& value) {
138 LoadTestPageWithData(
139 "<!DOCTYPE html>"
140 "<meta name='viewport' content='viewport-fit=" +
141 value + "'><iframe></iframe>");
142 }
143
LoadSubFrameWithViewportFitMetaValue(const std::string & value)144 void LoadSubFrameWithViewportFitMetaValue(const std::string& value) {
145 const std::string data =
146 "data:text/html;charset=utf-8,<!DOCTYPE html>"
147 "<meta name='viewport' content='viewport-fit=" +
148 value + "'>";
149
150 FrameTreeNode* root = web_contents_impl()->GetFrameTree()->root();
151 FrameTreeNode* child = root->child_at(0);
152
153 ASSERT_TRUE(NavigateToURLFromRenderer(child, GURL(data)));
154 web_contents_impl()->Focus();
155 }
156
ClearViewportFitTag()157 bool ClearViewportFitTag() {
158 return ExecuteScript(
159 web_contents_impl(),
160 "document.getElementsByTagName('meta')[0].content = ''");
161 }
162
SendSafeAreaToFrame(int top,int left,int bottom,int right)163 void SendSafeAreaToFrame(int top, int left, int bottom, int right) {
164 mojo::AssociatedRemote<blink::mojom::DisplayCutoutClient> client;
165 MainFrame()->GetRemoteAssociatedInterfaces()->GetInterface(
166 client.BindNewEndpointAndPassReceiver());
167 client->SetSafeArea(
168 blink::mojom::DisplayCutoutSafeArea::New(top, left, bottom, right));
169 }
170
GetCurrentSafeAreaValue(const std::string & name)171 std::string GetCurrentSafeAreaValue(const std::string& name) {
172 std::string value;
173 EXPECT_TRUE(ExecuteScriptAndExtractString(
174 MainFrame(),
175 "(() => {"
176 "const e = document.getElementById('target');"
177 "const style = window.getComputedStyle(e, null);"
178 "window.domAutomationController.send("
179 " style.getPropertyValue('margin-" +
180 name +
181 "'));"
182 "})();",
183 &value));
184 return value;
185 }
186
LoadTestPageWithData(const std::string & data)187 void LoadTestPageWithData(const std::string& data) {
188 // Write |data| to a temporary file that can be later reached at
189 // http://127.0.0.1/test_file_*.html.
190 static int s_test_file_number = 1;
191 base::FilePath file_path = temp_dir_.GetPath().AppendASCII(
192 base::StringPrintf("test_file_%d.html", s_test_file_number++));
193 {
194 base::ScopedAllowBlockingForTesting allow_temp_file_writing;
195 ASSERT_TRUE(base::WriteFile(file_path, data));
196 }
197 GURL url = embedded_test_server()->GetURL(
198 "/" + file_path.BaseName().AsUTF8Unsafe());
199
200 // Reset UKM and navigate to the html file created above.
201 ResetUKM();
202 ASSERT_TRUE(NavigateToURL(shell(), url));
203 }
204
SimulateFullscreenStateChanged(RenderFrameHost * frame,bool is_fullscreen)205 void SimulateFullscreenStateChanged(RenderFrameHost* frame,
206 bool is_fullscreen) {
207 web_contents_impl()->FullscreenStateChanged(frame, is_fullscreen);
208 }
209
SimulateFullscreenExit()210 void SimulateFullscreenExit() {
211 web_contents_impl()->ExitFullscreenMode(true);
212 }
213
MainFrame()214 RenderFrameHost* MainFrame() { return web_contents_impl()->GetMainFrame(); }
215
ChildFrame()216 RenderFrameHost* ChildFrame() {
217 FrameTreeNode* root = web_contents_impl()->GetFrameTree()->root();
218 return root->child_at(0)->current_frame_host();
219 }
220
web_contents_impl()221 WebContentsImpl* web_contents_impl() {
222 return static_cast<WebContentsImpl*>(shell()->web_contents());
223 }
224
GetUKMEntryCount() const225 unsigned GetUKMEntryCount() const {
226 using Entry = ukm::builders::Layout_DisplayCutout_StateChanged;
227 auto ukm_entries = test_ukm_recorder_->GetEntriesByName(Entry::kEntryName);
228 return ukm_entries.size();
229 }
230
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)231 void ExpectUKMEntry(int index,
232 ukm::SourceId source_id,
233 bool is_main_frame,
234 blink::mojom::ViewportFit applied_value,
235 blink::mojom::ViewportFit supplied_value,
236 int ignored_reason,
237 int safe_areas_present) {
238 using Entry = ukm::builders::Layout_DisplayCutout_StateChanged;
239 auto ukm_entries = test_ukm_recorder_->GetEntriesByName(Entry::kEntryName);
240
241 EXPECT_EQ(source_id, ukm_entries[index]->source_id);
242 EXPECT_EQ(is_main_frame, *test_ukm_recorder_->GetEntryMetric(
243 ukm_entries[index], Entry::kIsMainFrameName));
244 EXPECT_EQ(static_cast<int>(applied_value),
245 *test_ukm_recorder_->GetEntryMetric(
246 ukm_entries[index], Entry::kViewportFit_AppliedName));
247 EXPECT_EQ(static_cast<int>(supplied_value),
248 *test_ukm_recorder_->GetEntryMetric(
249 ukm_entries[index], Entry::kViewportFit_SuppliedName));
250 EXPECT_EQ(ignored_reason,
251 *test_ukm_recorder_->GetEntryMetric(
252 ukm_entries[index], Entry::kViewportFit_IgnoredReasonName));
253 EXPECT_EQ(safe_areas_present,
254 *test_ukm_recorder_->GetEntryMetric(
255 ukm_entries[index], Entry::kSafeAreasPresentName));
256 }
257
258 private:
ResetUKM()259 void ResetUKM() {
260 test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
261 }
262
263 base::ScopedTempDir temp_dir_;
264 std::unique_ptr<ukm::TestUkmRecorder> test_ukm_recorder_;
265
266 DISALLOW_COPY_AND_ASSIGN(DisplayCutoutBrowserTest);
267 };
268
269 // The viewport meta tag is only enabled on Android.
270 #if defined(OS_ANDROID)
271
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,ViewportFit_Fullscreen)272 IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, ViewportFit_Fullscreen) {
273 LoadTestPageWithViewportFitFromMeta("cover");
274 LoadSubFrameWithViewportFitMetaValue("contain");
275
276 {
277 TestWebContentsObserver observer(web_contents_impl());
278 SimulateFullscreenStateChanged(MainFrame(), true);
279 observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
280 web_contents_impl()->SetDisplayCutoutSafeArea(kCutoutInsets);
281 }
282
283 {
284 TestWebContentsObserver observer(web_contents_impl());
285 SimulateFullscreenStateChanged(ChildFrame(), true);
286 observer.WaitForWantedValue(blink::mojom::ViewportFit::kContain);
287 web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
288 }
289
290 {
291 TestWebContentsObserver observer(web_contents_impl());
292 SimulateFullscreenStateChanged(ChildFrame(), false);
293 observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
294
295 // This simulates the user rotating the device.
296 web_contents_impl()->SetDisplayCutoutSafeArea(kCutoutInsets);
297 web_contents_impl()->SetDisplayCutoutSafeArea(kRotatedCutoutInsets);
298 }
299
300 {
301 TestWebContentsObserver observer(web_contents_impl());
302 SimulateFullscreenStateChanged(MainFrame(), false);
303 SimulateFullscreenExit();
304 observer.WaitForWantedValue(blink::mojom::ViewportFit::kAuto);
305 web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
306 }
307
308 // Get the source id for the page and close the |shell|. This will flush any
309 // unrecorded UKM metrics.
310 ukm::SourceId source_id =
311 web_contents_impl()->GetMainFrame()->GetPageUkmSourceId();
312 shell()->Close();
313
314 // Check UKM metrics are recorded. The first two entries are from loading the
315 // frame and the subframe with a viewport fit attribute.
316 EXPECT_EQ(5u, GetUKMEntryCount());
317 ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
318 blink::mojom::ViewportFit::kCover,
319 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
320 kNoCutoutInsetsExpectedFlags);
321 ExpectUKMEntry(1, source_id, false, blink::mojom::ViewportFit::kAuto,
322 blink::mojom::ViewportFit::kContain,
323 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
324 kNoCutoutInsetsExpectedFlags);
325
326 // This is when we take the main frame fullscreen.
327 ExpectUKMEntry(2, source_id, true, blink::mojom::ViewportFit::kCover,
328 blink::mojom::ViewportFit::kCover,
329 DisplayCutoutIgnoredReason::kAllowed,
330 kCutoutInsetsExpectedFlags);
331
332 // This is when we take the subframe fullscreen.
333 ExpectUKMEntry(3, source_id, false, blink::mojom::ViewportFit::kContain,
334 blink::mojom::ViewportFit::kContain,
335 DisplayCutoutIgnoredReason::kAllowed,
336 kNoCutoutInsetsExpectedFlags);
337
338 // These is when the subframe exits fullscreen.
339 ExpectUKMEntry(4, source_id, true, blink::mojom::ViewportFit::kCover,
340 blink::mojom::ViewportFit::kCover,
341 DisplayCutoutIgnoredReason::kAllowed,
342 kRotatedCutoutInsetsExpectedFlags);
343 }
344
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,ViewportFit_Fullscreen_Update)345 IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,
346 ViewportFit_Fullscreen_Update) {
347 LoadTestPageWithViewportFitFromMeta("cover");
348
349 {
350 TestWebContentsObserver observer(web_contents_impl());
351 SimulateFullscreenStateChanged(MainFrame(), true);
352 observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
353 web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
354 }
355
356 {
357 TestWebContentsObserver observer(web_contents_impl());
358 EXPECT_TRUE(ClearViewportFitTag());
359 observer.WaitForWantedValue(blink::mojom::ViewportFit::kAuto);
360 web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
361 }
362
363 // Get the source id for the page and close the |shell|. This will flush any
364 // unrecorded UKM metrics.
365 ukm::SourceId source_id =
366 web_contents_impl()->GetMainFrame()->GetPageUkmSourceId();
367 shell()->Close();
368
369 // Check UKM metrics are recorded.
370 EXPECT_EQ(2u, GetUKMEntryCount());
371 ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
372 blink::mojom::ViewportFit::kCover,
373 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
374 kNoCutoutInsetsExpectedFlags);
375 ExpectUKMEntry(1, source_id, true, blink::mojom::ViewportFit::kCover,
376 blink::mojom::ViewportFit::kCover,
377 DisplayCutoutIgnoredReason::kAllowed,
378 kNoCutoutInsetsExpectedFlags);
379 }
380
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,ViewportFit_Noop_Navigate)381 IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, ViewportFit_Noop_Navigate) {
382 {
383 TestWebContentsObserver observer(web_contents_impl());
384 LoadTestPageWithViewportFitFromMeta("cover");
385 EXPECT_FALSE(observer.has_value());
386 }
387
388 ukm::SourceId source_id =
389 web_contents_impl()->GetMainFrame()->GetPageUkmSourceId();
390 LoadTestPageWithData("");
391
392 // Check UKM metrics are recorded.
393 EXPECT_EQ(1u, GetUKMEntryCount());
394 ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
395 blink::mojom::ViewportFit::kCover,
396 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
397 kNoCutoutInsetsExpectedFlags);
398 }
399
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,ViewportFit_Noop_WebContentsDestroyed)400 IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,
401 ViewportFit_Noop_WebContentsDestroyed) {
402 {
403 TestWebContentsObserver observer(web_contents_impl());
404 LoadTestPageWithViewportFitFromMeta("cover");
405 EXPECT_FALSE(observer.has_value());
406 }
407
408 ukm::SourceId source_id =
409 web_contents_impl()->GetMainFrame()->GetPageUkmSourceId();
410 shell()->Close();
411
412 // Check UKM metrics are recorded.
413 EXPECT_EQ(1u, GetUKMEntryCount());
414 ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
415 blink::mojom::ViewportFit::kCover,
416 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
417 kNoCutoutInsetsExpectedFlags);
418 }
419
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,WebDisplayMode)420 IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode) {
421 // Inject the custom delegate used for this test.
422 std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
423 new DisplayCutoutWebContentsDelegate());
424 web_contents_impl()->SetDelegate(delegate.get());
425 EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());
426
427 {
428 TestWebContentsObserver observer(web_contents_impl());
429 LoadTestPageWithViewportFitFromMeta("cover");
430 EXPECT_FALSE(observer.has_value());
431 }
432 }
433
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,WebDisplayMode_Fullscreen)434 IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode_Fullscreen) {
435 // Inject the custom delegate used for this test.
436 std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
437 new DisplayCutoutWebContentsDelegate());
438 delegate->SetDisplayMode(blink::mojom::DisplayMode::kFullscreen);
439 web_contents_impl()->SetDelegate(delegate.get());
440 EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());
441
442 {
443 TestWebContentsObserver observer(web_contents_impl());
444 LoadTestPageWithViewportFitFromMeta("cover");
445 observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
446 }
447 }
448
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,WebDisplayMode_Standalone)449 IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode_Standalone) {
450 // Inject the custom delegate used for this test.
451 std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
452 new DisplayCutoutWebContentsDelegate());
453 delegate->SetDisplayMode(blink::mojom::DisplayMode::kStandalone);
454 web_contents_impl()->SetDelegate(delegate.get());
455 EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());
456
457 {
458 TestWebContentsObserver observer(web_contents_impl());
459 LoadTestPageWithViewportFitFromMeta("cover");
460 EXPECT_FALSE(observer.has_value());
461 }
462 }
463
464 #endif
465
IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,PublishSafeAreaVariables)466 IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, PublishSafeAreaVariables) {
467 LoadTestPageWithData(kTestHTML);
468
469 // Make sure all the safe areas are currently zero.
470 EXPECT_EQ("0px", GetCurrentSafeAreaValue("top"));
471 EXPECT_EQ("0px", GetCurrentSafeAreaValue("left"));
472 EXPECT_EQ("0px", GetCurrentSafeAreaValue("bottom"));
473 EXPECT_EQ("0px", GetCurrentSafeAreaValue("right"));
474
475 SendSafeAreaToFrame(1, 2, 3, 4);
476
477 // Make sure all the safe ares are correctly set.
478 EXPECT_EQ("1px", GetCurrentSafeAreaValue("top"));
479 EXPECT_EQ("2px", GetCurrentSafeAreaValue("left"));
480 EXPECT_EQ("3px", GetCurrentSafeAreaValue("bottom"));
481 EXPECT_EQ("4px", GetCurrentSafeAreaValue("right"));
482 }
483
484 } // namespace content
485