blob: 987a09318233d1b8425eb981d18b084b5fdfb1f6 [file] [log] [blame]
// Copyright 2015 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 "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
#include <limits>
#include <memory>
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "third_party/blink/public/common/widget/screen_info.h"
#include "third_party/blink/public/mojom/input/focus_type.mojom-blink.h"
#include "third_party/blink/public/mojom/widget/screen_orientation.mojom-blink.h"
#include "third_party/blink/public/platform/modules/remoteplayback/web_remote_playback_client.h"
#include "third_party/blink/public/platform/web_size.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_gc_controller.h"
#include "third_party/blink/renderer/core/css/css_property_value_set.h"
#include "third_party/blink/renderer/core/css/document_style_environment_variables.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_parser.h"
#include "third_party/blink/renderer/core/dom/dom_token_list.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/geometry/dom_rect.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/html/media/html_video_element.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/loader/empty_clients.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_cast_button_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_current_time_display_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_download_button_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_mute_button_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_overflow_menu_button_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_overflow_menu_list_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_play_button_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_remaining_time_display_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_timeline_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_volume_slider_element.h"
#include "third_party/blink/renderer/modules/remoteplayback/remote_playback.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/testing/empty_web_media_player.h"
#include "third_party/blink/renderer/platform/testing/histogram_tester.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/web_test_support.h"
// The MediaTimelineWidths histogram suffix expected to be encountered in these
// tests.
#define TIMELINE_W "256_511"
namespace blink {
namespace {
class FakeChromeClient : public EmptyChromeClient {
public:
FakeChromeClient()
: screen_info_({.orientation_type =
mojom::blink::ScreenOrientation::kLandscapePrimary}) {
}
// ChromeClient overrides.
const ScreenInfo& GetScreenInfo(LocalFrame&) const override {
return screen_info_;
}
private:
const ScreenInfo screen_info_;
};
class MockWebMediaPlayerForImpl : public EmptyWebMediaPlayer {
public:
// WebMediaPlayer overrides:
WebTimeRanges Seekable() const override { return seekable_; }
bool HasVideo() const override { return true; }
bool HasAudio() const override { return has_audio_; }
SurfaceLayerMode GetVideoSurfaceLayerMode() const override {
return SurfaceLayerMode::kAlways;
}
bool has_audio_ = false;
WebTimeRanges seekable_;
};
class MockLayoutObject : public LayoutObject {
public:
MockLayoutObject(Node* node) : LayoutObject(node) {}
const char* GetName() const override { return "MockLayoutObject"; }
void UpdateLayout() override {}
FloatRect LocalBoundingBoxRectForAccessibility() const override {
return FloatRect();
}
};
class StubLocalFrameClientForImpl : public EmptyLocalFrameClient {
public:
std::unique_ptr<WebMediaPlayer> CreateWebMediaPlayer(
HTMLMediaElement&,
const WebMediaPlayerSource&,
WebMediaPlayerClient*) override {
return std::make_unique<MockWebMediaPlayerForImpl>();
}
WebRemotePlaybackClient* CreateWebRemotePlaybackClient(
HTMLMediaElement& element) override {
return &RemotePlayback::From(element);
}
};
Element* GetElementByShadowPseudoId(Node& root_node,
const char* shadow_pseudo_id) {
for (Element& element : ElementTraversal::DescendantsOf(root_node)) {
if (element.ShadowPseudoId() == shadow_pseudo_id)
return &element;
}
return nullptr;
}
bool IsElementVisible(Element& element) {
const CSSPropertyValueSet* inline_style = element.InlineStyle();
if (!inline_style)
return element.getAttribute("class") != "transparent";
if (inline_style->GetPropertyValue(CSSPropertyID::kDisplay) == "none")
return false;
if (inline_style->HasProperty(CSSPropertyID::kOpacity) &&
inline_style->GetPropertyValue(CSSPropertyID::kOpacity).ToDouble() ==
0.0) {
return false;
}
if (inline_style->GetPropertyValue(CSSPropertyID::kVisibility) == "hidden")
return false;
if (Element* parent = element.parentElement())
return IsElementVisible(*parent);
return true;
}
void SimulateTransitionEnd(Element& element) {
element.DispatchEvent(*Event::Create(event_type_names::kTransitionend));
}
// This must match MediaControlDownloadButtonElement::DownloadActionMetrics.
enum DownloadActionMetrics {
kShown = 0,
kClicked,
kCount // Keep last.
};
} // namespace
class MediaControlsImplTest : public PageTestBase,
private ScopedMediaCastOverlayButtonForTest {
public:
MediaControlsImplTest() : ScopedMediaCastOverlayButtonForTest(true) {}
protected:
void SetUp() override {
InitializePage();
}
void InitializePage() {
Page::PageClients clients;
FillWithEmptyClients(clients);
clients.chrome_client = MakeGarbageCollected<FakeChromeClient>();
SetupPageWithClients(&clients,
MakeGarbageCollected<StubLocalFrameClientForImpl>());
GetDocument().write("<video controls>");
auto& video = To<HTMLVideoElement>(*GetDocument().QuerySelector("video"));
media_controls_ = static_cast<MediaControlsImpl*>(video.GetMediaControls());
// Scripts are disabled by default which forces controls to be on.
GetFrame().GetSettings()->SetScriptEnabled(true);
}
void SimulateRouteAvailable() {
RemotePlayback::From(media_controls_->MediaElement())
.AvailabilityChangedForTesting(/* screen_is_available */ true);
}
void EnsureSizing() {
// Fire the size-change callback to ensure that the controls have
// been properly notified of the video size.
media_controls_->NotifyElementSizeChanged(
media_controls_->MediaElement().getBoundingClientRect());
}
void SimulateHideMediaControlsTimerFired() {
media_controls_->HideMediaControlsTimerFired(nullptr);
}
void SimulateLoadedMetadata() { media_controls_->OnLoadedMetadata(); }
void SimulateOnSeeking() { media_controls_->OnSeeking(); }
void SimulateOnSeeked() { media_controls_->OnSeeked(); }
MediaControlsImpl& MediaControls() { return *media_controls_; }
MediaControlVolumeSliderElement* VolumeSliderElement() const {
return media_controls_->volume_slider_;
}
MediaControlTimelineElement* TimelineElement() const {
return media_controls_->timeline_;
}
Element* TimelineTrackElement() const {
if (!TimelineElement())
return nullptr;
return &TimelineElement()->GetTrackElement();
}
MediaControlCurrentTimeDisplayElement* GetCurrentTimeDisplayElement() const {
return media_controls_->current_time_display_;
}
MediaControlRemainingTimeDisplayElement* GetRemainingTimeDisplayElement()
const {
return media_controls_->duration_display_;
}
MediaControlMuteButtonElement* MuteButtonElement() const {
return media_controls_->mute_button_;
}
MediaControlCastButtonElement* CastButtonElement() const {
return media_controls_->cast_button_;
}
MediaControlDownloadButtonElement* DownloadButtonElement() const {
return media_controls_->download_button_;
}
MediaControlPlayButtonElement* PlayButtonElement() const {
return media_controls_->play_button_;
}
MediaControlOverflowMenuButtonElement* OverflowMenuButtonElement() const {
return media_controls_->overflow_menu_;
}
MediaControlOverflowMenuListElement* OverflowMenuListElement() const {
return media_controls_->overflow_list_;
}
MockWebMediaPlayerForImpl* WebMediaPlayer() {
return static_cast<MockWebMediaPlayerForImpl*>(
MediaControls().MediaElement().GetWebMediaPlayer());
}
HistogramTester& GetHistogramTester() { return histogram_tester_; }
void LoadMediaWithDuration(double duration) {
MediaControls().MediaElement().SetSrc("https://example.com/foo.mp4");
test::RunPendingTasks();
WebTimeRange time_range(0.0, duration);
WebMediaPlayer()->seekable_.Assign(&time_range, 1);
MediaControls().MediaElement().DurationChanged(duration,
false /* requestSeek */);
SimulateLoadedMetadata();
}
void SetHasAudio(bool has_audio) { WebMediaPlayer()->has_audio_ = has_audio; }
void ClickOverflowButton() {
MediaControls()
.download_button_->OverflowElementForTests()
->DispatchSimulatedClick(nullptr);
}
void SetReady() {
MediaControls().MediaElement().SetReadyState(
HTMLMediaElement::kHaveEnoughData);
}
void MouseDownAt(gfx::PointF pos);
void MouseMoveTo(gfx::PointF pos);
void MouseUpAt(gfx::PointF pos);
void GestureTapAt(gfx::PointF pos);
void GestureDoubleTapAt(gfx::PointF pos);
bool HasAvailabilityCallbacks(RemotePlayback& remote_playback) {
return !remote_playback.availability_callbacks_.IsEmpty();
}
const String GetDisplayedTime(MediaControlTimeDisplayElement* display) {
return To<Text>(display->firstChild())->data();
}
bool IsOverflowElementVisible(MediaControlInputElement& element) {
MediaControlInputElement* overflow_element =
element.OverflowElementForTests();
if (!overflow_element)
return false;
Element* overflow_parent_label = overflow_element->parentElement();
if (!overflow_parent_label)
return false;
const CSSPropertyValueSet* inline_style =
overflow_parent_label->InlineStyle();
if (inline_style->GetPropertyValue(CSSPropertyID::kDisplay) == "none")
return false;
return true;
}
private:
Persistent<MediaControlsImpl> media_controls_;
HistogramTester histogram_tester_;
};
void MediaControlsImplTest::MouseDownAt(gfx::PointF pos) {
WebMouseEvent mouse_down_event(WebInputEvent::Type::kMouseDown,
pos /* client pos */, pos /* screen pos */,
WebPointerProperties::Button::kLeft, 1,
WebInputEvent::Modifiers::kLeftButtonDown,
WebInputEvent::GetStaticTimeStampForTests());
mouse_down_event.SetFrameScale(1);
GetDocument().GetFrame()->GetEventHandler().HandleMousePressEvent(
mouse_down_event);
}
void MediaControlsImplTest::MouseMoveTo(gfx::PointF pos) {
WebMouseEvent mouse_move_event(WebInputEvent::Type::kMouseMove,
pos /* client pos */, pos /* screen pos */,
WebPointerProperties::Button::kLeft, 1,
WebInputEvent::Modifiers::kLeftButtonDown,
WebInputEvent::GetStaticTimeStampForTests());
mouse_move_event.SetFrameScale(1);
GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
mouse_move_event, {}, {});
}
void MediaControlsImplTest::MouseUpAt(gfx::PointF pos) {
WebMouseEvent mouse_up_event(
WebMouseEvent::Type::kMouseUp, pos /* client pos */, pos /* screen pos */,
WebPointerProperties::Button::kLeft, 1, WebInputEvent::kNoModifiers,
WebInputEvent::GetStaticTimeStampForTests());
mouse_up_event.SetFrameScale(1);
GetDocument().GetFrame()->GetEventHandler().HandleMouseReleaseEvent(
mouse_up_event);
}
void MediaControlsImplTest::GestureTapAt(gfx::PointF pos) {
WebGestureEvent gesture_tap_event(
WebInputEvent::Type::kGestureTap, WebInputEvent::kNoModifiers,
WebInputEvent::GetStaticTimeStampForTests());
// Adjust |pos| by current frame scale.
float frame_scale = GetDocument().GetFrame()->PageZoomFactor();
gesture_tap_event.SetFrameScale(frame_scale);
pos.Scale(frame_scale);
gesture_tap_event.SetPositionInWidget(pos);
// Fire the event.
GetDocument().GetFrame()->GetEventHandler().HandleGestureEvent(
gesture_tap_event);
}
void MediaControlsImplTest::GestureDoubleTapAt(gfx::PointF pos) {
GestureTapAt(pos);
GestureTapAt(pos);
}
TEST_F(MediaControlsImplTest, HideAndShow) {
Element* panel = GetElementByShadowPseudoId(MediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
ASSERT_TRUE(IsElementVisible(*panel));
MediaControls().Hide();
ASSERT_FALSE(IsElementVisible(*panel));
MediaControls().MaybeShow();
ASSERT_TRUE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTest, Reset) {
Element* panel = GetElementByShadowPseudoId(MediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
ASSERT_TRUE(IsElementVisible(*panel));
MediaControls().Reset();
ASSERT_TRUE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTest, HideAndReset) {
Element* panel = GetElementByShadowPseudoId(MediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
ASSERT_TRUE(IsElementVisible(*panel));
MediaControls().Hide();
ASSERT_FALSE(IsElementVisible(*panel));
MediaControls().Reset();
ASSERT_FALSE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTest, ResetDoesNotTriggerInitialLayout) {
Document& document = this->GetDocument();
int old_element_count = document.GetStyleEngine().StyleForElementCount();
// Also assert that there are no layouts yet.
ASSERT_EQ(0, old_element_count);
MediaControls().Reset();
int new_element_count = document.GetStyleEngine().StyleForElementCount();
ASSERT_EQ(old_element_count, new_element_count);
}
TEST_F(MediaControlsImplTest, CastButtonRequiresRoute) {
EnsureSizing();
MediaControlCastButtonElement* cast_button = CastButtonElement();
ASSERT_NE(nullptr, cast_button);
ASSERT_FALSE(IsOverflowElementVisible(*cast_button));
SimulateRouteAvailable();
ASSERT_TRUE(IsOverflowElementVisible(*cast_button));
}
TEST_F(MediaControlsImplTest, CastButtonDisableRemotePlaybackAttr) {
EnsureSizing();
MediaControlCastButtonElement* cast_button = CastButtonElement();
ASSERT_NE(nullptr, cast_button);
ASSERT_FALSE(IsOverflowElementVisible(*cast_button));
SimulateRouteAvailable();
ASSERT_TRUE(IsOverflowElementVisible(*cast_button));
MediaControls().MediaElement().SetBooleanAttribute(
html_names::kDisableremoteplaybackAttr, true);
test::RunPendingTasks();
ASSERT_FALSE(IsOverflowElementVisible(*cast_button));
MediaControls().MediaElement().SetBooleanAttribute(
html_names::kDisableremoteplaybackAttr, false);
test::RunPendingTasks();
ASSERT_TRUE(IsOverflowElementVisible(*cast_button));
}
TEST_F(MediaControlsImplTest, CastOverlayDefault) {
MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
false);
Element* cast_overlay_button = GetElementByShadowPseudoId(
MediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, cast_overlay_button);
SimulateRouteAvailable();
ASSERT_TRUE(IsElementVisible(*cast_overlay_button));
}
TEST_F(MediaControlsImplTest, CastOverlayDisabled) {
MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
false);
ScopedMediaCastOverlayButtonForTest media_cast_overlay_button(false);
Element* cast_overlay_button = GetElementByShadowPseudoId(
MediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, cast_overlay_button);
SimulateRouteAvailable();
ASSERT_FALSE(IsElementVisible(*cast_overlay_button));
}
TEST_F(MediaControlsImplTest, CastOverlayDisableRemotePlaybackAttr) {
MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
false);
Element* cast_overlay_button = GetElementByShadowPseudoId(
MediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, cast_overlay_button);
ASSERT_FALSE(IsElementVisible(*cast_overlay_button));
SimulateRouteAvailable();
ASSERT_TRUE(IsElementVisible(*cast_overlay_button));
MediaControls().MediaElement().SetBooleanAttribute(
html_names::kDisableremoteplaybackAttr, true);
test::RunPendingTasks();
ASSERT_FALSE(IsElementVisible(*cast_overlay_button));
MediaControls().MediaElement().SetBooleanAttribute(
html_names::kDisableremoteplaybackAttr, false);
test::RunPendingTasks();
ASSERT_TRUE(IsElementVisible(*cast_overlay_button));
}
TEST_F(MediaControlsImplTest, CastOverlayMediaControlsDisabled) {
MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
false);
Element* cast_overlay_button = GetElementByShadowPseudoId(
MediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, cast_overlay_button);
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
SimulateRouteAvailable();
EXPECT_TRUE(IsElementVisible(*cast_overlay_button));
GetDocument().GetSettings()->SetMediaControlsEnabled(false);
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
GetDocument().GetSettings()->SetMediaControlsEnabled(true);
EXPECT_TRUE(IsElementVisible(*cast_overlay_button));
}
TEST_F(MediaControlsImplTest, CastOverlayDisabledMediaControlsDisabled) {
MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
false);
ScopedMediaCastOverlayButtonForTest media_cast_overlay_button(false);
Element* cast_overlay_button = GetElementByShadowPseudoId(
MediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, cast_overlay_button);
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
SimulateRouteAvailable();
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
GetDocument().GetSettings()->SetMediaControlsEnabled(false);
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
GetDocument().GetSettings()->SetMediaControlsEnabled(true);
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
}
TEST_F(MediaControlsImplTest, KeepControlsVisibleIfOverflowListVisible) {
Element* overflow_list = GetElementByShadowPseudoId(
MediaControls(), "-internal-media-controls-overflow-menu-list");
ASSERT_NE(nullptr, overflow_list);
Element* panel = GetElementByShadowPseudoId(MediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
test::RunPendingTasks();
MediaControls().MaybeShow();
MediaControls().ToggleOverflowMenu();
EXPECT_TRUE(IsElementVisible(*overflow_list));
SimulateHideMediaControlsTimerFired();
EXPECT_TRUE(IsElementVisible(*overflow_list));
EXPECT_TRUE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTest, DownloadButtonDisplayed) {
EnsureSizing();
MediaControlDownloadButtonElement* download_button = DownloadButtonElement();
ASSERT_NE(nullptr, download_button);
MediaControls().MediaElement().SetSrc("https://example.com/foo.mp4");
test::RunPendingTasks();
SimulateLoadedMetadata();
// Download button should normally be displayed.
EXPECT_TRUE(IsOverflowElementVisible(*download_button));
}
TEST_F(MediaControlsImplTest, DownloadButtonNotDisplayedEmptyUrl) {
EnsureSizing();
MediaControlDownloadButtonElement* download_button = DownloadButtonElement();
ASSERT_NE(nullptr, download_button);
// Download button should not be displayed when URL is empty.
MediaControls().MediaElement().SetSrc("");
test::RunPendingTasks();
SimulateLoadedMetadata();
EXPECT_FALSE(IsOverflowElementVisible(*download_button));
}
TEST_F(MediaControlsImplTest, DownloadButtonNotDisplayedInfiniteDuration) {
EnsureSizing();
MediaControlDownloadButtonElement* download_button = DownloadButtonElement();
ASSERT_NE(nullptr, download_button);
MediaControls().MediaElement().SetSrc("https://example.com/foo.mp4");
test::RunPendingTasks();
// Download button should not be displayed when duration is infinite.
MediaControls().MediaElement().DurationChanged(
std::numeric_limits<double>::infinity(), false /* requestSeek */);
SimulateLoadedMetadata();
EXPECT_FALSE(IsOverflowElementVisible(*download_button));
// Download button should be shown if the duration changes back to finite.
MediaControls().MediaElement().DurationChanged(20.0f,
false /* requestSeek */);
SimulateLoadedMetadata();
EXPECT_TRUE(IsOverflowElementVisible(*download_button));
}
TEST_F(MediaControlsImplTest, DownloadButtonNotDisplayedHLS) {
EnsureSizing();
MediaControlDownloadButtonElement* download_button = DownloadButtonElement();
ASSERT_NE(nullptr, download_button);
// Download button should not be displayed for HLS streams.
MediaControls().MediaElement().SetSrc("https://example.com/foo.m3u8");
test::RunPendingTasks();
SimulateLoadedMetadata();
EXPECT_FALSE(IsOverflowElementVisible(*download_button));
}
TEST_F(MediaControlsImplTest, TimelineSeekToRoundedEnd) {
EnsureSizing();
// Tests the case where the real length of the video, |exact_duration|, gets
// rounded up slightly to |rounded_up_duration| when setting the timeline's
// |max| attribute (crbug.com/695065).
double exact_duration = 596.586667;
double rounded_up_duration = 596.586667;
LoadMediaWithDuration(exact_duration);
// Simulate a click slightly past the end of the track of the timeline's
// underlying <input type="range">. This would set the |value| to the |max|
// attribute, which can be slightly rounded relative to the duration.
MediaControlTimelineElement* timeline = TimelineElement();
timeline->setValueAsNumber(rounded_up_duration, ASSERT_NO_EXCEPTION);
ASSERT_EQ(rounded_up_duration, timeline->valueAsNumber());
EXPECT_EQ(0.0, MediaControls().MediaElement().currentTime());
timeline->DispatchInputEvent();
EXPECT_EQ(exact_duration, MediaControls().MediaElement().currentTime());
}
TEST_F(MediaControlsImplTest, TimelineImmediatelyUpdatesCurrentTime) {
EnsureSizing();
MediaControlCurrentTimeDisplayElement* current_time_display =
GetCurrentTimeDisplayElement();
double duration = 600;
LoadMediaWithDuration(duration);
// Simulate seeking the underlying range to 50%. Current time display should
// update synchronously (rather than waiting for media to finish seeking).
TimelineElement()->setValueAsNumber(duration / 2, ASSERT_NO_EXCEPTION);
TimelineElement()->DispatchInputEvent();
EXPECT_EQ(duration / 2, current_time_display->CurrentValue());
}
TEST_F(MediaControlsImplTest, TimeIndicatorsUpdatedOnSeeking) {
EnsureSizing();
MediaControlCurrentTimeDisplayElement* current_time_display =
GetCurrentTimeDisplayElement();
MediaControlTimelineElement* timeline = TimelineElement();
double duration = 1000;
LoadMediaWithDuration(duration);
EXPECT_EQ(0, current_time_display->CurrentValue());
EXPECT_EQ(0, timeline->valueAsNumber());
MediaControls().MediaElement().setCurrentTime(duration / 4);
// Time indicators are not yet updated.
EXPECT_EQ(0, current_time_display->CurrentValue());
EXPECT_EQ(0, timeline->valueAsNumber());
SimulateOnSeeking();
// The time indicators should be updated immediately when the 'seeking' event
// is fired.
EXPECT_EQ(duration / 4, current_time_display->CurrentValue());
EXPECT_EQ(duration / 4, timeline->valueAsNumber());
}
TEST_F(MediaControlsImplTest, TimeIsCorrectlyFormatted) {
struct {
double time;
String expected_result;
} tests[] = {
{-3661, "-1:01:01"}, {-1, "-0:01"}, {0, "0:00"},
{1, "0:01"}, {15, "0:15"}, {125, "2:05"},
{615, "10:15"}, {3666, "1:01:06"}, {75123, "20:52:03"},
{360600, "100:10:00"},
};
double duration = 360600; // Long enough to check each of the tests.
LoadMediaWithDuration(duration);
EnsureSizing();
test::RunPendingTasks();
MediaControlCurrentTimeDisplayElement* current_display =
GetCurrentTimeDisplayElement();
MediaControlRemainingTimeDisplayElement* duration_display =
GetRemainingTimeDisplayElement();
// The value and format of the duration display should be correct.
EXPECT_EQ(360600, duration_display->CurrentValue());
EXPECT_EQ("/ 100:10:00", GetDisplayedTime(duration_display));
for (const auto& testcase : tests) {
current_display->SetCurrentValue(testcase.time);
// Current value should be updated.
EXPECT_EQ(testcase.time, current_display->CurrentValue());
// Display text should be updated and correctly formatted.
EXPECT_EQ(testcase.expected_result, GetDisplayedTime(current_display));
}
}
namespace {
class MediaControlsImplTestWithMockScheduler : public MediaControlsImplTest {
public:
MediaControlsImplTestWithMockScheduler() { EnablePlatform(); }
protected:
void SetUp() override {
// DocumentParserTiming has DCHECKS to make sure time > 0.0.
platform()->AdvanceClockSeconds(1);
platform()->SetAutoAdvanceNowToPendingTasks(false);
MediaControlsImplTest::SetUp();
}
void TearDown() override {
platform()->SetAutoAdvanceNowToPendingTasks(true);
}
void ToggleOverflowMenu() {
MediaControls().ToggleOverflowMenu();
platform()->RunUntilIdle();
}
bool IsCursorHidden() {
const CSSPropertyValueSet* style = MediaControls().InlineStyle();
if (!style)
return false;
return style->GetPropertyValue(CSSPropertyID::kCursor) == "none";
}
};
} // namespace
TEST_F(MediaControlsImplTestWithMockScheduler, SeekingShowsControls) {
Element* panel = GetElementByShadowPseudoId(MediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
// Hide the controls to start.
MediaControls().Hide();
EXPECT_FALSE(IsElementVisible(*panel));
// Seeking should cause the controls to become visible.
SimulateOnSeeking();
EXPECT_TRUE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTestWithMockScheduler,
SeekingDoesNotShowControlsWhenNoControlsAttr) {
Element* panel = GetElementByShadowPseudoId(MediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
false);
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
// Hide the controls to start.
MediaControls().Hide();
EXPECT_FALSE(IsElementVisible(*panel));
// Seeking should not cause the controls to become visible because the
// controls attribute is not set.
SimulateOnSeeking();
EXPECT_FALSE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTestWithMockScheduler,
ControlsRemainVisibleDuringKeyboardInteraction) {
EnsureSizing();
Element* panel = MediaControls().PanelElement();
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
// Controls start out visible.
EXPECT_TRUE(IsElementVisible(*panel));
// Tabbing between controls prevents controls from hiding.
platform()->RunForPeriodSeconds(2);
MuteButtonElement()->DispatchEvent(*Event::CreateBubble("focusin"));
platform()->RunForPeriodSeconds(2);
EXPECT_TRUE(IsElementVisible(*panel));
// Seeking on the timeline or volume bar prevents controls from hiding.
TimelineElement()->DispatchEvent(*Event::CreateBubble("input"));
platform()->RunForPeriodSeconds(2);
EXPECT_TRUE(IsElementVisible(*panel));
// Pressing a key prevents controls from hiding.
MuteButtonElement()->DispatchEvent(*Event::CreateBubble("keypress"));
platform()->RunForPeriodSeconds(2);
EXPECT_TRUE(IsElementVisible(*panel));
// Once user interaction stops, controls can hide.
platform()->RunForPeriodSeconds(2);
SimulateTransitionEnd(*panel);
EXPECT_FALSE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTestWithMockScheduler,
ControlsHideAfterFocusedAndMouseMovement) {
EnsureSizing();
Element* panel = MediaControls().PanelElement();
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
// Controls start out visible
EXPECT_TRUE(IsElementVisible(*panel));
platform()->RunForPeriodSeconds(1);
// Mouse move while focused
MediaControls().DispatchEvent(*Event::Create("focusin"));
MediaControls().MediaElement().SetFocused(true,
mojom::blink::FocusType::kNone);
MediaControls().DispatchEvent(*Event::Create("pointermove"));
// Controls should remain visible
platform()->RunForPeriodSeconds(2);
EXPECT_TRUE(IsElementVisible(*panel));
// Controls should hide after being inactive for 4 seconds.
platform()->RunForPeriodSeconds(2);
EXPECT_FALSE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTestWithMockScheduler,
ControlsHideAfterFocusedAndMouseMoveout) {
EnsureSizing();
Element* panel = MediaControls().PanelElement();
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
// Controls start out visible
EXPECT_TRUE(IsElementVisible(*panel));
platform()->RunForPeriodSeconds(1);
// Mouse move out while focused, controls should hide
MediaControls().DispatchEvent(*Event::Create("focusin"));
MediaControls().MediaElement().SetFocused(true,
mojom::blink::FocusType::kNone);
MediaControls().DispatchEvent(*Event::Create("pointerout"));
EXPECT_FALSE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTestWithMockScheduler, CursorHidesWhenControlsHide) {
EnsureSizing();
MediaControls().MediaElement().SetSrc("http://example.com");
// Cursor is not initially hidden.
EXPECT_FALSE(IsCursorHidden());
MediaControls().MediaElement().Play();
// Tabbing into the controls shows the controls and therefore the cursor.
MediaControls().DispatchEvent(*Event::Create("focusin"));
EXPECT_FALSE(IsCursorHidden());
// Once the controls hide, the cursor is hidden.
platform()->RunForPeriodSeconds(4);
EXPECT_TRUE(IsCursorHidden());
// If the mouse moves, the controls are shown and the cursor is no longer
// hidden.
MediaControls().DispatchEvent(*Event::Create("pointermove"));
EXPECT_FALSE(IsCursorHidden());
// Once the controls hide again, the cursor is hidden again.
platform()->RunForPeriodSeconds(4);
EXPECT_TRUE(IsCursorHidden());
}
TEST_F(MediaControlsImplTestWithMockScheduler, AccessibleFocusShowsControls) {
EnsureSizing();
Element* panel = MediaControls().PanelElement();
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
platform()->RunForPeriodSeconds(2);
EXPECT_TRUE(IsElementVisible(*panel));
MediaControls().OnAccessibleFocus();
platform()->RunForPeriodSeconds(2);
EXPECT_TRUE(IsElementVisible(*panel));
platform()->RunForPeriodSeconds(2);
SimulateHideMediaControlsTimerFired();
EXPECT_TRUE(IsElementVisible(*panel));
MediaControls().OnAccessibleBlur();
platform()->RunForPeriodSeconds(4);
SimulateHideMediaControlsTimerFired();
EXPECT_FALSE(IsElementVisible(*panel));
}
TEST_F(MediaControlsImplTest,
RemovingFromDocumentRemovesListenersAndCallbacks) {
auto page_holder = std::make_unique<DummyPageHolder>();
auto* element =
MakeGarbageCollected<HTMLVideoElement>(page_holder->GetDocument());
page_holder->GetDocument().body()->AppendChild(element);
RemotePlayback& remote_playback = RemotePlayback::From(*element);
EXPECT_TRUE(remote_playback.HasEventListeners());
EXPECT_TRUE(HasAvailabilityCallbacks(remote_playback));
WeakPersistent<HTMLMediaElement> weak_persistent_video = element;
{
Persistent<HTMLMediaElement> persistent_video = element;
page_holder->GetDocument().body()->setInnerHTML("");
// When removed from the document, the event listeners should have been
// dropped.
EXPECT_FALSE(remote_playback.HasEventListeners());
EXPECT_FALSE(HasAvailabilityCallbacks(remote_playback));
}
test::RunPendingTasks();
ThreadState::Current()->CollectAllGarbageForTesting();
// It has been GC'd.
EXPECT_EQ(nullptr, weak_persistent_video);
}
TEST_F(MediaControlsImplTest,
RemovingFromDocumentWhenResettingSrcAllowsReclamation) {
// Regression test: https://crbug.com/918064
//
// Test ensures that unified heap garbage collections are able to collect
// detached HTMLVideoElements. The tricky part is that ResizeObserver's are
// treated as roots as long as they have observations which prevent the video
// element from being collected.
auto page_holder = std::make_unique<DummyPageHolder>();
page_holder->GetDocument().write("<video controls>");
page_holder->GetDocument().Parser()->Finish();
auto& video =
To<HTMLVideoElement>(*page_holder->GetDocument().QuerySelector("video"));
WeakPersistent<HTMLMediaElement> weak_persistent_video = &video;
video.remove();
test::RunPendingTasks();
ThreadState::Current()->CollectAllGarbageForTesting();
EXPECT_EQ(nullptr, weak_persistent_video);
}
TEST_F(MediaControlsImplTest,
ReInsertingInDocumentRestoresListenersAndCallbacks) {
auto page_holder = std::make_unique<DummyPageHolder>();
auto* element =
MakeGarbageCollected<HTMLVideoElement>(page_holder->GetDocument());
page_holder->GetDocument().body()->AppendChild(element);
RemotePlayback& remote_playback = RemotePlayback::From(*element);
// This should be a no-op. We keep a reference on the media element to avoid
// an unexpected GC.
{
Persistent<HTMLMediaElement> video_holder = element;
page_holder->GetDocument().body()->RemoveChild(element);
page_holder->GetDocument().body()->AppendChild(video_holder.Get());
EXPECT_TRUE(remote_playback.HasEventListeners());
EXPECT_TRUE(HasAvailabilityCallbacks(remote_playback));
}
}
TEST_F(MediaControlsImplTest, InitialInfinityDurationHidesDurationField) {
EnsureSizing();
LoadMediaWithDuration(std::numeric_limits<double>::infinity());
MediaControlRemainingTimeDisplayElement* duration_display =
GetRemainingTimeDisplayElement();
EXPECT_FALSE(duration_display->IsWanted());
EXPECT_EQ(std::numeric_limits<double>::infinity(),
duration_display->CurrentValue());
}
TEST_F(MediaControlsImplTest, InfinityDurationChangeHidesDurationField) {
EnsureSizing();
LoadMediaWithDuration(42);
MediaControlRemainingTimeDisplayElement* duration_display =
GetRemainingTimeDisplayElement();
EXPECT_TRUE(duration_display->IsWanted());
EXPECT_EQ(42, duration_display->CurrentValue());
MediaControls().MediaElement().DurationChanged(
std::numeric_limits<double>::infinity(), false /* request_seek */);
test::RunPendingTasks();
EXPECT_FALSE(duration_display->IsWanted());
EXPECT_EQ(std::numeric_limits<double>::infinity(),
duration_display->CurrentValue());
}
TEST_F(MediaControlsImplTestWithMockScheduler,
ShowVolumeSliderAfterHoverTimerFired) {
const double kTimeToShowVolumeSlider = 0.2;
EnsureSizing();
MediaControls().MediaElement().SetSrc("https://example.com/foo.mp4");
platform()->RunForPeriodSeconds(1);
SetHasAudio(true);
SimulateLoadedMetadata();
WebTestSupport::SetIsRunningWebTest(false);
Element* volume_slider = VolumeSliderElement();
Element* mute_btn = MuteButtonElement();
ASSERT_NE(nullptr, volume_slider);
ASSERT_NE(nullptr, mute_btn);
EXPECT_TRUE(IsElementVisible(*mute_btn));
EXPECT_TRUE(volume_slider->classList().contains("closed"));
DOMRect* mute_btn_rect = mute_btn->getBoundingClientRect();
gfx::PointF mute_btn_center(
mute_btn_rect->left() + mute_btn_rect->width() / 2,
mute_btn_rect->top() + mute_btn_rect->height() / 2);
gfx::PointF edge(0, 0);
// Hover on mute button and stay
MouseMoveTo(mute_btn_center);
platform()->RunForPeriodSeconds(kTimeToShowVolumeSlider - 0.001);
EXPECT_TRUE(volume_slider->classList().contains("closed"));
platform()->RunForPeriodSeconds(0.002);
EXPECT_FALSE(volume_slider->classList().contains("closed"));
MouseMoveTo(edge);
EXPECT_TRUE(volume_slider->classList().contains("closed"));
// Hover on mute button and move away before timer fired
MouseMoveTo(mute_btn_center);
platform()->RunForPeriodSeconds(kTimeToShowVolumeSlider - 0.001);
EXPECT_TRUE(volume_slider->classList().contains("closed"));
MouseMoveTo(edge);
EXPECT_TRUE(volume_slider->classList().contains("closed"));
}
TEST_F(MediaControlsImplTestWithMockScheduler,
VolumeSliderBehaviorWhenFocused) {
MediaControls().MediaElement().SetSrc("https://example.com/foo.mp4");
platform()->RunForPeriodSeconds(1);
SetHasAudio(true);
WebTestSupport::SetIsRunningWebTest(false);
Element* volume_slider = VolumeSliderElement();
ASSERT_NE(nullptr, volume_slider);
// Volume slider starts out hidden
EXPECT_TRUE(volume_slider->classList().contains("closed"));
// Tab focus should open volume slider immediately.
volume_slider->SetFocused(true, mojom::blink::FocusType::kNone);
volume_slider->DispatchEvent(*Event::Create("focus"));
EXPECT_FALSE(volume_slider->classList().contains("closed"));
// Unhover slider while focused should not close slider.
volume_slider->DispatchEvent(*Event::Create("mouseout"));
EXPECT_FALSE(volume_slider->classList().contains("closed"));
}
TEST_F(MediaControlsImplTest, CastOverlayDefaultHidesOnTimer) {
MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
false);
Element* cast_overlay_button = GetElementByShadowPseudoId(
MediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, cast_overlay_button);
SimulateRouteAvailable();
EXPECT_TRUE(IsElementVisible(*cast_overlay_button));
// Starts playback because overlay never hides if paused.
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
test::RunPendingTasks();
SimulateHideMediaControlsTimerFired();
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
}
TEST_F(MediaControlsImplTest, CastOverlayShowsOnSomeEvents) {
MediaControls().MediaElement().SetBooleanAttribute(html_names::kControlsAttr,
false);
Element* cast_overlay_button = GetElementByShadowPseudoId(
MediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, cast_overlay_button);
Element* overlay_enclosure = GetElementByShadowPseudoId(
MediaControls(), "-webkit-media-controls-overlay-enclosure");
ASSERT_NE(nullptr, overlay_enclosure);
SimulateRouteAvailable();
EXPECT_TRUE(IsElementVisible(*cast_overlay_button));
// Starts playback because overlay never hides if paused.
MediaControls().MediaElement().SetSrc("http://example.com");
MediaControls().MediaElement().Play();
test::RunPendingTasks();
SimulateRouteAvailable();
SimulateHideMediaControlsTimerFired();
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
for (auto* const event_name :
{"gesturetap", "click", "pointerover", "pointermove"}) {
overlay_enclosure->DispatchEvent(*Event::Create(event_name));
EXPECT_TRUE(IsElementVisible(*cast_overlay_button));
SimulateHideMediaControlsTimerFired();
EXPECT_FALSE(IsElementVisible(*cast_overlay_button));
}
}
TEST_F(MediaControlsImplTest, isConnected) {
EXPECT_TRUE(MediaControls().isConnected());
MediaControls().MediaElement().remove();
EXPECT_FALSE(MediaControls().isConnected());
}
TEST_F(MediaControlsImplTest, ControlsShouldUseSafeAreaInsets) {
UpdateAllLifecyclePhasesForTest();
{
const ComputedStyle* style = MediaControls().GetComputedStyle();
EXPECT_EQ(0.0, style->MarginTop().Pixels());
EXPECT_EQ(0.0, style->MarginLeft().Pixels());
EXPECT_EQ(0.0, style->MarginBottom().Pixels());
EXPECT_EQ(0.0, style->MarginRight().Pixels());
}
GetStyleEngine().EnsureEnvironmentVariables().SetVariable(
"safe-area-inset-top", "1px");
GetStyleEngine().EnsureEnvironmentVariables().SetVariable(
"safe-area-inset-left", "2px");
GetStyleEngine().EnsureEnvironmentVariables().SetVariable(
"safe-area-inset-bottom", "3px");
GetStyleEngine().EnsureEnvironmentVariables().SetVariable(
"safe-area-inset-right", "4px");
EXPECT_TRUE(GetDocument().NeedsLayoutTreeUpdate());
UpdateAllLifecyclePhasesForTest();
{
const ComputedStyle* style = MediaControls().GetComputedStyle();
EXPECT_EQ(1.0, style->MarginTop().Pixels());
EXPECT_EQ(2.0, style->MarginLeft().Pixels());
EXPECT_EQ(3.0, style->MarginBottom().Pixels());
EXPECT_EQ(4.0, style->MarginRight().Pixels());
}
}
TEST_F(MediaControlsImplTest, MediaControlsDisabledWithNoSource) {
EXPECT_EQ(MediaControls().State(), MediaControlsImpl::kNoSource);
EXPECT_TRUE(PlayButtonElement()->FastHasAttribute(html_names::kDisabledAttr));
EXPECT_TRUE(
OverflowMenuButtonElement()->FastHasAttribute(html_names::kDisabledAttr));
EXPECT_TRUE(TimelineElement()->FastHasAttribute(html_names::kDisabledAttr));
MediaControls().MediaElement().setAttribute(html_names::kPreloadAttr, "none");
MediaControls().MediaElement().SetSrc("https://example.com/foo.mp4");
test::RunPendingTasks();
SimulateLoadedMetadata();
EXPECT_EQ(MediaControls().State(), MediaControlsImpl::kNotLoaded);
EXPECT_FALSE(
PlayButtonElement()->FastHasAttribute(html_names::kDisabledAttr));
EXPECT_FALSE(
OverflowMenuButtonElement()->FastHasAttribute(html_names::kDisabledAttr));
EXPECT_TRUE(TimelineElement()->FastHasAttribute(html_names::kDisabledAttr));
MediaControls().MediaElement().removeAttribute(html_names::kPreloadAttr);
SimulateLoadedMetadata();
EXPECT_EQ(MediaControls().State(), MediaControlsImpl::kLoadingMetadataPaused);
EXPECT_FALSE(
PlayButtonElement()->FastHasAttribute(html_names::kDisabledAttr));
EXPECT_FALSE(
OverflowMenuButtonElement()->FastHasAttribute(html_names::kDisabledAttr));
EXPECT_FALSE(TimelineElement()->FastHasAttribute(html_names::kDisabledAttr));
}
TEST_F(MediaControlsImplTest, DoubleTouchChangesTime) {
double duration = 60; // 1 minute.
LoadMediaWithDuration(duration);
EnsureSizing();
MediaControls().MediaElement().setCurrentTime(30);
test::RunPendingTasks();
// We've set the video to the halfway mark.
EXPECT_EQ(30, MediaControls().MediaElement().currentTime());
DOMRect* videoRect = MediaControls().MediaElement().getBoundingClientRect();
ASSERT_LT(0, videoRect->width());
gfx::PointF leftOfCenter(videoRect->left() + (videoRect->width() / 2) - 5,
videoRect->top() + 5);
gfx::PointF rightOfCenter(videoRect->left() + (videoRect->width() / 2) + 5,
videoRect->top() + 5);
// Double-tapping left of center should shift the time backwards by 10
// seconds.
GestureDoubleTapAt(leftOfCenter);
test::RunPendingTasks();
EXPECT_EQ(20, MediaControls().MediaElement().currentTime());
// Double-tapping right of center should shift the time forwards by 10
// seconds.
GestureDoubleTapAt(rightOfCenter);
test::RunPendingTasks();
EXPECT_EQ(30, MediaControls().MediaElement().currentTime());
}
TEST_F(MediaControlsImplTest, DoubleTouchChangesTimeWhenZoomed) {
double duration = 60; // 1 minute.
LoadMediaWithDuration(duration);
EnsureSizing();
MediaControls().MediaElement().setCurrentTime(30);
test::RunPendingTasks();
// We've set the video to the halfway mark.
EXPECT_EQ(30, MediaControls().MediaElement().currentTime());
DOMRect* videoRect = MediaControls().MediaElement().getBoundingClientRect();
ASSERT_LT(0, videoRect->width());
gfx::PointF leftOfCenter(videoRect->left() + (videoRect->width() / 2) - 5,
videoRect->top() + 10);
gfx::PointF rightOfCenter(videoRect->left() + (videoRect->width() / 2) + 5,
videoRect->top() + 10);
// Add a zoom factor and ensure that it's properly handled.
MediaControls().GetDocument().GetFrame()->SetPageZoomFactor(2);
// Double-tapping left of center should shift the time backwards by 10
// seconds.
GestureDoubleTapAt(leftOfCenter);
test::RunPendingTasks();
EXPECT_EQ(20, MediaControls().MediaElement().currentTime());
// Double-tapping right of center should shift the time forwards by 10
// seconds.
GestureDoubleTapAt(rightOfCenter);
test::RunPendingTasks();
EXPECT_EQ(30, MediaControls().MediaElement().currentTime());
}
} // namespace blink