| // 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/media_router_views_ui.h" |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/media/router/media_sinks_observer.h" |
| #include "chrome/browser/media/router/test/mock_media_router.h" |
| #include "chrome/browser/media/router/test/test_helper.h" |
| #include "chrome/browser/sessions/session_tab_helper.h" |
| #include "chrome/browser/ui/media_router/cast_dialog_controller.h" |
| #include "chrome/browser/ui/media_router/media_cast_mode.h" |
| #include "chrome/common/media_router/media_source_helper.h" |
| #include "chrome/common/media_router/route_request_result.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/origin.h" |
| |
| using testing::_; |
| using testing::Invoke; |
| using testing::WithArg; |
| |
| namespace media_router { |
| |
| namespace { |
| |
| constexpr char kPseudoSinkId[] = "pseudo:sink"; |
| constexpr char kRouteId[] = "route1"; |
| constexpr char kSinkId[] = "sink1"; |
| constexpr char kSinkName[] = "sink name"; |
| constexpr char kSourceId[] = "source1"; |
| |
| } // namespace |
| |
| class MockControllerObserver : public CastDialogController::Observer { |
| public: |
| MOCK_METHOD1(OnModelUpdated, void(const CastDialogModel& model)); |
| MOCK_METHOD0(OnControllerInvalidated, void()); |
| }; |
| |
| // Injects a MediaRouter instance into MediaRouterViewsUI. |
| class TestMediaRouterViewsUI : public MediaRouterViewsUI { |
| public: |
| explicit TestMediaRouterViewsUI(MediaRouter* router) : router_(router) {} |
| ~TestMediaRouterViewsUI() override = default; |
| |
| MediaRouter* GetMediaRouter() const override { return router_; } |
| |
| private: |
| MediaRouter* router_; |
| DISALLOW_COPY_AND_ASSIGN(TestMediaRouterViewsUI); |
| }; |
| |
| class MediaRouterViewsUITest : public ChromeRenderViewHostTestHarness { |
| public: |
| void SetUp() override { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| |
| // Store sink observers so that they can be notified in tests. |
| ON_CALL(mock_router_, RegisterMediaSinksObserver(_)) |
| .WillByDefault(Invoke([this](MediaSinksObserver* observer) { |
| media_sinks_observers_.push_back(observer); |
| return true; |
| })); |
| |
| SessionTabHelper::CreateForWebContents(web_contents()); |
| ui_ = std::make_unique<TestMediaRouterViewsUI>(&mock_router_); |
| ui_->InitWithDefaultMediaSource(web_contents(), nullptr); |
| } |
| |
| void TearDown() override { |
| ui_.reset(); |
| ChromeRenderViewHostTestHarness::TearDown(); |
| } |
| |
| protected: |
| std::vector<MediaSinksObserver*> media_sinks_observers_; |
| MockMediaRouter mock_router_; |
| std::unique_ptr<MediaRouterViewsUI> ui_; |
| }; |
| |
| TEST_F(MediaRouterViewsUITest, NotifyObserver) { |
| MockControllerObserver observer; |
| |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>(Invoke([](const CastDialogModel& model) { |
| EXPECT_TRUE(model.media_sinks().empty()); |
| }))); |
| ui_->AddObserver(&observer); |
| |
| MediaSink sink(kSinkId, kSinkName, SinkIconType::CAST_AUDIO); |
| MediaSinkWithCastModes sink_with_cast_modes(sink); |
| sink_with_cast_modes.cast_modes = {MediaCastMode::TAB_MIRROR}; |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>(Invoke([&sink](const CastDialogModel& model) { |
| EXPECT_EQ(1u, model.media_sinks().size()); |
| const UIMediaSink& ui_sink = model.media_sinks()[0]; |
| EXPECT_EQ(sink.id(), ui_sink.id); |
| EXPECT_EQ(base::UTF8ToUTF16(sink.name()), ui_sink.friendly_name); |
| EXPECT_EQ(UIMediaSinkState::AVAILABLE, ui_sink.state); |
| EXPECT_TRUE( |
| base::ContainsKey(ui_sink.cast_modes, MediaCastMode::TAB_MIRROR)); |
| EXPECT_EQ(sink.icon_type(), ui_sink.icon_type); |
| }))); |
| ui_->OnResultsUpdated({sink_with_cast_modes}); |
| |
| MediaRoute route(kRouteId, MediaSource(kSourceId), kSinkId, "", true, true); |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce( |
| WithArg<0>(Invoke([&sink, &route](const CastDialogModel& model) { |
| EXPECT_EQ(1u, model.media_sinks().size()); |
| const UIMediaSink& ui_sink = model.media_sinks()[0]; |
| EXPECT_EQ(sink.id(), ui_sink.id); |
| EXPECT_EQ(UIMediaSinkState::CONNECTED, ui_sink.state); |
| EXPECT_EQ(route.media_route_id(), ui_sink.route_id); |
| }))); |
| ui_->OnRoutesUpdated({route}, {}); |
| |
| EXPECT_CALL(observer, OnControllerInvalidated()); |
| ui_.reset(); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, StartCasting) { |
| MediaSource media_source = |
| MediaSourceForTab(SessionTabHelper::IdForTab(web_contents()).id()); |
| EXPECT_CALL(mock_router_, |
| CreateRouteInternal(media_source.id(), kSinkId, _, web_contents(), |
| _, base::TimeDelta::FromSeconds(60), false)); |
| MediaSink sink(kSinkId, kSinkName, SinkIconType::GENERIC); |
| for (MediaSinksObserver* observer : media_sinks_observers_) |
| observer->OnSinksUpdated({sink}, std::vector<url::Origin>()); |
| ui_->StartCasting(kSinkId, MediaCastMode::TAB_MIRROR); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, StopCasting) { |
| EXPECT_CALL(mock_router_, TerminateRoute(kRouteId)); |
| ui_->StopCasting(kRouteId); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, RemovePseudoSink) { |
| MockControllerObserver observer; |
| ui_->AddObserver(&observer); |
| |
| MediaSink sink(kSinkId, kSinkName, SinkIconType::CAST_AUDIO); |
| MediaSinkWithCastModes sink_with_cast_modes(sink); |
| sink_with_cast_modes.cast_modes = {MediaCastMode::TAB_MIRROR}; |
| MediaSink pseudo_sink(kPseudoSinkId, kSinkName, SinkIconType::MEETING); |
| MediaSinkWithCastModes pseudo_sink_with_cast_modes(pseudo_sink); |
| pseudo_sink_with_cast_modes.cast_modes = {MediaCastMode::TAB_MIRROR}; |
| |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>(Invoke([&sink](const CastDialogModel& model) { |
| EXPECT_EQ(1u, model.media_sinks().size()); |
| EXPECT_EQ(sink.id(), model.media_sinks()[0].id); |
| }))); |
| ui_->OnResultsUpdated({sink_with_cast_modes, pseudo_sink_with_cast_modes}); |
| ui_->RemoveObserver(&observer); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, ConnectingState) { |
| MockControllerObserver observer; |
| ui_->AddObserver(&observer); |
| |
| MediaSink sink(kSinkId, kSinkName, SinkIconType::GENERIC); |
| for (MediaSinksObserver* sinks_observer : media_sinks_observers_) |
| sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>()); |
| |
| // When a request to Cast to a sink is made, its state should become |
| // CONNECTING. |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>(Invoke([&sink](const CastDialogModel& model) { |
| ASSERT_EQ(1u, model.media_sinks().size()); |
| EXPECT_EQ(UIMediaSinkState::CONNECTING, model.media_sinks()[0].state); |
| }))); |
| ui_->StartCasting(kSinkId, MediaCastMode::TAB_MIRROR); |
| |
| // Once a route is created for the sink, its state should become CONNECTED. |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>(Invoke([&sink](const CastDialogModel& model) { |
| ASSERT_EQ(1u, model.media_sinks().size()); |
| EXPECT_EQ(UIMediaSinkState::CONNECTED, model.media_sinks()[0].state); |
| }))); |
| MediaRoute route(kRouteId, MediaSource(kSourceId), kSinkId, "", true, true); |
| ui_->OnRoutesUpdated({route}, {}); |
| ui_->RemoveObserver(&observer); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, AddAndRemoveIssue) { |
| MediaSink sink1("sink_id1", "Sink 1", SinkIconType::CAST_AUDIO); |
| MediaSinkWithCastModes sink1_with_cast_modes(sink1); |
| sink1_with_cast_modes.cast_modes = {MediaCastMode::TAB_MIRROR}; |
| MediaSink sink2("sink_id2", "Sink 2", SinkIconType::CAST_AUDIO); |
| MediaSinkWithCastModes sink2_with_cast_modes(sink2); |
| sink2_with_cast_modes.cast_modes = {MediaCastMode::TAB_MIRROR}; |
| ui_->OnResultsUpdated({sink1_with_cast_modes, sink2_with_cast_modes}); |
| |
| MockControllerObserver observer; |
| ui_->AddObserver(&observer); |
| MockIssuesObserver issues_observer(mock_router_.GetIssueManager()); |
| issues_observer.Init(); |
| const std::string issue_title("Issue 1"); |
| IssueInfo issue(issue_title, IssueInfo::Action::DISMISS, |
| IssueInfo::Severity::WARNING); |
| issue.sink_id = sink2.id(); |
| Issue::Id issue_id = -1; |
| |
| EXPECT_CALL(issues_observer, OnIssue) |
| .WillOnce( |
| Invoke([&issue_id](const Issue& issue) { issue_id = issue.id(); })); |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>( |
| Invoke([&sink1, &sink2, &issue_title](const CastDialogModel& model) { |
| EXPECT_EQ(2u, model.media_sinks().size()); |
| EXPECT_EQ(model.media_sinks()[0].id, sink1.id()); |
| EXPECT_FALSE(model.media_sinks()[0].issue.has_value()); |
| EXPECT_EQ(model.media_sinks()[1].id, sink2.id()); |
| EXPECT_EQ(model.media_sinks()[1].issue->info().title, issue_title); |
| }))); |
| mock_router_.GetIssueManager()->AddIssue(issue); |
| |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>(Invoke([&sink2](const CastDialogModel& model) { |
| EXPECT_EQ(2u, model.media_sinks().size()); |
| EXPECT_EQ(model.media_sinks()[1].id, sink2.id()); |
| EXPECT_FALSE(model.media_sinks()[1].issue.has_value()); |
| }))); |
| mock_router_.GetIssueManager()->ClearIssue(issue_id); |
| ui_->RemoveObserver(&observer); |
| } |
| |
| } // namespace media_router |