blob: aa19a0f5dd5a41d7ba0bd2d4ecd6a0d7e7216244 [file] [log] [blame]
// Copyright 2019 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 "chrome/browser/ui/global_media_controls/media_toolbar_button_controller.h"
#include <memory>
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/unguessable_token.h"
#include "chrome/browser/ui/global_media_controls/media_dialog_delegate.h"
#include "chrome/browser/ui/global_media_controls/media_toolbar_button_controller_delegate.h"
#include "components/media_message_center/media_notification_item.h"
#include "components/media_message_center/media_notification_util.h"
#include "services/media_session/public/mojom/audio_focus.mojom.h"
#include "services/media_session/public/mojom/media_session.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using media_session::mojom::AudioFocusRequestState;
using media_session::mojom::AudioFocusRequestStatePtr;
using media_session::mojom::MediaSessionInfo;
using media_session::mojom::MediaSessionInfoPtr;
using testing::_;
namespace {
class MockMediaToolbarButtonControllerDelegate
: public MediaToolbarButtonControllerDelegate {
public:
MockMediaToolbarButtonControllerDelegate() = default;
~MockMediaToolbarButtonControllerDelegate() override = default;
// MediaToolbarButtonControllerDelegate implementation.
MOCK_METHOD0(Show, void());
MOCK_METHOD0(Hide, void());
MOCK_METHOD0(Enable, void());
MOCK_METHOD0(Disable, void());
};
class MockMediaDialogDelegate : public MediaDialogDelegate {
public:
MockMediaDialogDelegate() = default;
~MockMediaDialogDelegate() override { Close(); }
void Open(MediaToolbarButtonController* controller) {
ASSERT_NE(nullptr, controller);
controller_ = controller;
controller_->SetDialogDelegate(this);
}
void Close() {
if (!controller_)
return;
controller_->SetDialogDelegate(nullptr);
controller_ = nullptr;
}
// MediaDialogDelegate implementation.
MOCK_METHOD2(
ShowMediaSession,
MediaNotificationContainerImpl*(
const std::string& id,
base::WeakPtr<media_message_center::MediaNotificationItem> item));
MOCK_METHOD1(HideMediaSession, void(const std::string& id));
private:
MediaToolbarButtonController* controller_;
DISALLOW_COPY_AND_ASSIGN(MockMediaDialogDelegate);
};
} // anonymous namespace
class MediaToolbarButtonControllerTest : public testing::Test {
public:
MediaToolbarButtonControllerTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME,
base::test::TaskEnvironment::MainThreadType::UI) {}
~MediaToolbarButtonControllerTest() override = default;
void SetUp() override {
controller_ = std::make_unique<MediaToolbarButtonController>(
base::UnguessableToken::Create(), nullptr, &delegate_);
}
protected:
void AdvanceClockMilliseconds(int milliseconds) {
task_environment_.FastForwardBy(
base::TimeDelta::FromMilliseconds(milliseconds));
}
base::UnguessableToken SimulatePlayingControllableMedia() {
base::UnguessableToken id = base::UnguessableToken::Create();
SimulateFocusGained(id, true);
SimulateNecessaryMetadata(id);
return id;
}
AudioFocusRequestStatePtr CreateFocusRequest(const base::UnguessableToken& id,
bool controllable) {
MediaSessionInfoPtr session_info(MediaSessionInfo::New());
session_info->is_controllable = controllable;
AudioFocusRequestStatePtr focus(AudioFocusRequestState::New());
focus->request_id = id;
focus->session_info = std::move(session_info);
return focus;
}
void SimulateFocusGained(const base::UnguessableToken& id,
bool controllable) {
controller_->OnFocusGained(CreateFocusRequest(id, controllable));
}
void SimulateFocusLost(const base::UnguessableToken& id) {
AudioFocusRequestStatePtr focus(AudioFocusRequestState::New());
focus->request_id = id;
controller_->OnFocusLost(std::move(focus));
}
void SimulateNecessaryMetadata(const base::UnguessableToken& id) {
// In order for the MediaNotificationItem to tell the
// MediaToolbarButtonController to show a media session, that session needs
// a title and artist. Typically this would happen through the media session
// service, but since the service doesn't run for this test, we'll manually
// grab the MediaNotificationItem from the MediaToolbarButtonController and
// set the metadata.
auto item_itr = controller_->sessions_.find(id.ToString());
ASSERT_NE(controller_->sessions_.end(), item_itr);
media_session::MediaMetadata metadata;
metadata.title = base::ASCIIToUTF16("title");
metadata.artist = base::ASCIIToUTF16("artist");
item_itr->second.item()->MediaSessionMetadataChanged(std::move(metadata));
}
void SimulateReceivedAudioFocusRequests(
std::vector<AudioFocusRequestStatePtr> requests) {
controller_->OnReceivedAudioFocusRequests(std::move(requests));
}
bool IsSessionFrozen(const base::UnguessableToken& id) const {
auto item_itr = controller_->sessions_.find(id.ToString());
EXPECT_NE(controller_->sessions_.end(), item_itr);
return item_itr->second.item()->frozen();
}
void SimulateDialogOpened(MockMediaDialogDelegate* delegate) {
delegate->Open(controller_.get());
}
void SimulateTabClosed(const base::UnguessableToken& id) {
// When a tab is closing, audio focus will be lost before the WebContents is
// destroyed, so to simulate closer to reality we will also simulate audio
// focus lost here.
SimulateFocusLost(id);
// Now, close the tab.
auto item_itr = controller_->sessions_.find(id.ToString());
EXPECT_NE(controller_->sessions_.end(), item_itr);
item_itr->second.WebContentsDestroyed();
}
void SimulateDismissButtonClicked(const base::UnguessableToken& id) {
controller_->OnContainerDismissed(id.ToString());
}
void ExpectHistogramCountRecorded(int count, int size) {
histogram_tester_.ExpectBucketCount(
media_message_center::kCountHistogramName, count, size);
}
MockMediaToolbarButtonControllerDelegate& delegate() { return delegate_; }
private:
base::test::TaskEnvironment task_environment_;
MockMediaToolbarButtonControllerDelegate delegate_;
std::unique_ptr<MediaToolbarButtonController> controller_;
base::HistogramTester histogram_tester_;
DISALLOW_COPY_AND_ASSIGN(MediaToolbarButtonControllerTest);
};
TEST_F(MediaToolbarButtonControllerTest, ShowControllableOnGainAndHideOnLoss) {
// Simulate a new active, controllable media session.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_FALSE(IsSessionFrozen(id));
// Ensure that the toolbar button was shown.
testing::Mock::VerifyAndClearExpectations(&delegate());
// Simulate opening a MediaDialogView.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Ensure that the session was shown.
ExpectHistogramCountRecorded(1, 1);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Simulate the active session ending.
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString())).Times(0);
SimulateFocusLost(id);
// Ensure that the session was frozen and not hidden.
EXPECT_TRUE(IsSessionFrozen(id));
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Once the freeze timer fires, we should hide the media session.
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString()));
AdvanceClockMilliseconds(2500);
}
TEST_F(MediaToolbarButtonControllerTest, DoesNotShowUncontrollableSession) {
base::UnguessableToken id = base::UnguessableToken::Create();
EXPECT_CALL(delegate(), Show()).Times(0);
SimulateFocusGained(id, false);
SimulateNecessaryMetadata(id);
}
TEST_F(MediaToolbarButtonControllerTest, ShowsAllInitialControllableSessions) {
base::UnguessableToken controllable1_id = base::UnguessableToken::Create();
base::UnguessableToken uncontrollable_id = base::UnguessableToken::Create();
base::UnguessableToken controllable2_id = base::UnguessableToken::Create();
std::vector<AudioFocusRequestStatePtr> requests;
requests.push_back(CreateFocusRequest(controllable1_id, true));
requests.push_back(CreateFocusRequest(uncontrollable_id, false));
requests.push_back(CreateFocusRequest(controllable2_id, true));
// Having active, controllable sessions should show the toolbar button.
EXPECT_CALL(delegate(), Show());
SimulateReceivedAudioFocusRequests(std::move(requests));
SimulateNecessaryMetadata(controllable1_id);
SimulateNecessaryMetadata(uncontrollable_id);
SimulateNecessaryMetadata(controllable2_id);
testing::Mock::VerifyAndClearExpectations(&delegate());
// If we open a dialog, it should be told to show the controllable sessions,
// but not the uncontrollable one.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate,
ShowMediaSession(controllable1_id.ToString(), _));
EXPECT_CALL(dialog_delegate,
ShowMediaSession(uncontrollable_id.ToString(), _))
.Times(0);
EXPECT_CALL(dialog_delegate,
ShowMediaSession(controllable2_id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Ensure that we properly recorded the number of active sessions shown.
ExpectHistogramCountRecorded(2, 1);
}
TEST_F(MediaToolbarButtonControllerTest, HidesAfterTimeoutAndShowsAgainOnPlay) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, stop playing media so the button is disabled, but hasn't been hidden
// yet.
EXPECT_CALL(delegate(), Disable());
EXPECT_CALL(delegate(), Hide()).Times(0);
SimulateFocusLost(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
// If the time hasn't elapsed yet, the button should still not be hidden.
EXPECT_CALL(delegate(), Hide()).Times(0);
AdvanceClockMilliseconds(2400);
testing::Mock::VerifyAndClearExpectations(&delegate());
// Once the time is elapsed, the button should be hidden.
EXPECT_CALL(delegate(), Hide());
AdvanceClockMilliseconds(200);
testing::Mock::VerifyAndClearExpectations(&delegate());
// If media starts playing again, we should show and enable the button.
EXPECT_CALL(delegate(), Show());
EXPECT_CALL(delegate(), Enable());
SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
}
TEST_F(MediaToolbarButtonControllerTest, DoesNotDisableButtonIfDialogIsOpen) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, open a dialog.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Then, have the session lose focus. This should not disable the button when
// a dialog is present (since the button can still be used to close the
// dialog).
EXPECT_CALL(delegate(), Disable()).Times(0);
SimulateFocusLost(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
}
TEST_F(MediaToolbarButtonControllerTest,
DoesNotHideIfMediaStartsPlayingWithinTimeout) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, stop playing media so the button is disabled, but hasn't been hidden
// yet.
EXPECT_CALL(delegate(), Disable());
EXPECT_CALL(delegate(), Hide()).Times(0);
SimulateFocusLost(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
// If the time hasn't elapsed yet, we should still not be hidden.
EXPECT_CALL(delegate(), Hide()).Times(0);
AdvanceClockMilliseconds(2400);
testing::Mock::VerifyAndClearExpectations(&delegate());
// If media starts playing again, we should show and enable the button.
EXPECT_CALL(delegate(), Show());
EXPECT_CALL(delegate(), Enable());
SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
}
TEST_F(MediaToolbarButtonControllerTest, NewMediaSessionWhileDialogOpen) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, open a dialog.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
ExpectHistogramCountRecorded(1, 1);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Then, have a new media session start while the dialog is opened. This
// should update the dialog.
base::UnguessableToken new_id = base::UnguessableToken::Create();
EXPECT_CALL(dialog_delegate, ShowMediaSession(new_id.ToString(), _));
SimulateFocusGained(new_id, true);
SimulateNecessaryMetadata(new_id);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// If we close this dialog and open a new one, the new one should receive both
// media sessions immediately.
dialog_delegate.Close();
MockMediaDialogDelegate new_dialog;
EXPECT_CALL(new_dialog, ShowMediaSession(id.ToString(), _));
EXPECT_CALL(new_dialog, ShowMediaSession(new_id.ToString(), _));
SimulateDialogOpened(&new_dialog);
ExpectHistogramCountRecorded(1, 1);
ExpectHistogramCountRecorded(2, 1);
}
TEST_F(MediaToolbarButtonControllerTest,
SessionIsRemovedImmediatelyWhenATabCloses) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, close the tab. The button should immediately hide.
EXPECT_CALL(delegate(), Hide());
SimulateTabClosed(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
}
TEST_F(MediaToolbarButtonControllerTest, DismissesMediaSession) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, open a dialog.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Then, click the dismiss button. This should stop and hide the session.
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString()));
SimulateDismissButtonClicked(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
}