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 "third_party/blink/renderer/modules/media_controls/media_controls_display_cutout_delegate.h"
6 
7 #include "third_party/blink/public/mojom/page/display_cutout.mojom-blink.h"
8 #include "third_party/blink/renderer/core/events/touch_event.h"
9 #include "third_party/blink/renderer/core/frame/local_frame.h"
10 #include "third_party/blink/renderer/core/frame/viewport_data.h"
11 #include "third_party/blink/renderer/core/frame/web_feature.h"
12 #include "third_party/blink/renderer/core/fullscreen/fullscreen.h"
13 #include "third_party/blink/renderer/core/html/media/html_video_element.h"
14 #include "third_party/blink/renderer/core/input/touch.h"
15 #include "third_party/blink/renderer/core/input/touch_list.h"
16 #include "third_party/blink/renderer/core/loader/empty_clients.h"
17 #include "third_party/blink/renderer/core/testing/page_test_base.h"
18 #include "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
19 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
20 #include "third_party/blink/renderer/platform/runtime_enabled_features.h"
21 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
22 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
23 
24 namespace blink {
25 
26 namespace {
27 
28 class DisplayCutoutMockChromeClient : public EmptyChromeClient {
29  public:
30   // ChromeClient overrides:
EnterFullscreen(LocalFrame & frame,const FullscreenOptions *,FullscreenRequestType)31   void EnterFullscreen(LocalFrame& frame,
32                        const FullscreenOptions*,
33                        FullscreenRequestType) override {
34     Fullscreen::DidResolveEnterFullscreenRequest(*frame.GetDocument(),
35                                                  true /* granted */);
36   }
ExitFullscreen(LocalFrame & frame)37   void ExitFullscreen(LocalFrame& frame) override {
38     Fullscreen::DidExitFullscreen(*frame.GetDocument());
39   }
40 };
41 
42 }  // namespace
43 
44 class MediaControlsDisplayCutoutDelegateTest
45     : public PageTestBase,
46       private ScopedDisplayCutoutAPIForTest,
47       private ScopedMediaControlsExpandGestureForTest {
48  public:
MediaControlsDisplayCutoutDelegateTest()49   MediaControlsDisplayCutoutDelegateTest()
50       : ScopedDisplayCutoutAPIForTest(true),
51         ScopedMediaControlsExpandGestureForTest(true) {}
SetUp()52   void SetUp() override {
53     chrome_client_ = MakeGarbageCollected<DisplayCutoutMockChromeClient>();
54 
55     Page::PageClients clients;
56     FillWithEmptyClients(clients);
57     clients.chrome_client = chrome_client_.Get();
58     SetupPageWithClients(&clients,
59                          MakeGarbageCollected<EmptyLocalFrameClient>());
60     GetDocument().write("<body><video id=video></body>");
61   }
62 
SimulateEnterFullscreen()63   void SimulateEnterFullscreen() {
64     {
65       LocalFrame::NotifyUserActivation(
66           GetDocument().GetFrame(),
67           mojom::UserActivationNotificationType::kTest);
68       Fullscreen::RequestFullscreen(GetVideoElement());
69     }
70 
71     test::RunPendingTasks();
72     GetDocument().ServiceScriptedAnimations(base::TimeTicks());
73 
74     EXPECT_TRUE(GetVideoElement().IsFullscreen());
75   }
76 
SimulateExitFullscreen()77   void SimulateExitFullscreen() {
78     Fullscreen::FullyExitFullscreen(GetDocument());
79 
80     GetDocument().ServiceScriptedAnimations(base::TimeTicks());
81 
82     EXPECT_FALSE(GetVideoElement().IsFullscreen());
83   }
84 
SimulateContractingGesture()85   void SimulateContractingGesture() {
86     TouchList* list = CreateTouchListWithTwoPoints(5, 5, -5, -5);
87     SimulateEvent(
88         CreateTouchEventWithList(event_type_names::kTouchstart, list));
89 
90     list = CreateTouchListWithTwoPoints(4, 4, -4, -4);
91     SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchmove, list));
92 
93     list = CreateTouchListWithTwoPoints(0, 0, 0, 0);
94     SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchend, list));
95   }
96 
SimulateExpandingGesture()97   void SimulateExpandingGesture() {
98     TouchList* list = CreateTouchListWithTwoPoints(1, 1, -1, -1);
99     SimulateEvent(
100         CreateTouchEventWithList(event_type_names::kTouchstart, list));
101 
102     list = CreateTouchListWithTwoPoints(4, 4, -4, -4);
103     SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchmove, list));
104 
105     list = CreateTouchListWithTwoPoints(5, 5, -5, -5);
106     SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchend, list));
107   }
108 
SimulateSingleTouchGesture()109   void SimulateSingleTouchGesture() {
110     TouchList* list = CreateTouchListWithOnePoint(1, 1);
111     SimulateEvent(
112         CreateTouchEventWithList(event_type_names::kTouchstart, list));
113 
114     list = CreateTouchListWithOnePoint(4, 4);
115     SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchmove, list));
116 
117     list = CreateTouchListWithOnePoint(5, 5);
118     SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchend, list));
119   }
120 
HasGestureState()121   bool HasGestureState() { return GetDelegate().previous_.has_value(); }
122 
DirectionIsExpanding()123   bool DirectionIsExpanding() {
124     return GetDelegate().previous_->second ==
125            MediaControlsDisplayCutoutDelegate::Direction::kExpanding;
126   }
127 
DirectionIsUnknown()128   bool DirectionIsUnknown() {
129     return GetDelegate().previous_->second ==
130            MediaControlsDisplayCutoutDelegate::Direction::kUnknown;
131   }
132 
SimulateEvent(TouchEvent * event)133   void SimulateEvent(TouchEvent* event) {
134     DCHECK(event);
135     GetVideoElement().FireEventListeners(*event);
136   }
137 
CreateTouchListWithOnePoint(int x,int y)138   TouchList* CreateTouchListWithOnePoint(int x, int y) {
139     TouchList* list = TouchList::Create();
140     list->Append(CreateTouchAtPoint(x, y));
141     return list;
142   }
143 
CreateTouchListWithTwoPoints(int x1,int y1,int x2,int y2)144   TouchList* CreateTouchListWithTwoPoints(int x1, int y1, int x2, int y2) {
145     TouchList* list = TouchList::Create();
146     list->Append(CreateTouchAtPoint(x1, y1));
147     list->Append(CreateTouchAtPoint(x2, y2));
148     return list;
149   }
150 
CreateTouchEventWithList(const AtomicString & type,TouchList * list)151   TouchEvent* CreateTouchEventWithList(const AtomicString& type,
152                                        TouchList* list) {
153     TouchEvent* event = TouchEvent::Create();
154     event->initEvent(type, true, false);
155     event->SetTouches(list);
156     return event;
157   }
158 
CreateTouchAtPoint(int x,int y)159   Touch* CreateTouchAtPoint(int x, int y) {
160     return Touch::Create(GetDocument().GetFrame(), &GetVideoElement(),
161                          1 /* identifier */, FloatPoint(x, y), FloatPoint(x, y),
162                          FloatSize(1, 1), 90, 0, "test");
163   }
164 
CurrentViewportFit() const165   mojom::ViewportFit CurrentViewportFit() const {
166     return GetDocument().GetViewportData().GetCurrentViewportFitForTests();
167   }
168 
169  private:
GetDelegate()170   MediaControlsDisplayCutoutDelegate& GetDelegate() {
171     MediaControlsImpl* controls =
172         static_cast<MediaControlsImpl*>(GetVideoElement().GetMediaControls());
173     return *controls->display_cutout_delegate_;
174   }
175 
GetVideoElement()176   HTMLVideoElement& GetVideoElement() {
177     return *To<HTMLVideoElement>(GetDocument().getElementById("video"));
178   }
179 
180   Persistent<DisplayCutoutMockChromeClient> chrome_client_;
181 };
182 
TEST_F(MediaControlsDisplayCutoutDelegateTest,CombinedGesture)183 TEST_F(MediaControlsDisplayCutoutDelegateTest, CombinedGesture) {
184   SimulateEnterFullscreen();
185 
186   // Simulate the an expanding gesture but do not finish it.
187   TouchList* list = CreateTouchListWithTwoPoints(1, 1, -1, -1);
188   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchstart, list));
189   list = CreateTouchListWithTwoPoints(4, 4, -4, -4);
190   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchmove, list));
191 
192   // Check the viewport fit value has been correctly set.
193   EXPECT_EQ(mojom::ViewportFit::kCoverForcedByUserAgent, CurrentViewportFit());
194 
195   // Finish the gesture by contracting.
196   list = CreateTouchListWithTwoPoints(0, 0, 0, 0);
197   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchend, list));
198 
199   // Check the viewport fit value has been correctly set.
200   EXPECT_EQ(mojom::ViewportFit::kAuto, CurrentViewportFit());
201 
202   // Make sure we recorded a UseCounter metric.
203   EXPECT_TRUE(GetDocument().IsUseCounted(
204       WebFeature::kMediaControlsDisplayCutoutGesture));
205 }
206 
TEST_F(MediaControlsDisplayCutoutDelegateTest,ContractingGesture)207 TEST_F(MediaControlsDisplayCutoutDelegateTest, ContractingGesture) {
208   // Go fullscreen and simulate an expanding gesture.
209   SimulateEnterFullscreen();
210   SimulateExpandingGesture();
211 
212   // Check the viewport fit value has been correctly set.
213   EXPECT_EQ(mojom::ViewportFit::kCoverForcedByUserAgent, CurrentViewportFit());
214 
215   // Simulate a contracting gesture and check the value has been restored.
216   SimulateContractingGesture();
217   EXPECT_EQ(mojom::ViewportFit::kAuto, CurrentViewportFit());
218 
219   // Make sure we recorded a UseCounter metric.
220   EXPECT_TRUE(GetDocument().IsUseCounted(
221       WebFeature::kMediaControlsDisplayCutoutGesture));
222 }
223 
TEST_F(MediaControlsDisplayCutoutDelegateTest,ContractingGesture_Noop)224 TEST_F(MediaControlsDisplayCutoutDelegateTest, ContractingGesture_Noop) {
225   // Go fullscreen and simulate a contracting gesture.
226   SimulateEnterFullscreen();
227   SimulateContractingGesture();
228 
229   // Check that the value did not change.
230   EXPECT_EQ(mojom::ViewportFit::kAuto, CurrentViewportFit());
231 }
232 
TEST_F(MediaControlsDisplayCutoutDelegateTest,ExpandingGesture)233 TEST_F(MediaControlsDisplayCutoutDelegateTest, ExpandingGesture) {
234   // Go fullscreen and simulate an expanding gesture.
235   SimulateEnterFullscreen();
236   SimulateExpandingGesture();
237 
238   // Check the viewport fit value has been correctly set.
239   EXPECT_EQ(mojom::ViewportFit::kCoverForcedByUserAgent, CurrentViewportFit());
240 
241   // Exit fullscreen and check the value has been restored.
242   SimulateExitFullscreen();
243   EXPECT_EQ(mojom::ViewportFit::kAuto, CurrentViewportFit());
244 
245   // Make sure we recorded a UseCounter metric.
246   EXPECT_TRUE(GetDocument().IsUseCounted(
247       WebFeature::kMediaControlsDisplayCutoutGesture));
248 }
249 
TEST_F(MediaControlsDisplayCutoutDelegateTest,ExpandingGesture_DoubleNoop)250 TEST_F(MediaControlsDisplayCutoutDelegateTest, ExpandingGesture_DoubleNoop) {
251   // Go fullscreen and simulate an expanding gesture.
252   SimulateEnterFullscreen();
253   SimulateExpandingGesture();
254 
255   // Check the viewport fit value has been correctly set.
256   EXPECT_EQ(mojom::ViewportFit::kCoverForcedByUserAgent, CurrentViewportFit());
257 
258   // Simulate another expanding gesture and make sure nothing changed.
259   SimulateExpandingGesture();
260   EXPECT_EQ(mojom::ViewportFit::kCoverForcedByUserAgent, CurrentViewportFit());
261 }
262 
TEST_F(MediaControlsDisplayCutoutDelegateTest,IncompleteGestureClearsState)263 TEST_F(MediaControlsDisplayCutoutDelegateTest, IncompleteGestureClearsState) {
264   SimulateEnterFullscreen();
265 
266   // Simulate a gesture and check we have state.
267   TouchList* list = CreateTouchListWithTwoPoints(1, 1, -1, -1);
268   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchstart, list));
269 
270   list = CreateTouchListWithTwoPoints(2, 2, -2, -2);
271   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchmove, list));
272   EXPECT_TRUE(DirectionIsExpanding());
273 
274   // Simulate another start gesture and make sure we do not have a direction.
275   list = CreateTouchListWithTwoPoints(3, 3, -3, -3);
276   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchstart, list));
277   EXPECT_TRUE(DirectionIsUnknown());
278 }
279 
TEST_F(MediaControlsDisplayCutoutDelegateTest,MetricsNoop)280 TEST_F(MediaControlsDisplayCutoutDelegateTest, MetricsNoop) {
281   EXPECT_FALSE(GetDocument().IsUseCounted(
282       WebFeature::kMediaControlsDisplayCutoutGesture));
283 }
284 
TEST_F(MediaControlsDisplayCutoutDelegateTest,NoFullscreen_Noop)285 TEST_F(MediaControlsDisplayCutoutDelegateTest, NoFullscreen_Noop) {
286   // Simulate an expanding gesture and make sure it had no effect.
287   SimulateExpandingGesture();
288   EXPECT_EQ(mojom::ViewportFit::kAuto, CurrentViewportFit());
289 }
290 
TEST_F(MediaControlsDisplayCutoutDelegateTest,SingleTouchGesture_Noop)291 TEST_F(MediaControlsDisplayCutoutDelegateTest, SingleTouchGesture_Noop) {
292   // Simulate a single touch gesture and make sure it had no effect.
293   SimulateEnterFullscreen();
294   SimulateSingleTouchGesture();
295   mojom::ViewportFit expected =
296       RuntimeEnabledFeatures::MediaControlsUseCutOutByDefaultEnabled()
297           ? mojom::ViewportFit::kCoverForcedByUserAgent
298           : mojom::ViewportFit::kAuto;
299   EXPECT_EQ(expected, CurrentViewportFit());
300 }
301 
TEST_F(MediaControlsDisplayCutoutDelegateTest,TouchCancelShouldClearState)302 TEST_F(MediaControlsDisplayCutoutDelegateTest, TouchCancelShouldClearState) {
303   SimulateEnterFullscreen();
304 
305   // Simulate a gesture and check we have state.
306   TouchList* list = CreateTouchListWithTwoPoints(1, 1, -1, -1);
307   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchstart, list));
308   EXPECT_TRUE(HasGestureState());
309 
310   // Simulate a touchcancel gesture and check that clears the state.
311   list = CreateTouchListWithTwoPoints(1, 1, -1, -1);
312   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchcancel, list));
313   EXPECT_FALSE(HasGestureState());
314   mojom::ViewportFit expected =
315       RuntimeEnabledFeatures::MediaControlsUseCutOutByDefaultEnabled()
316           ? mojom::ViewportFit::kCoverForcedByUserAgent
317           : mojom::ViewportFit::kAuto;
318   EXPECT_EQ(expected, CurrentViewportFit());
319 }
320 
TEST_F(MediaControlsDisplayCutoutDelegateTest,TouchEndShouldClearState)321 TEST_F(MediaControlsDisplayCutoutDelegateTest, TouchEndShouldClearState) {
322   SimulateEnterFullscreen();
323 
324   // Simulate a gesture and check we have state.
325   TouchList* list = CreateTouchListWithTwoPoints(1, 1, -1, -1);
326   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchstart, list));
327   EXPECT_TRUE(HasGestureState());
328 
329   // Simulate a touchend gesture and check that clears the state.
330   list = CreateTouchListWithTwoPoints(1, 1, -1, -1);
331   SimulateEvent(CreateTouchEventWithList(event_type_names::kTouchend, list));
332   EXPECT_FALSE(HasGestureState());
333 
334   mojom::ViewportFit expected =
335       RuntimeEnabledFeatures::MediaControlsUseCutOutByDefaultEnabled()
336           ? mojom::ViewportFit::kCoverForcedByUserAgent
337           : mojom::ViewportFit::kAuto;
338   EXPECT_EQ(expected, CurrentViewportFit());
339 }
340 
TEST_F(MediaControlsDisplayCutoutDelegateTest,DefaultExpand)341 TEST_F(MediaControlsDisplayCutoutDelegateTest, DefaultExpand) {
342   ScopedMediaControlsUseCutOutByDefaultForTest scoped_default_expand(true);
343 
344   SimulateEnterFullscreen();
345   EXPECT_EQ(mojom::ViewportFit::kCoverForcedByUserAgent, CurrentViewportFit());
346 }
347 
TEST_F(MediaControlsDisplayCutoutDelegateTest,DefaultNotExpand)348 TEST_F(MediaControlsDisplayCutoutDelegateTest, DefaultNotExpand) {
349   ScopedMediaControlsUseCutOutByDefaultForTest scoped_default_expand(false);
350 
351   SimulateEnterFullscreen();
352   EXPECT_EQ(mojom::ViewportFit::kAuto, CurrentViewportFit());
353 }
354 
355 }  // namespace blink
356