blob: c2a0f029e15ae2deac9498222f57b233f4a9111e [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/media_router/cast_dialog_view.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/ui/media_router/cast_dialog_controller.h"
#include "chrome/browser/ui/media_router/cast_dialog_model.h"
#include "chrome/browser/ui/media_router/media_route_starter.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/controls/hover_button.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_coordinator.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_sink_button.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "components/media_router/browser/presentation/start_presentation_context.h"
#include "components/media_router/common/mojom/media_router.mojom.h"
#include "components/prefs/pref_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/base_event_utils.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/widget/widget.h"
using testing::_;
using testing::Invoke;
using testing::Mock;
using testing::NiceMock;
using testing::WithArg;
namespace media_router {
namespace {
UIMediaSink CreateAvailableSink() {
UIMediaSink sink{mojom::MediaRouteProviderId::CAST};
sink.id = "sink_available";
sink.state = UIMediaSinkState::AVAILABLE;
sink.cast_modes = {TAB_MIRROR};
return sink;
}
UIMediaSink CreateConnectedSink() {
UIMediaSink sink{mojom::MediaRouteProviderId::CAST};
sink.id = "sink_connected";
sink.state = UIMediaSinkState::CONNECTED;
sink.cast_modes = {TAB_MIRROR};
sink.route = MediaRoute("route_id", MediaSource("https://example.com"),
sink.id, "", true);
return sink;
}
CastDialogModel CreateModelWithSinks(std::vector<UIMediaSink> sinks) {
CastDialogModel model;
model.set_dialog_header(u"Dialog header");
model.set_media_sinks(std::move(sinks));
return model;
}
ui::MouseEvent CreateMouseEvent() {
return ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(0, 0),
gfx::Point(0, 0), ui::EventTimeForNow(), 0, 0);
}
} // namespace
class MockCastDialogController : public CastDialogController {
public:
MOCK_METHOD1(AddObserver, void(CastDialogController::Observer* observer));
MOCK_METHOD1(RemoveObserver, void(CastDialogController::Observer* observer));
MOCK_METHOD2(StartCasting,
void(const std::string& sink_id, MediaCastMode cast_mode));
MOCK_METHOD1(StopCasting, void(const std::string& route_id));
MOCK_METHOD1(ClearIssue, void(const Issue::Id& issue_id));
MOCK_METHOD0(TakeMediaRouteStarter, std::unique_ptr<MediaRouteStarter>());
};
class CastDialogViewTest : public ChromeViewsTestBase {
protected:
void SetUp() override {
ChromeViewsTestBase::SetUp();
// Create an anchor for the dialog.
anchor_widget_ = CreateTestWidget(views::Widget::InitParams::TYPE_WINDOW);
anchor_widget_->Show();
}
void TearDown() override {
anchor_widget_.reset();
ChromeViewsTestBase::TearDown();
}
void InitializeDialogWithModel(const CastDialogModel& model) {
EXPECT_CALL(controller_, AddObserver(_));
cast_dialog_coordinator_.Show(anchor_widget_->GetContentsView(),
views::BubbleBorder::TOP_RIGHT, &controller_,
&profile_, base::Time::Now(),
MediaRouterDialogActivationLocation::PAGE);
dialog_ = cast_dialog_coordinator_.GetCastDialogView();
dialog_->OnModelUpdated(model);
}
void SinkPressedAtIndex(int index) {
ui::MouseEvent mouse_event(ui::ET_MOUSE_PRESSED, gfx::Point(0, 0),
gfx::Point(0, 0), ui::EventTimeForNow(), 0, 0);
views::test::ButtonTestApi(sink_buttons().at(index))
.NotifyClick(mouse_event);
// The request to cast/stop is sent asynchronously, so we must call
// RunUntilIdle().
base::RunLoop().RunUntilIdle();
}
const std::vector<CastDialogSinkButton*>& sink_buttons() {
return dialog_->sink_buttons_for_test();
}
views::ScrollView* scroll_view() { return dialog_->scroll_view_for_test(); }
views::View* no_sinks_view() { return dialog_->no_sinks_view_for_test(); }
views::Button* sources_button() { return dialog_->sources_button_for_test(); }
HoverButton* access_code_cast_button() {
return dialog_->access_code_cast_button_for_test();
}
ui::SimpleMenuModel* sources_menu_model() {
return dialog_->sources_menu_model_for_test();
}
views::MenuRunner* sources_menu_runner() {
return dialog_->sources_menu_runner_for_test();
}
std::unique_ptr<views::Widget> anchor_widget_;
NiceMock<MockCastDialogController> controller_;
CastDialogCoordinator cast_dialog_coordinator_;
raw_ptr<CastDialogView> dialog_ = nullptr;
TestingProfile profile_;
};
TEST_F(CastDialogViewTest, PopulateDialog) {
CastDialogModel model = CreateModelWithSinks({CreateAvailableSink()});
InitializeDialogWithModel(model);
EXPECT_TRUE(dialog_->ShouldShowCloseButton());
EXPECT_EQ(model.dialog_header(), dialog_->GetWindowTitle());
EXPECT_EQ(ui::DIALOG_BUTTON_NONE, dialog_->GetDialogButtons());
}
TEST_F(CastDialogViewTest, StartCasting) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink(),
CreateAvailableSink()};
media_sinks[0].id = "sink0";
media_sinks[1].id = "sink1";
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
EXPECT_CALL(controller_, StartCasting(model.media_sinks()[0].id, TAB_MIRROR));
SinkPressedAtIndex(0);
}
TEST_F(CastDialogViewTest, StopCasting) {
CastDialogModel model =
CreateModelWithSinks({CreateAvailableSink(), CreateConnectedSink()});
InitializeDialogWithModel(model);
EXPECT_CALL(controller_,
StopCasting(model.media_sinks()[1].route->media_route_id()));
SinkPressedAtIndex(1);
}
TEST_F(CastDialogViewTest, ClearIssue) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink()};
media_sinks[0].issue = Issue(IssueInfo("title", IssueInfo::Action::DISMISS,
IssueInfo::Severity::WARNING));
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
// When there is an issue, clicking on an available sink should clear the
// issue instead of starting casting.
EXPECT_CALL(controller_, StartCasting(_, _)).Times(0);
EXPECT_CALL(controller_, ClearIssue(model.media_sinks()[0].issue->id()));
SinkPressedAtIndex(0);
}
TEST_F(CastDialogViewTest, ShowSourcesMenu) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink()};
media_sinks[0].cast_modes = {TAB_MIRROR, PRESENTATION, DESKTOP_MIRROR};
CastDialogModel model = CreateModelWithSinks(media_sinks);
InitializeDialogWithModel(model);
// Press the button to show the sources menu.
views::test::ButtonTestApi(sources_button()).NotifyClick(CreateMouseEvent());
// The items should be "tab" (includes tab mirroring and presentation) and
// "desktop".
EXPECT_EQ(2u, sources_menu_model()->GetItemCount());
EXPECT_EQ(CastDialogView::kTab, sources_menu_model()->GetCommandIdAt(0));
EXPECT_EQ(CastDialogView::kDesktop, sources_menu_model()->GetCommandIdAt(1));
// When there are no sinks, the sources button should be disabled.
model.set_media_sinks({});
dialog_->OnModelUpdated(model);
EXPECT_FALSE(sources_button()->GetEnabled());
}
TEST_F(CastDialogViewTest, CastAlternativeSources) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink()};
media_sinks[0].cast_modes = {TAB_MIRROR, DESKTOP_MIRROR};
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
// Press the button to show the sources menu.
views::test::ButtonTestApi(sources_button()).NotifyClick(CreateMouseEvent());
// There should be two sources: tab and desktop.
ASSERT_EQ(2u, sources_menu_model()->GetItemCount());
EXPECT_CALL(controller_, StartCasting(model.media_sinks()[0].id, TAB_MIRROR));
sources_menu_model()->ActivatedAt(0);
SinkPressedAtIndex(0);
Mock::VerifyAndClearExpectations(&controller_);
EXPECT_CALL(controller_,
StartCasting(model.media_sinks()[0].id, DESKTOP_MIRROR));
sources_menu_model()->ActivatedAt(1);
SinkPressedAtIndex(0);
}
TEST_F(CastDialogViewTest, DisableUnsupportedSinks) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink(),
CreateAvailableSink()};
media_sinks[1].id = "sink_2";
media_sinks[0].cast_modes = {TAB_MIRROR};
media_sinks[1].cast_modes = {PRESENTATION, DESKTOP_MIRROR};
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
views::test::ButtonTestApi test_api(sources_button());
test_api.NotifyClick(CreateMouseEvent());
EXPECT_EQ(CastDialogView::kDesktop, sources_menu_model()->GetCommandIdAt(1));
sources_menu_model()->ActivatedAt(1);
// Sink at index 0 doesn't support desktop mirroring, so it should be
// disabled.
EXPECT_FALSE(sink_buttons().at(0)->GetEnabled());
EXPECT_TRUE(sink_buttons().at(1)->GetEnabled());
test_api.NotifyClick(CreateMouseEvent());
EXPECT_EQ(CastDialogView::kTab, sources_menu_model()->GetCommandIdAt(0));
sources_menu_model()->ActivatedAt(0);
// Both sinks support tab or presentation casting, so they should be enabled.
EXPECT_TRUE(sink_buttons().at(0)->GetEnabled());
EXPECT_TRUE(sink_buttons().at(1)->GetEnabled());
}
TEST_F(CastDialogViewTest, ShowNoDeviceView) {
CastDialogModel model;
InitializeDialogWithModel(model);
// The no-device view should be shown when there are no sinks.
EXPECT_TRUE(no_sinks_view()->GetVisible());
EXPECT_FALSE(scroll_view());
std::vector<UIMediaSink> media_sinks = {CreateConnectedSink()};
model.set_media_sinks(std::move(media_sinks));
dialog_->OnModelUpdated(model);
// The scroll view should be shown when there are sinks.
EXPECT_FALSE(no_sinks_view());
EXPECT_TRUE(scroll_view()->GetVisible());
}
TEST_F(CastDialogViewTest, SwitchToNoDeviceView) {
// Start with one sink. The sink list scroll view should be shown.
CastDialogModel model = CreateModelWithSinks({CreateAvailableSink()});
InitializeDialogWithModel(model);
EXPECT_TRUE(scroll_view()->GetVisible());
EXPECT_FALSE(no_sinks_view());
// Remove the sink. The no-device view should be shown.
model.set_media_sinks({});
dialog_->OnModelUpdated(model);
EXPECT_TRUE(no_sinks_view()->GetVisible());
EXPECT_FALSE(scroll_view());
}
TEST_F(CastDialogViewTest, ShowAccessCodeCastButtonDisabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(features::kAccessCodeCastUI);
profile_.GetPrefs()->SetBoolean(prefs::kAccessCodeCastEnabled, false);
CastDialogModel model = CreateModelWithSinks({CreateAvailableSink()});
InitializeDialogWithModel(model);
EXPECT_FALSE(access_code_cast_button());
}
TEST_F(CastDialogViewTest, ShowAccessCodeCastButtonEnabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(features::kAccessCodeCastUI);
profile_.GetPrefs()->SetBoolean(prefs::kAccessCodeCastEnabled, true);
CastDialogModel model = CreateModelWithSinks({CreateAvailableSink()});
InitializeDialogWithModel(model);
EXPECT_TRUE(access_code_cast_button());
}
// This test demonstrates that when the access code casting feature is
// available to the user, that the sources button is available even if no
// sinks are available.
TEST_F(CastDialogViewTest, AccessCodeEmptySinksSourcesAvailable) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(features::kAccessCodeCastUI);
profile_.GetPrefs()->SetBoolean(prefs::kAccessCodeCastEnabled, false);
CastDialogModel model;
InitializeDialogWithModel(model);
// With policy disabled, button is still disabled even with feature enabled.
EXPECT_FALSE(sources_button()->GetEnabled());
// But with policy enabled, button is now enabled even with no sinks.
profile_.GetPrefs()->SetBoolean(prefs::kAccessCodeCastEnabled, true);
dialog_->OnModelUpdated(model);
EXPECT_TRUE(sources_button()->GetEnabled());
}
} // namespace media_router