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