blob: 43b53f9b9c3dc4e51ac30aa4e24db5bc1487a34c [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/media_router_views_ui.h"
#include <initializer_list>
#include <memory>
#include <utility>
#include <vector>
#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.h"
#include "chrome/common/media_router/route_request_result.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "content/public/browser/browser_context.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.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 kSinkDescription[] = "description";
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::Contains(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->media_route_id());
})));
ui_->OnRoutesUpdated({route}, {});
EXPECT_CALL(observer, OnControllerInvalidated());
ui_.reset();
}
TEST_F(MediaRouterViewsUITest, SinkFriendlyName) {
MockControllerObserver observer;
ui_->AddObserver(&observer);
MediaSink sink(kSinkId, kSinkName, SinkIconType::CAST);
sink.set_description(kSinkDescription);
MediaSinkWithCastModes sink_with_cast_modes(sink);
const char* separator = u8" \u2010 ";
EXPECT_CALL(observer, OnModelUpdated(_))
.WillOnce(Invoke([&](const CastDialogModel& model) {
EXPECT_EQ(base::UTF8ToUTF16(sink.name() + separator +
sink.description().value()),
model.media_sinks()[0].friendly_name);
}));
ui_->OnResultsUpdated({sink_with_cast_modes});
ui_->RemoveObserver(&observer);
}
TEST_F(MediaRouterViewsUITest, SetDialogHeader) {
MockControllerObserver observer;
// Initially, the dialog header should simply say "Cast".
EXPECT_CALL(observer, OnModelUpdated(_))
.WillOnce([&](const CastDialogModel& model) {
EXPECT_EQ(
l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_TAB_MIRROR_CAST_MODE),
model.dialog_header());
});
ui_->AddObserver(&observer);
// We temporarily remove the observer here because the implementation calls
// OnModelUpdated() multiple times when the presentation request gets set.
ui_->RemoveObserver(&observer);
GURL gurl("https://example.com");
url::Origin origin = url::Origin::Create(gurl);
ui_->OnDefaultPresentationChanged(content::PresentationRequest(
content::GlobalFrameRoutingId(), {gurl}, origin));
// Now that the presentation request has been set, the dialog header contains
// its origin.
EXPECT_CALL(observer, OnModelUpdated(_))
.WillOnce([&](const CastDialogModel& model) {
EXPECT_EQ(
l10n_util::GetStringFUTF16(IDS_MEDIA_ROUTER_PRESENTATION_CAST_MODE,
base::UTF8ToUTF16(origin.host())),
model.dialog_header());
});
ui_->AddObserver(&observer);
ui_->RemoveObserver(&observer);
}
TEST_F(MediaRouterViewsUITest, StartCasting) {
MediaSource media_source =
MediaSource::ForTab(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([](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([](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, DisconnectingState) {
MockControllerObserver observer;
ui_->AddObserver(&observer);
MediaSink sink(kSinkId, kSinkName, SinkIconType::GENERIC);
MediaRoute route(kRouteId, MediaSource(kSourceId), kSinkId, "", true, true);
for (MediaSinksObserver* sinks_observer : media_sinks_observers_)
sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>());
ui_->OnRoutesUpdated({route}, {});
// When a request to stop casting to a sink is made, its state should become
// DISCONNECTING.
EXPECT_CALL(observer, OnModelUpdated(_))
.WillOnce(WithArg<0>(Invoke([](const CastDialogModel& model) {
ASSERT_EQ(1u, model.media_sinks().size());
EXPECT_EQ(UIMediaSinkState::DISCONNECTING,
model.media_sinks()[0].state);
})));
ui_->StopCasting(kRouteId);
// Once the route is removed, the sink's state should become AVAILABLE.
EXPECT_CALL(observer, OnModelUpdated(_))
.WillOnce(WithArg<0>(Invoke([](const CastDialogModel& model) {
ASSERT_EQ(1u, model.media_sinks().size());
EXPECT_EQ(UIMediaSinkState::AVAILABLE, model.media_sinks()[0].state);
})));
ui_->OnRoutesUpdated({}, {});
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);
}
TEST_F(MediaRouterViewsUITest, ShowDomainForHangouts) {
const std::string domain1 = "domain1.com";
const std::string domain2 = "domain2.com";
MediaSinkWithCastModes available_hangout(
MediaSink("sink1", "Hangout 1", SinkIconType::HANGOUT));
MediaSinkWithCastModes connected_hangout(
MediaSink("sink2", "Hangout 2", SinkIconType::HANGOUT));
available_hangout.sink.set_domain(domain1);
connected_hangout.sink.set_domain(domain2);
available_hangout.cast_modes = {MediaCastMode::TAB_MIRROR};
connected_hangout.cast_modes = {MediaCastMode::TAB_MIRROR};
MockControllerObserver observer;
ui_->AddObserver(&observer);
const std::string route_description = "route 1";
MediaRoute route(kRouteId, MediaSource(kSourceId), "sink2", route_description,
true, true);
ui_->OnRoutesUpdated({route}, {});
// The domain should be used as the status text only if the sink is available.
// If the sink has a route, the route description is used.
EXPECT_CALL(observer, OnModelUpdated(_))
.WillOnce(WithArg<0>([&](const CastDialogModel& model) {
EXPECT_EQ(2u, model.media_sinks().size());
EXPECT_EQ(model.media_sinks()[0].id, available_hangout.sink.id());
EXPECT_EQ(base::UTF8ToUTF16(domain1),
model.media_sinks()[0].status_text);
EXPECT_EQ(model.media_sinks()[1].id, connected_hangout.sink.id());
EXPECT_EQ(base::UTF8ToUTF16(route_description),
model.media_sinks()[1].status_text);
}));
ui_->OnResultsUpdated({available_hangout, connected_hangout});
ui_->RemoveObserver(&observer);
}
class MediaRouterViewsUIIncognitoTest : public MediaRouterViewsUITest {
protected:
content::BrowserContext* GetBrowserContext() override {
return static_cast<Profile*>(MediaRouterViewsUITest::GetBrowserContext())
->GetOffTheRecordProfile();
}
};
TEST_F(MediaRouterViewsUIIncognitoTest, HidesCloudSinksForIncognito) {
const std::string domain1 = "domain1.com";
MediaSinkWithCastModes hangout(
MediaSink("sink1", "Hangout", SinkIconType::HANGOUT));
MediaSinkWithCastModes meeting(
MediaSink("sink2", "Meeting", SinkIconType::MEETING));
MediaSinkWithCastModes eduReceiver(
MediaSink("sink3", "Cast for EDU", SinkIconType::EDUCATION));
MediaSinkWithCastModes chromeCast(
MediaSink("sink4", "Living Room TV", SinkIconType::CAST));
chromeCast.cast_modes = {MediaCastMode::TAB_MIRROR};
for (MediaSinkWithCastModes* sink :
std::initializer_list<MediaSinkWithCastModes*>{&hangout, &meeting,
&eduReceiver}) {
sink->sink.set_domain(domain1);
sink->cast_modes = {MediaCastMode::TAB_MIRROR};
}
ui_->OnResultsUpdated({hangout, meeting, eduReceiver, chromeCast});
EXPECT_EQ(std::vector<MediaSinkWithCastModes>{chromeCast},
ui_->GetEnabledSinks());
}
} // namespace media_router