blob: 7084123c49efc92006176de56571a9f30e50ece6 [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 "core/html/shadow/MediaControls.h"
#include "core/HTMLNames.h"
#include "core/css/StylePropertySet.h"
#include "core/dom/Document.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/StyleEngine.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLVideoElement.h"
#include "core/loader/EmptyClients.h"
#include "core/testing/DummyPageHolder.h"
#include "platform/heap/Handle.h"
#include "platform/testing/UnitTestHelpers.h"
#include "public/platform/WebMediaPlayer.h"
#include "public/platform/WebSize.h"
#include "public/platform/modules/remoteplayback/WebRemotePlaybackAvailability.h"
#include "public/platform/modules/remoteplayback/WebRemotePlaybackClient.h"
#include "testing/gtest/include/gtest/gtest.h"
#include <memory>
namespace blink {
namespace {
class MockVideoWebMediaPlayer : public WebMediaPlayer {
public:
void load(LoadType, const WebMediaPlayerSource&, CORSMode) override{};
void play() override{};
void pause() override{};
bool supportsSave() const override { return false; };
void seek(double seconds) override{};
void setRate(double) override{};
void setVolume(double) override{};
WebTimeRanges buffered() const override { return WebTimeRanges(); };
WebTimeRanges seekable() const override { return WebTimeRanges(); };
void setSinkId(const WebString& sinkId,
const WebSecurityOrigin&,
WebSetSinkIdCallbacks*) override{};
bool hasVideo() const override { return true; };
bool hasAudio() const override { return false; };
WebSize naturalSize() const override { return WebSize(0, 0); };
bool paused() const override { return false; };
bool seeking() const override { return false; };
double duration() const override { return 0.0; };
double currentTime() const override { return 0.0; };
NetworkState getNetworkState() const override { return NetworkStateEmpty; };
ReadyState getReadyState() const override { return ReadyStateHaveNothing; };
WebString getErrorMessage() override { return WebString(); };
bool didLoadingProgress() override { return false; };
bool hasSingleSecurityOrigin() const override { return true; };
bool didPassCORSAccessCheck() const override { return true; };
double mediaTimeForTimeValue(double timeValue) const override {
return timeValue;
};
unsigned decodedFrameCount() const override { return 0; };
unsigned droppedFrameCount() const override { return 0; };
size_t audioDecodedByteCount() const override { return 0; };
size_t videoDecodedByteCount() const override { return 0; };
void paint(WebCanvas*, const WebRect&, PaintFlags&) override{};
};
class MockWebRemotePlaybackClient : public WebRemotePlaybackClient {
public:
void stateChanged(WebRemotePlaybackState) override {}
void availabilityChanged(
WebRemotePlaybackAvailability availability) override {
m_availability = availability;
}
void promptCancelled() override {}
bool remotePlaybackAvailable() const override {
return m_availability == WebRemotePlaybackAvailability::DeviceAvailable;
}
private:
WebRemotePlaybackAvailability m_availability =
WebRemotePlaybackAvailability::Unknown;
};
class StubFrameLoaderClient : public EmptyFrameLoaderClient {
public:
static StubFrameLoaderClient* create() { return new StubFrameLoaderClient; }
std::unique_ptr<WebMediaPlayer> createWebMediaPlayer(
HTMLMediaElement&,
const WebMediaPlayerSource&,
WebMediaPlayerClient*) override {
return WTF::wrapUnique(new MockVideoWebMediaPlayer);
}
WebRemotePlaybackClient* createWebRemotePlaybackClient(
HTMLMediaElement&) override {
if (!m_remotePlaybackClient) {
m_remotePlaybackClient = WTF::wrapUnique(new MockWebRemotePlaybackClient);
}
return m_remotePlaybackClient.get();
}
private:
std::unique_ptr<MockWebRemotePlaybackClient> m_remotePlaybackClient;
};
Element* getElementByShadowPseudoId(Node& rootNode,
const char* shadowPseudoId) {
for (Element& element : ElementTraversal::descendantsOf(rootNode)) {
if (element.shadowPseudoId() == shadowPseudoId)
return &element;
}
return nullptr;
}
bool isElementVisible(Element& element) {
const StylePropertySet* inlineStyle = element.inlineStyle();
if (!inlineStyle)
return true;
if (inlineStyle->getPropertyValue(CSSPropertyDisplay) == "none")
return false;
if (inlineStyle->hasProperty(CSSPropertyOpacity) &&
inlineStyle->getPropertyValue(CSSPropertyOpacity).toDouble() == 0.0) {
return false;
}
if (inlineStyle->getPropertyValue(CSSPropertyVisibility) == "hidden")
return false;
if (Element* parent = element.parentElement())
return isElementVisible(*parent);
return true;
}
} // namespace
class MediaControlsTest : public ::testing::Test {
protected:
virtual void SetUp() {
m_pageHolder = DummyPageHolder::create(IntSize(800, 600), nullptr,
StubFrameLoaderClient::create());
Document& document = this->document();
document.write("<video>");
HTMLVideoElement& video =
toHTMLVideoElement(*document.querySelector("video"));
m_mediaControls = video.mediaControls();
// If scripts are not enabled, controls will always be shown.
m_pageHolder->frame().settings()->setScriptEnabled(true);
}
void simulateRouteAvailabe() {
m_mediaControls->mediaElement().remoteRouteAvailabilityChanged(
WebRemotePlaybackAvailability::DeviceAvailable);
}
void ensureLayout() {
// Force a relayout, so that the controls know the width. Otherwise,
// they don't know if, for example, the cast button will fit.
m_mediaControls->mediaElement().clientWidth();
}
void simulateHideMediaControlsTimerFired() {
m_mediaControls->hideMediaControlsTimerFired(nullptr);
}
MediaControls& mediaControls() { return *m_mediaControls; }
Document& document() { return m_pageHolder->document(); }
private:
std::unique_ptr<DummyPageHolder> m_pageHolder;
Persistent<MediaControls> m_mediaControls;
};
TEST_F(MediaControlsTest, HideAndShow) {
mediaControls().mediaElement().setBooleanAttribute(HTMLNames::controlsAttr,
true);
Element* panel = getElementByShadowPseudoId(mediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
ASSERT_TRUE(isElementVisible(*panel));
mediaControls().hide();
ASSERT_FALSE(isElementVisible(*panel));
mediaControls().show();
ASSERT_TRUE(isElementVisible(*panel));
}
TEST_F(MediaControlsTest, Reset) {
mediaControls().mediaElement().setBooleanAttribute(HTMLNames::controlsAttr,
true);
Element* panel = getElementByShadowPseudoId(mediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
ASSERT_TRUE(isElementVisible(*panel));
mediaControls().reset();
ASSERT_TRUE(isElementVisible(*panel));
}
TEST_F(MediaControlsTest, HideAndReset) {
mediaControls().mediaElement().setBooleanAttribute(HTMLNames::controlsAttr,
true);
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(MediaControlsTest, ResetDoesNotTriggerInitialLayout) {
Document& document = this->document();
int oldElementCount = document.styleEngine().styleForElementCount();
// Also assert that there are no layouts yet.
ASSERT_EQ(0, oldElementCount);
mediaControls().reset();
int newElementCount = document.styleEngine().styleForElementCount();
ASSERT_EQ(oldElementCount, newElementCount);
}
TEST_F(MediaControlsTest, CastButtonRequiresRoute) {
ensureLayout();
mediaControls().mediaElement().setBooleanAttribute(HTMLNames::controlsAttr,
true);
Element* castButton = getElementByShadowPseudoId(
mediaControls(), "-internal-media-controls-cast-button");
ASSERT_NE(nullptr, castButton);
ASSERT_FALSE(isElementVisible(*castButton));
simulateRouteAvailabe();
ASSERT_TRUE(isElementVisible(*castButton));
}
TEST_F(MediaControlsTest, CastButtonDisableRemotePlaybackAttr) {
ensureLayout();
mediaControls().mediaElement().setBooleanAttribute(HTMLNames::controlsAttr,
true);
Element* castButton = getElementByShadowPseudoId(
mediaControls(), "-internal-media-controls-cast-button");
ASSERT_NE(nullptr, castButton);
ASSERT_FALSE(isElementVisible(*castButton));
simulateRouteAvailabe();
ASSERT_TRUE(isElementVisible(*castButton));
mediaControls().mediaElement().setBooleanAttribute(
HTMLNames::disableremoteplaybackAttr, true);
ASSERT_FALSE(isElementVisible(*castButton));
mediaControls().mediaElement().setBooleanAttribute(
HTMLNames::disableremoteplaybackAttr, false);
ASSERT_TRUE(isElementVisible(*castButton));
}
TEST_F(MediaControlsTest, CastOverlayDefault) {
Element* castOverlayButton = getElementByShadowPseudoId(
mediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, castOverlayButton);
simulateRouteAvailabe();
ASSERT_TRUE(isElementVisible(*castOverlayButton));
}
TEST_F(MediaControlsTest, CastOverlayDisableRemotePlaybackAttr) {
Element* castOverlayButton = getElementByShadowPseudoId(
mediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, castOverlayButton);
ASSERT_FALSE(isElementVisible(*castOverlayButton));
simulateRouteAvailabe();
ASSERT_TRUE(isElementVisible(*castOverlayButton));
mediaControls().mediaElement().setBooleanAttribute(
HTMLNames::disableremoteplaybackAttr, true);
ASSERT_FALSE(isElementVisible(*castOverlayButton));
mediaControls().mediaElement().setBooleanAttribute(
HTMLNames::disableremoteplaybackAttr, false);
ASSERT_TRUE(isElementVisible(*castOverlayButton));
}
TEST_F(MediaControlsTest, CastOverlayMediaControlsDisabled) {
Element* castOverlayButton = getElementByShadowPseudoId(
mediaControls(), "-internal-media-controls-overlay-cast-button");
ASSERT_NE(nullptr, castOverlayButton);
EXPECT_FALSE(isElementVisible(*castOverlayButton));
simulateRouteAvailabe();
EXPECT_TRUE(isElementVisible(*castOverlayButton));
document().settings()->setMediaControlsEnabled(false);
EXPECT_FALSE(isElementVisible(*castOverlayButton));
document().settings()->setMediaControlsEnabled(true);
EXPECT_TRUE(isElementVisible(*castOverlayButton));
}
TEST_F(MediaControlsTest, KeepControlsVisibleIfOverflowListVisible) {
Element* overflowList = getElementByShadowPseudoId(
mediaControls(), "-internal-media-controls-overflow-menu-list");
ASSERT_NE(nullptr, overflowList);
Element* panel = getElementByShadowPseudoId(mediaControls(),
"-webkit-media-controls-panel");
ASSERT_NE(nullptr, panel);
mediaControls().mediaElement().setSrc("http://example.com");
mediaControls().mediaElement().play();
testing::runPendingTasks();
mediaControls().show();
mediaControls().toggleOverflowMenu();
EXPECT_TRUE(isElementVisible(*overflowList));
simulateHideMediaControlsTimerFired();
EXPECT_TRUE(isElementVisible(*overflowList));
EXPECT_TRUE(isElementVisible(*panel));
}
} // namespace blink