blob: 3db607b441b6d6118d581b6abb91fd48a78a5516 [file] [log] [blame]
// Copyright 2020 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 "components/media_message_center/media_notification_view_modern_impl.h"
#include <memory>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/containers/flat_set.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "components/media_message_center/media_controls_progress_view.h"
#include "components/media_message_center/media_notification_background_impl.h"
#include "components/media_message_center/media_notification_container.h"
#include "components/media_message_center/media_notification_util.h"
#include "components/media_message_center/mock_media_notification_item.h"
#include "services/media_session/public/mojom/media_session.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/events/base_event_utils.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/views/notification_control_buttons_view.h"
#include "ui/message_center/views/notification_header_view.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/test/views_test_base.h"
namespace media_message_center {
using media_session::mojom::MediaSessionAction;
using testing::_;
using testing::Expectation;
using testing::Invoke;
using testing::Return;
namespace {
const int kMediaButtonIconSize = 20;
const int kPipButtonIconSize = 18;
const gfx::Size kWidgetSize(500, 500);
constexpr int kViewWidth = 350;
const gfx::Size kViewSize(kViewWidth, 400);
class MockMediaNotificationContainer : public MediaNotificationContainer {
public:
MockMediaNotificationContainer() = default;
MockMediaNotificationContainer(const MockMediaNotificationContainer&) =
delete;
MockMediaNotificationContainer& operator=(
const MockMediaNotificationContainer&) = delete;
~MockMediaNotificationContainer() override = default;
// MediaNotificationContainer implementation.
MOCK_METHOD1(OnExpanded, void(bool expanded));
MOCK_METHOD1(
OnMediaSessionInfoChanged,
void(const media_session::mojom::MediaSessionInfoPtr& session_info));
MOCK_METHOD1(OnMediaSessionMetadataChanged,
void(const media_session::MediaMetadata& metadata));
MOCK_METHOD1(OnVisibleActionsChanged,
void(const base::flat_set<MediaSessionAction>& actions));
MOCK_METHOD1(OnMediaArtworkChanged, void(const gfx::ImageSkia& image));
MOCK_METHOD3(OnColorsChanged,
void(SkColor foreground,
SkColor foreground_disabled,
SkColor background));
MOCK_METHOD0(OnHeaderClicked, void());
};
} // namespace
class MediaNotificationViewModernImplTest : public views::ViewsTestBase {
public:
MediaNotificationViewModernImplTest()
: views::ViewsTestBase(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
MediaNotificationViewModernImplTest(
const MediaNotificationViewModernImplTest&) = delete;
MediaNotificationViewModernImplTest& operator=(
const MediaNotificationViewModernImplTest&) = delete;
~MediaNotificationViewModernImplTest() override = default;
void SetUp() override {
views::ViewsTestBase::SetUp();
// Create a widget to show on the screen for testing screen coordinates and
// focus.
widget_ = std::make_unique<views::Widget>();
views::Widget::InitParams params =
CreateParams(views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.bounds = gfx::Rect(kWidgetSize);
widget_->Init(std::move(params));
widget_->Show();
// Creates the view and adds it to the widget.
CreateView();
}
void TearDown() override {
view_ = nullptr;
widget_.reset();
actions_.clear();
views::ViewsTestBase::TearDown();
}
void EnableAllActions() {
actions_.insert(MediaSessionAction::kPlay);
actions_.insert(MediaSessionAction::kPause);
actions_.insert(MediaSessionAction::kPreviousTrack);
actions_.insert(MediaSessionAction::kNextTrack);
actions_.insert(MediaSessionAction::kSeekBackward);
actions_.insert(MediaSessionAction::kSeekForward);
actions_.insert(MediaSessionAction::kStop);
actions_.insert(MediaSessionAction::kEnterPictureInPicture);
actions_.insert(MediaSessionAction::kExitPictureInPicture);
NotifyUpdatedActions();
}
void EnableAction(MediaSessionAction action) {
actions_.insert(action);
NotifyUpdatedActions();
}
void DisableAction(MediaSessionAction action) {
actions_.erase(action);
NotifyUpdatedActions();
}
MockMediaNotificationContainer& container() { return container_; }
MediaNotificationViewModernImpl* view() const { return view_; }
const std::u16string& accessible_name() const {
return view()->accessible_name_;
}
test::MockMediaNotificationItem& item() { return item_; }
views::Label* title_label() const { return view()->title_label_; }
views::Label* subtitle_label() const { return view()->subtitle_label_; }
views::View* artwork_container() const { return view()->artwork_container_; }
views::View* media_controls_container() const {
return view()->media_controls_container_;
}
views::Button* picture_in_picture_button() const {
return view()->picture_in_picture_button_for_testing();
}
std::vector<views::Button*> media_control_buttons() const {
std::vector<views::Button*> buttons;
auto children = view()->media_controls_container_->children();
std::transform(
children.begin(), children.end(), std::back_inserter(buttons),
[](views::View* child) { return views::Button::AsButton(child); });
buttons.push_back(views::Button::AsButton(picture_in_picture_button()));
return buttons;
}
MediaControlsProgressView* progress_view() const { return view()->progress_; }
views::Button* GetButtonForAction(MediaSessionAction action) const {
auto buttons = media_control_buttons();
const auto i = std::find_if(
buttons.begin(), buttons.end(), [action](const views::Button* button) {
return button->tag() == static_cast<int>(action);
});
return (i == buttons.end()) ? nullptr : *i;
}
bool IsActionButtonVisible(MediaSessionAction action) const {
return GetButtonForAction(action)->GetVisible();
}
const gfx::ImageSkia& GetArtworkImage() const {
return static_cast<MediaNotificationBackgroundImpl*>(
view()->GetMediaNotificationBackground())
->artwork_;
}
void SimulateButtonClick(MediaSessionAction action) {
views::Button* button = GetButtonForAction(action);
EXPECT_TRUE(button->GetVisible());
views::test::ButtonTestApi(button).NotifyClick(
ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
ui::EventTimeForNow(), 0, 0));
}
void SimulateTab() {
ui::KeyEvent pressed_tab(ui::ET_KEY_PRESSED, ui::VKEY_TAB, ui::EF_NONE);
view()->GetFocusManager()->OnKeyEvent(pressed_tab);
}
void ExpectHistogramArtworkRecorded(bool present, int count) {
histogram_tester_.ExpectBucketCount(
MediaNotificationViewModernImpl::kArtworkHistogramName,
static_cast<base::HistogramBase::Sample>(present), count);
}
void ExpectHistogramMetadataRecorded(
MediaNotificationViewModernImpl::Metadata metadata,
int count) {
histogram_tester_.ExpectBucketCount(
MediaNotificationViewModernImpl::kMetadataHistogramName,
static_cast<base::HistogramBase::Sample>(metadata), count);
}
private:
void NotifyUpdatedActions() { view_->UpdateWithMediaActions(actions_); }
void CreateView() {
// On creation, the view should notify |item_|.
auto view = std::make_unique<MediaNotificationViewModernImpl>(
&container_, item_.GetWeakPtr(), std::make_unique<views::View>(),
std::make_unique<views::View>(), kViewWidth);
view->SetSize(kViewSize);
media_session::MediaMetadata metadata;
metadata.title = u"title";
metadata.artist = u"artist";
metadata.source_title = u"source title";
view->UpdateWithMediaMetadata(metadata);
view->UpdateWithMediaActions(actions_);
// Display it in |widget_|. Widget now owns |view|.
view_ = widget_->SetContentsView(std::move(view));
}
base::HistogramTester histogram_tester_;
base::flat_set<MediaSessionAction> actions_;
MockMediaNotificationContainer container_;
test::MockMediaNotificationItem item_;
raw_ptr<MediaNotificationViewModernImpl> view_;
std::unique_ptr<views::Widget> widget_;
};
TEST_F(MediaNotificationViewModernImplTest, ButtonsSanityCheck) {
EnableAllActions();
EXPECT_TRUE(media_controls_container()->GetVisible());
EXPECT_GT(media_controls_container()->width(), 0);
EXPECT_GT(media_controls_container()->height(), 0);
auto buttons = media_control_buttons();
EXPECT_EQ(6u, buttons.size());
for (auto* button : buttons) {
EXPECT_TRUE(button->GetVisible());
if (button == picture_in_picture_button()) {
EXPECT_LT(kPipButtonIconSize, button->width());
EXPECT_LT(kPipButtonIconSize, button->height());
} else {
EXPECT_LT(kMediaButtonIconSize, button->width());
EXPECT_LT(kMediaButtonIconSize, button->height());
}
EXPECT_FALSE(views::Button::AsButton(button)->GetAccessibleName().empty());
}
EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay));
EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPreviousTrack));
EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kNextTrack));
EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kSeekBackward));
EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kSeekForward));
EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kEnterPictureInPicture));
// |kPause| cannot be present if |kPlay| is.
EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause));
EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kExitPictureInPicture));
}
#if BUILDFLAG(IS_WIN)
#define MAYBE_ButtonsFocusCheck DISABLED_ButtonsFocusCheck
#else
#define MAYBE_ButtonsFocusCheck ButtonsFocusCheck
#endif
TEST_F(MediaNotificationViewModernImplTest, MAYBE_ButtonsFocusCheck) {
// Expand and enable all actions to show all buttons.
EnableAllActions();
views::FocusManager* focus_manager = view()->GetFocusManager();
{
// Focus the first action button.
auto* button = GetButtonForAction(MediaSessionAction::kPreviousTrack);
focus_manager->SetFocusedView(button);
EXPECT_EQ(button, focus_manager->GetFocusedView());
}
SimulateTab();
EXPECT_EQ(GetButtonForAction(MediaSessionAction::kSeekBackward),
focus_manager->GetFocusedView());
SimulateTab();
EXPECT_EQ(GetButtonForAction(MediaSessionAction::kPlay),
focus_manager->GetFocusedView());
SimulateTab();
EXPECT_EQ(GetButtonForAction(MediaSessionAction::kSeekForward),
focus_manager->GetFocusedView());
SimulateTab();
EXPECT_EQ(GetButtonForAction(MediaSessionAction::kNextTrack),
focus_manager->GetFocusedView());
}
TEST_F(MediaNotificationViewModernImplTest, PlayPauseButtonTooltipCheck) {
EnableAction(MediaSessionAction::kPlay);
EnableAction(MediaSessionAction::kPause);
EXPECT_CALL(container(), OnMediaSessionInfoChanged(_));
auto* button = GetButtonForAction(MediaSessionAction::kPlay);
std::u16string tooltip = button->GetTooltipText(gfx::Point());
EXPECT_FALSE(tooltip.empty());
auto session_info = media_session::mojom::MediaSessionInfo::New();
session_info->playback_state =
media_session::mojom::MediaPlaybackState::kPlaying;
session_info->is_controllable = true;
view()->UpdateWithMediaSessionInfo(std::move(session_info));
std::u16string new_tooltip = button->GetTooltipText(gfx::Point());
EXPECT_FALSE(new_tooltip.empty());
EXPECT_NE(tooltip, new_tooltip);
}
TEST_F(MediaNotificationViewModernImplTest, NextTrackButtonClick) {
EnableAction(MediaSessionAction::kNextTrack);
EXPECT_CALL(item(), OnMediaSessionActionButtonPressed(
MediaSessionAction::kNextTrack));
SimulateButtonClick(MediaSessionAction::kNextTrack);
}
TEST_F(MediaNotificationViewModernImplTest, PlayButtonClick) {
EnableAction(MediaSessionAction::kPlay);
EXPECT_CALL(item(),
OnMediaSessionActionButtonPressed(MediaSessionAction::kPlay));
SimulateButtonClick(MediaSessionAction::kPlay);
}
TEST_F(MediaNotificationViewModernImplTest, PauseButtonClick) {
EnableAction(MediaSessionAction::kPause);
auto session_info = media_session::mojom::MediaSessionInfo::New();
session_info->playback_state =
media_session::mojom::MediaPlaybackState::kPlaying;
session_info->is_controllable = true;
EXPECT_CALL(container(), OnMediaSessionInfoChanged(_));
view()->UpdateWithMediaSessionInfo(session_info.Clone());
testing::Mock::VerifyAndClearExpectations(&container());
EXPECT_CALL(item(),
OnMediaSessionActionButtonPressed(MediaSessionAction::kPause));
SimulateButtonClick(MediaSessionAction::kPause);
}
TEST_F(MediaNotificationViewModernImplTest, PreviousTrackButtonClick) {
EnableAction(MediaSessionAction::kPreviousTrack);
EXPECT_CALL(item(), OnMediaSessionActionButtonPressed(
MediaSessionAction::kPreviousTrack));
SimulateButtonClick(MediaSessionAction::kPreviousTrack);
}
TEST_F(MediaNotificationViewModernImplTest, SeekBackwardButtonClick) {
EnableAction(MediaSessionAction::kSeekBackward);
EXPECT_CALL(item(), OnMediaSessionActionButtonPressed(
MediaSessionAction::kSeekBackward));
SimulateButtonClick(MediaSessionAction::kSeekBackward);
}
TEST_F(MediaNotificationViewModernImplTest, SeekForwardButtonClick) {
EnableAction(MediaSessionAction::kSeekForward);
EXPECT_CALL(item(), OnMediaSessionActionButtonPressed(
MediaSessionAction::kSeekForward));
SimulateButtonClick(MediaSessionAction::kSeekForward);
}
TEST_F(MediaNotificationViewModernImplTest, PlayToggle_FromObserver_Empty) {
EnableAction(MediaSessionAction::kPlay);
{
views::Button* button = GetButtonForAction(MediaSessionAction::kPlay);
EXPECT_NE(button, nullptr);
EXPECT_EQ(button->tag(), static_cast<int>(MediaSessionAction::kPlay));
}
view()->UpdateWithMediaSessionInfo(
media_session::mojom::MediaSessionInfo::New());
{
views::Button* button = GetButtonForAction(MediaSessionAction::kPlay);
EXPECT_NE(button, nullptr);
EXPECT_EQ(button->tag(), static_cast<int>(MediaSessionAction::kPlay));
}
}
TEST_F(MediaNotificationViewModernImplTest,
PlayToggle_FromObserver_PlaybackState) {
EnableAction(MediaSessionAction::kPlay);
EnableAction(MediaSessionAction::kPause);
{
views::Button* button = GetButtonForAction(MediaSessionAction::kPlay);
EXPECT_NE(button, nullptr);
EXPECT_EQ(button->tag(), static_cast<int>(MediaSessionAction::kPlay));
}
media_session::mojom::MediaSessionInfoPtr session_info(
media_session::mojom::MediaSessionInfo::New());
session_info->playback_state =
media_session::mojom::MediaPlaybackState::kPlaying;
view()->UpdateWithMediaSessionInfo(session_info.Clone());
{
views::Button* button = GetButtonForAction(MediaSessionAction::kPause);
EXPECT_NE(button, nullptr);
EXPECT_EQ(button->tag(), static_cast<int>(MediaSessionAction::kPause));
}
session_info->playback_state =
media_session::mojom::MediaPlaybackState::kPaused;
view()->UpdateWithMediaSessionInfo(session_info.Clone());
{
views::Button* button = GetButtonForAction(MediaSessionAction::kPlay);
EXPECT_NE(button, nullptr);
EXPECT_EQ(button->tag(), static_cast<int>(MediaSessionAction::kPlay));
}
}
TEST_F(MediaNotificationViewModernImplTest, MetadataIsDisplayed) {
EnableAllActions();
EXPECT_TRUE(title_label()->GetVisible());
EXPECT_TRUE(subtitle_label()->GetVisible());
EXPECT_EQ(u"title", title_label()->GetText());
EXPECT_EQ(u"source title", subtitle_label()->GetText());
}
TEST_F(MediaNotificationViewModernImplTest, UpdateMetadata_FromObserver) {
EnableAllActions();
ExpectHistogramMetadataRecorded(
MediaNotificationViewModernImpl::Metadata::kTitle, 1);
ExpectHistogramMetadataRecorded(
MediaNotificationViewModernImpl::Metadata::kSource, 1);
ExpectHistogramMetadataRecorded(
MediaNotificationViewModernImpl::Metadata::kCount, 1);
media_session::MediaMetadata metadata;
metadata.title = u"title2";
metadata.source_title = u"source title2";
metadata.artist = u"artist2";
metadata.album = u"album";
EXPECT_CALL(container(), OnMediaSessionMetadataChanged(_));
view()->UpdateWithMediaMetadata(metadata);
testing::Mock::VerifyAndClearExpectations(&container());
EXPECT_TRUE(title_label()->GetVisible());
EXPECT_TRUE(subtitle_label()->GetVisible());
EXPECT_EQ(metadata.title, title_label()->GetText());
EXPECT_EQ(metadata.source_title, subtitle_label()->GetText());
EXPECT_EQ(u"title2 - artist2 - album", accessible_name());
ExpectHistogramMetadataRecorded(
MediaNotificationViewModernImpl::Metadata::kTitle, 2);
ExpectHistogramMetadataRecorded(
MediaNotificationViewModernImpl::Metadata::kSource, 2);
ExpectHistogramMetadataRecorded(
MediaNotificationViewModernImpl::Metadata::kCount, 2);
}
TEST_F(MediaNotificationViewModernImplTest, ActionButtonsHiddenByDefault) {
EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kPlay));
EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kNextTrack));
EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kPreviousTrack));
EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kSeekForward));
EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kSeekBackward));
}
TEST_F(MediaNotificationViewModernImplTest, ActionButtonsToggleVisibility) {
EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kNextTrack));
EnableAction(MediaSessionAction::kNextTrack);
EXPECT_TRUE(IsActionButtonVisible(MediaSessionAction::kNextTrack));
DisableAction(MediaSessionAction::kNextTrack);
EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kNextTrack));
}
TEST_F(MediaNotificationViewModernImplTest, UpdateArtworkFromItem) {
int labels_container_width = title_label()->parent()->width();
gfx::Size size = view()->size();
EXPECT_CALL(container(), OnMediaArtworkChanged(_)).Times(2);
EXPECT_CALL(container(), OnColorsChanged(_, _, _)).Times(2);
SkBitmap image;
image.allocN32Pixels(10, 10);
image.eraseColor(SK_ColorGREEN);
EXPECT_TRUE(GetArtworkImage().isNull());
view()->UpdateWithMediaArtwork(gfx::ImageSkia::CreateFrom1xBitmap(image));
ExpectHistogramArtworkRecorded(true, 1);
// The size of the labels container should not change when there is artwork.
EXPECT_EQ(labels_container_width, title_label()->parent()->width());
// Ensure that the labels container does not extend into the artwork bounds.
EXPECT_FALSE(artwork_container()->bounds().Intersects(
title_label()->parent()->bounds()));
// Ensure that when the image is displayed that the size of the notification
// was not affected.
EXPECT_FALSE(GetArtworkImage().isNull());
EXPECT_EQ(gfx::Size(10, 10), GetArtworkImage().size());
EXPECT_EQ(size, view()->size());
view()->UpdateWithMediaArtwork(
gfx::ImageSkia::CreateFrom1xBitmap(SkBitmap()));
ExpectHistogramArtworkRecorded(false, 1);
// Ensure the labels container goes back to the original width now that we
// do not have any artwork.
EXPECT_EQ(labels_container_width, title_label()->parent()->width());
// Ensure that the artwork was reset and the size was still not
// affected.
EXPECT_TRUE(GetArtworkImage().isNull());
EXPECT_EQ(size, view()->size());
}
TEST_F(MediaNotificationViewModernImplTest, UpdateProgressBar) {
media_session::MediaPosition media_position(
/*playback_rate=*/1.0, /*duration=*/base::Seconds(600),
/*position=*/base::Seconds(0), /*end_of_media=*/false);
view()->UpdateWithMediaPosition(media_position);
EXPECT_EQ(progress_view()->duration_for_testing(), u"10:00");
}
TEST_F(MediaNotificationViewModernImplTest, AccessibleNodeData) {
ui::AXNodeData data;
view()->GetAccessibleNodeData(&data);
EXPECT_TRUE(
data.HasStringAttribute(ax::mojom::StringAttribute::kRoleDescription));
EXPECT_EQ(u"title - artist", accessible_name());
}
class MediaNotificationViewModernImplCastTest
: public MediaNotificationViewModernImplTest {
public:
void SetUp() override {
EXPECT_CALL(item(), SourceType())
.WillRepeatedly(Return(media_message_center::SourceType::kCast));
MediaNotificationViewModernImplTest::SetUp();
}
};
TEST_F(MediaNotificationViewModernImplCastTest, PictureInPictureButton) {
// We should not create picture-in-picture button for cast session.
EXPECT_EQ(picture_in_picture_button(), nullptr);
}
} // namespace media_message_center