blob: c85d61e13268690a8703355b00680277f62fb2c0 [file] [log] [blame]
// Copyright 2018 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/views/media_router/cast_dialog_view.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.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/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_sink_button.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/views/chrome_views_test_base.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/widget/widget.h"
#include "ui/views/window/dialog_client_view.h"
using testing::_;
using testing::Invoke;
using testing::Mock;
using testing::WithArg;
namespace media_router {
namespace {
UIMediaSink CreateAvailableSink() {
UIMediaSink sink;
sink.id = "sink_available";
sink.state = UIMediaSinkState::AVAILABLE;
sink.cast_modes = {TAB_MIRROR};
return sink;
}
UIMediaSink CreateConnectedSink() {
UIMediaSink sink;
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, true);
return sink;
}
CastDialogModel CreateModelWithSinks(std::vector<UIMediaSink> sinks) {
CastDialogModel model;
model.set_dialog_header(base::UTF8ToUTF16("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(
ChooseLocalFile,
void(base::OnceCallback<void(const ui::SelectedFileInfo*)> callback));
};
class CastDialogViewTest : public ChromeViewsTestBase {
protected:
void SetUp() override {
ChromeViewsTestBase::SetUp();
// Create an anchor for the dialog.
views::Widget::InitParams params =
CreateParams(views::Widget::InitParams::TYPE_WINDOW);
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
anchor_widget_ = std::make_unique<views::Widget>();
anchor_widget_->Init(params);
anchor_widget_->Show();
}
void TearDown() override {
anchor_widget_.reset();
ChromeViewsTestBase::TearDown();
}
void InitializeDialogWithModel(const CastDialogModel& model) {
EXPECT_CALL(controller_, AddObserver(_))
.WillOnce(
WithArg<0>(Invoke([this](CastDialogController::Observer* observer) {
dialog_ = static_cast<CastDialogView*>(observer);
})));
CastDialogView::ShowDialog(anchor_widget_->GetContentsView(),
views::BubbleBorder::TOP_RIGHT, &controller_,
nullptr, base::Time::Now());
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);
dialog_->ButtonPressed(sink_buttons().at(index), 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(); }
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_;
MockCastDialogController controller_;
CastDialogView* dialog_ = nullptr;
};
TEST_F(CastDialogViewTest, ShowAndHideDialog) {
EXPECT_FALSE(CastDialogView::IsShowing());
EXPECT_EQ(nullptr, CastDialogView::GetCurrentDialogWidget());
EXPECT_CALL(controller_, AddObserver(_));
CastDialogView::ShowDialog(anchor_widget_->GetContentsView(),
views::BubbleBorder::TOP_RIGHT, &controller_,
nullptr, base::Time::Now());
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(CastDialogView::IsShowing());
EXPECT_NE(nullptr, CastDialogView::GetCurrentDialogWidget());
EXPECT_CALL(controller_, RemoveObserver(_));
CastDialogView::HideDialog();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(CastDialogView::IsShowing());
EXPECT_EQ(nullptr, CastDialogView::GetCurrentDialogWidget());
}
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, 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.
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
// The items should be "tab" (includes tab mirroring and presentation),
// "desktop", and "local file".
EXPECT_EQ(3, sources_menu_model()->GetItemCount());
EXPECT_EQ(CastDialogView::kTab, sources_menu_model()->GetCommandIdAt(0));
EXPECT_EQ(CastDialogView::kDesktop, sources_menu_model()->GetCommandIdAt(1));
EXPECT_EQ(CastDialogView::kLocalFile,
sources_menu_model()->GetCommandIdAt(2));
// When there are no sinks, the sources button should be disabled.
model.set_media_sinks({});
dialog_->OnModelUpdated(model);
EXPECT_FALSE(sources_button()->enabled());
}
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.
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
// There should be three sources: tab, desktop, and local file.
ASSERT_EQ(3, 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, CastLocalFile) {
const std::string file_name = "example.mp4";
const std::string file_path = "path/to/" + file_name;
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink()};
media_sinks[0].cast_modes = {TAB_MIRROR, LOCAL_FILE};
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
#if defined(OS_WIN)
ui::SelectedFileInfo file_info{base::FilePath(base::UTF8ToUTF16(file_name)),
base::FilePath(base::UTF8ToUTF16(file_path))};
#else
ui::SelectedFileInfo file_info{base::FilePath(file_name),
base::FilePath(file_path)};
#endif // defined(OS_WIN)
EXPECT_CALL(controller_, ChooseLocalFile(_))
.WillOnce(
[file_info](base::OnceCallback<void(const ui::SelectedFileInfo*)>
file_callback) {
std::move(file_callback).Run(&file_info);
});
ASSERT_EQ(CastDialogView::kLocalFile,
sources_menu_model()->GetCommandIdAt(2));
sources_menu_model()->ActivatedAt(2);
EXPECT_EQ(dialog_->GetWindowTitle(),
l10n_util::GetStringFUTF16(IDS_MEDIA_ROUTER_CAST_LOCAL_MEDIA_TITLE,
base::UTF8ToUTF16(file_name)));
EXPECT_CALL(controller_, StartCasting(model.media_sinks()[0].id, LOCAL_FILE));
SinkPressedAtIndex(0);
}
TEST_F(CastDialogViewTest, CancelLocalFileSelection) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink()};
media_sinks[0].cast_modes = {TAB_MIRROR, LOCAL_FILE};
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
// The tab source should be selected by default.
ASSERT_EQ(CastDialogView::kTab, sources_menu_model()->GetCommandIdAt(0));
ASSERT_TRUE(sources_menu_model()->IsItemCheckedAt(0));
// Select the local file source, then cancel file selection by passing a
// nullptr into the callback.
EXPECT_CALL(controller_, ChooseLocalFile(_))
.WillOnce(
[](base::OnceCallback<void(const ui::SelectedFileInfo*)>
file_callback) { std::move(file_callback).Run(nullptr); });
ASSERT_EQ(CastDialogView::kLocalFile,
sources_menu_model()->GetCommandIdAt(2));
sources_menu_model()->ActivatedAt(2);
// Since we cancelled file selection, "tab" should still be the selected
// source.
EXPECT_TRUE(sources_menu_model()->IsItemCheckedAt(0));
EXPECT_CALL(controller_, StartCasting(model.media_sinks()[0].id, TAB_MIRROR));
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);
dialog_->ButtonPressed(sources_button(), 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)->enabled());
EXPECT_TRUE(sink_buttons().at(1)->enabled());
dialog_->ButtonPressed(sources_button(), 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)->enabled());
EXPECT_TRUE(sink_buttons().at(1)->enabled());
}
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()->visible());
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()->visible());
}
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()->visible());
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()->visible());
EXPECT_FALSE(scroll_view());
}
} // namespace media_router