| // 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/media_router/media_router_ui.h" |
| |
| #include <initializer_list> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/media/router/chrome_media_router_factory.h" |
| #include "chrome/browser/media/router/providers/wired_display/wired_display_media_route_provider.h" |
| #include "chrome/browser/sessions/session_tab_helper_factory.h" |
| #include "chrome/browser/ui/media_router/cast_dialog_controller.h" |
| #include "chrome/browser/ui/media_router/media_cast_mode.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "components/media_router/browser/media_router_factory.h" |
| #include "components/media_router/browser/media_sinks_observer.h" |
| #include "components/media_router/browser/presentation/presentation_service_delegate_impl.h" |
| #include "components/media_router/browser/test/mock_media_router.h" |
| #include "components/media_router/browser/test/test_helper.h" |
| #include "components/media_router/common/media_source.h" |
| #include "components/media_router/common/route_request_result.h" |
| #include "components/media_router/common/test/test_helper.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "content/public/browser/browser_context.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/common/extension_builder.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/display/display.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "base/mac/mac_util.h" |
| #include "ui/base/cocoa/permissions_utils.h" |
| #endif |
| |
| using testing::_; |
| using testing::Invoke; |
| using testing::Mock; |
| using testing::NiceMock; |
| using testing::Return; |
| using testing::WithArg; |
| |
| namespace media_router { |
| |
| namespace { |
| |
| constexpr char kRouteId[] = "route1"; |
| constexpr char kSinkDescription[] = "description"; |
| constexpr char kSinkId[] = "sink1"; |
| constexpr char kSinkName[] = "sink name"; |
| constexpr char kSourceId[] = "source1"; |
| |
| ACTION_TEMPLATE(SaveArgWithMove, |
| HAS_1_TEMPLATE_PARAMS(int, k), |
| AND_1_VALUE_PARAMS(pointer)) { |
| *pointer = std::move(::testing::get<k>(args)); |
| } |
| |
| class MockControllerObserver : public CastDialogController::Observer { |
| public: |
| MockControllerObserver() = default; |
| explicit MockControllerObserver(CastDialogController* controller) |
| : controller_(controller) { |
| controller_->AddObserver(this); |
| } |
| |
| ~MockControllerObserver() override { |
| if (controller_) |
| controller_->RemoveObserver(this); |
| } |
| |
| MOCK_METHOD1(OnModelUpdated, void(const CastDialogModel& model)); |
| void OnControllerInvalidated() { |
| controller_ = nullptr; |
| OnControllerInvalidatedInternal(); |
| } |
| MOCK_METHOD0(OnControllerInvalidatedInternal, void()); |
| |
| private: |
| raw_ptr<CastDialogController> controller_ = nullptr; |
| }; |
| |
| class PresentationRequestCallbacks { |
| public: |
| PresentationRequestCallbacks() {} |
| |
| explicit PresentationRequestCallbacks( |
| const blink::mojom::PresentationError& expected_error) |
| : expected_error_(expected_error) {} |
| |
| void Success(const blink::mojom::PresentationInfo&, |
| mojom::RoutePresentationConnectionPtr, |
| const MediaRoute&) {} |
| |
| void Error(const blink::mojom::PresentationError& error) { |
| EXPECT_EQ(expected_error_.error_type, error.error_type); |
| EXPECT_EQ(expected_error_.message, error.message); |
| } |
| |
| private: |
| blink::mojom::PresentationError expected_error_; |
| }; |
| |
| class TestWebContentsDisplayObserver : public WebContentsDisplayObserver { |
| public: |
| explicit TestWebContentsDisplayObserver(const display::Display& display) |
| : display_(display) {} |
| ~TestWebContentsDisplayObserver() override {} |
| |
| const display::Display& GetCurrentDisplay() const override { |
| return display_; |
| } |
| |
| void set_display(const display::Display& display) { display_ = display; } |
| |
| private: |
| display::Display display_; |
| }; |
| |
| } // namespace |
| |
| class MediaRouterViewsUITest : public ChromeRenderViewHostTestHarness { |
| public: |
| void SetUp() override { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| |
| SetMediaRouterFactory(); |
| mock_router_ = static_cast<MockMediaRouter*>( |
| MediaRouterFactory::GetApiForBrowserContext(GetBrowserContext())); |
| logger_ = std::make_unique<LoggerImpl>(); |
| |
| // Store sink observers so that they can be notified in tests. |
| ON_CALL(*mock_router_, RegisterMediaSinksObserver(_)) |
| .WillByDefault([this](MediaSinksObserver* observer) { |
| media_sinks_observers_.push_back(observer); |
| return true; |
| }); |
| ON_CALL(*mock_router_, GetLogger()).WillByDefault(Return(logger_.get())); |
| |
| CreateSessionServiceTabHelper(web_contents()); |
| ui_ = |
| MediaRouterUI::CreateWithDefaultMediaSourceAndMirroring(web_contents()); |
| } |
| |
| void TearDown() override { |
| #if BUILDFLAG(IS_MAC) |
| clear_screen_capture_allowed_for_testing(); |
| #endif |
| ui_.reset(); |
| ChromeRenderViewHostTestHarness::TearDown(); |
| } |
| |
| virtual void SetMediaRouterFactory() { |
| ChromeMediaRouterFactory::GetInstance()->SetTestingFactory( |
| GetBrowserContext(), base::BindRepeating(&MockMediaRouter::Create)); |
| } |
| |
| void CreateMediaRouterUIForURL(const GURL& url) { |
| web_contents()->GetController().LoadURL(url, content::Referrer(), |
| ui::PAGE_TRANSITION_LINK, ""); |
| content::RenderFrameHostTester::CommitPendingLoad( |
| &web_contents()->GetController()); |
| CreateSessionServiceTabHelper(web_contents()); |
| ui_ = |
| MediaRouterUI::CreateWithDefaultMediaSourceAndMirroring(web_contents()); |
| } |
| |
| // These methods are used so that we don't have to friend each test case that |
| // calls the private methods. |
| void NotifyUiOnSinksUpdated( |
| const std::vector<MediaSinkWithCastModes>& sinks) { |
| ui_->OnSinksUpdated(sinks); |
| } |
| void NotifyUiOnRoutesUpdated(const std::vector<MediaRoute>& routes) { |
| ui_->OnRoutesUpdated(routes); |
| } |
| |
| void StartTabCasting(bool is_incognito) { |
| MediaSource media_source = MediaSource::ForTab( |
| sessions::SessionTabHelper::IdForTab(web_contents()).id()); |
| EXPECT_CALL( |
| *mock_router_, |
| CreateRouteInternal(media_source.id(), kSinkId, _, web_contents(), _, |
| base::Seconds(60), is_incognito)); |
| MediaSink sink{CreateCastSink(kSinkId, kSinkName)}; |
| for (MediaSinksObserver* sinks_observer : media_sinks_observers_) |
| sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>()); |
| ui_->StartCasting(kSinkId, MediaCastMode::TAB_MIRROR); |
| } |
| |
| void StartCastingAndExpectTimeout(MediaCastMode cast_mode, |
| const std::string& expected_issue_title, |
| int timeout_seconds) { |
| NiceMock<MockControllerObserver> observer(ui_.get()); |
| MediaSink sink{CreateCastSink(kSinkId, kSinkName)}; |
| ui_->OnSinksUpdated({{sink, {cast_mode}}}); |
| MediaRouteResponseCallback callback; |
| EXPECT_CALL(*mock_router_, |
| CreateRouteInternal(_, _, _, _, _, |
| base::Seconds(timeout_seconds), false)) |
| .WillOnce(SaveArgWithMove<4>(&callback)); |
| for (MediaSinksObserver* sinks_observer : media_sinks_observers_) |
| sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>()); |
| ui_->StartCasting(kSinkId, cast_mode); |
| Mock::VerifyAndClearExpectations(mock_router_); |
| |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>([&](const CastDialogModel& model) { |
| EXPECT_EQ(model.media_sinks()[0].issue->info().title, |
| expected_issue_title); |
| })); |
| std::unique_ptr<RouteRequestResult> result = RouteRequestResult::FromError( |
| "Timed out", mojom::RouteRequestResultCode::TIMED_OUT); |
| std::move(callback).Run(nullptr, *result); |
| } |
| |
| // The caller must hold on to PresentationRequestCallbacks returned so that |
| // a callback can later be called on it. |
| std::unique_ptr<PresentationRequestCallbacks> ExpectPresentationError( |
| blink::mojom::PresentationErrorType error_type, |
| const std::string& error_message) { |
| blink::mojom::PresentationError expected_error(error_type, error_message); |
| auto request_callbacks = |
| std::make_unique<PresentationRequestCallbacks>(expected_error); |
| start_presentation_context_ = std::make_unique<StartPresentationContext>( |
| presentation_request_, |
| base::BindOnce(&PresentationRequestCallbacks::Success, |
| base::Unretained(request_callbacks.get())), |
| base::BindOnce(&PresentationRequestCallbacks::Error, |
| base::Unretained(request_callbacks.get()))); |
| StartPresentationContext* context_ptr = start_presentation_context_.get(); |
| ui_->media_route_starter()->set_start_presentation_context_for_test( |
| std::move(start_presentation_context_)); |
| ui_->media_route_starter()->OnDefaultPresentationChanged( |
| &context_ptr->presentation_request()); |
| return request_callbacks; |
| } |
| |
| protected: |
| std::vector<MediaSinksObserver*> media_sinks_observers_; |
| raw_ptr<MockMediaRouter> mock_router_ = nullptr; |
| std::unique_ptr<MediaRouterUI> ui_; |
| std::unique_ptr<StartPresentationContext> start_presentation_context_; |
| std::unique_ptr<LoggerImpl> logger_; |
| content::PresentationRequest presentation_request_{ |
| {0, 0}, |
| {GURL("https://google.com/presentation")}, |
| url::Origin::Create(GURL("http://google.com"))}; |
| }; |
| |
| 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{CreateCastSink(kSinkId, kSinkName)}; |
| 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); |
| }))); |
| NotifyUiOnSinksUpdated({sink_with_cast_modes}); |
| |
| MediaRoute route(kRouteId, MediaSource(kSourceId), kSinkId, "", 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()); |
| }))); |
| NotifyUiOnRoutesUpdated({route}); |
| |
| EXPECT_CALL(observer, OnControllerInvalidatedInternal()); |
| ui_.reset(); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, SinkFriendlyName) { |
| NiceMock<MockControllerObserver> observer(ui_.get()); |
| |
| MediaSink sink{CreateCastSink(kSinkId, kSinkName)}; |
| sink.set_description(kSinkDescription); |
| MediaSinkWithCastModes sink_with_cast_modes(sink); |
| const char* separator = " \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); |
| })); |
| NotifyUiOnSinksUpdated({sink_with_cast_modes}); |
| } |
| |
| 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); |
| |
| // The observer is called multiple times when the default PresentationRequest |
| // is changed; the last invocation has the correct header. |
| std::u16string current_header; |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillRepeatedly([&](const CastDialogModel& model) { |
| current_header = model.dialog_header(); |
| }); |
| |
| // First test a presentation started from an https: origin. |
| const GURL presentation_url("https://presentation.com"); |
| const auto https_origin = |
| url::Origin::Create(GURL("https://requesting-page.com")); |
| // An https origin is included in the dialog header without the scheme. |
| content::PresentationRequest presentation_request( |
| content::GlobalRenderFrameHostId(), {presentation_url}, https_origin); |
| ui_->media_route_starter()->OnDefaultPresentationChanged( |
| &presentation_request); |
| EXPECT_EQ(l10n_util::GetStringFUTF16(IDS_MEDIA_ROUTER_PRESENTATION_CAST_MODE, |
| base::UTF8ToUTF16(https_origin.host())), |
| current_header); |
| |
| // An opaque origin is empty, which causes the dialog to fall back to the tab |
| // mirroring header. |
| presentation_request = content::PresentationRequest( |
| content::GlobalRenderFrameHostId(), {presentation_url}, url::Origin()); |
| ui_->media_route_starter()->OnDefaultPresentationChanged( |
| &presentation_request); |
| EXPECT_EQ(l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_TAB_MIRROR_CAST_MODE), |
| current_header); |
| |
| // An extension origin is replaced by the extension name. |
| const std::string extension_id = "extensionid"; |
| const auto extension_origin = |
| url::Origin::Create(GURL("chrome-extension://" + extension_id)); |
| auto* registry = extensions::ExtensionRegistry::Get(GetBrowserContext()); |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder( |
| "Test Extension", extensions::ExtensionBuilder::Type::EXTENSION) |
| .SetID(extension_id) |
| .Build(); |
| |
| ASSERT_TRUE(registry->AddEnabled(extension)); |
| |
| presentation_request = content::PresentationRequest( |
| content::GlobalRenderFrameHostId(), {presentation_url}, extension_origin); |
| ui_->media_route_starter()->OnDefaultPresentationChanged( |
| &presentation_request); |
| EXPECT_EQ(l10n_util::GetStringFUTF16(IDS_MEDIA_ROUTER_PRESENTATION_CAST_MODE, |
| std::u16string(u"Test Extension")), |
| current_header); |
| |
| ui_->RemoveObserver(&observer); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, StartCasting) { |
| StartTabCasting(false); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, StopCasting) { |
| EXPECT_CALL(*mock_router_, TerminateRoute(kRouteId)); |
| ui_->StopCasting(kRouteId); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, ConnectingState) { |
| NiceMock<MockControllerObserver> observer(ui_.get()); |
| |
| MediaSink sink{CreateDialSink(kSinkId, kSinkName)}; |
| 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); |
| NotifyUiOnRoutesUpdated({route}); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, DisconnectingState) { |
| NiceMock<MockControllerObserver> observer(ui_.get()); |
| |
| MediaSink sink{CreateDialSink(kSinkId, kSinkName)}; |
| MediaRoute route(kRouteId, MediaSource(kSourceId), kSinkId, "", true); |
| for (MediaSinksObserver* sinks_observer : media_sinks_observers_) |
| sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>()); |
| NotifyUiOnRoutesUpdated({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); |
| }))); |
| NotifyUiOnRoutesUpdated({}); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, AddAndRemoveIssue) { |
| MediaSink sink1{CreateCastSink("sink_id1", "Sink 1")}; |
| MediaSink sink2{CreateCastSink("sink_id2", "Sink 2")}; |
| NotifyUiOnSinksUpdated({{sink1, {MediaCastMode::TAB_MIRROR}}, |
| {sink2, {MediaCastMode::TAB_MIRROR}}}); |
| |
| NiceMock<MockControllerObserver> observer(ui_.get()); |
| NiceMock<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); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, RouteCreationTimeoutForTab) { |
| StartCastingAndExpectTimeout( |
| MediaCastMode::TAB_MIRROR, |
| l10n_util::GetStringUTF8( |
| IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_TIMEOUT_FOR_TAB), |
| 60); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, RouteCreationTimeoutForDesktop) { |
| #if BUILDFLAG(IS_MAC) |
| if (base::mac::IsAtLeastOS10_15()) |
| set_screen_capture_allowed_for_testing(true); |
| #endif |
| |
| StartCastingAndExpectTimeout( |
| MediaCastMode::DESKTOP_MIRROR, |
| l10n_util::GetStringUTF8( |
| IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_TIMEOUT_FOR_DESKTOP), |
| 120); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, RouteCreationTimeoutForPresentation) { |
| content::PresentationRequest presentation_request( |
| {0, 0}, {GURL("https://presentationurl.com")}, |
| url::Origin::Create(GURL("https://frameurl.fakeurl"))); |
| ui_->media_route_starter()->OnDefaultPresentationChanged( |
| &presentation_request); |
| StartCastingAndExpectTimeout( |
| MediaCastMode::PRESENTATION, |
| l10n_util::GetStringFUTF8(IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_TIMEOUT, |
| u"frameurl.fakeurl"), |
| 20); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| TEST_F(MediaRouterViewsUITest, DesktopMirroringFailsWhenDisallowedOnMac) { |
| // Failure due to a lack of screen capture permissions only happens on macOS |
| // 10.15 or later. See crbug.com/1087236 for more info. |
| if (!base::mac::IsAtLeastOS10_15()) |
| return; |
| |
| set_screen_capture_allowed_for_testing(false); |
| MockControllerObserver observer(ui_.get()); |
| MediaSink sink{CreateCastSink(kSinkId, kSinkName)}; |
| ui_->OnSinksUpdated({{sink, {MediaCastMode::DESKTOP_MIRROR}}}); |
| for (MediaSinksObserver* sinks_observer : media_sinks_observers_) |
| sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>()); |
| |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>([&](const CastDialogModel& model) { |
| EXPECT_EQ( |
| model.media_sinks()[0].issue->info().title, |
| l10n_util::GetStringUTF8( |
| IDS_MEDIA_ROUTER_ISSUE_MAC_SCREEN_CAPTURE_PERMISSION_ERROR)); |
| })); |
| ui_->StartCasting(kSinkId, MediaCastMode::DESKTOP_MIRROR); |
| } |
| #endif |
| |
| TEST_F(MediaRouterViewsUITest, SortedSinks) { |
| NotifyUiOnSinksUpdated({{CreateCastSink("sink3", "B sink"), {}}, |
| {CreateCastSink("sink2", "A sink"), {}}, |
| {CreateCastSink("sink1", "B sink"), {}}}); |
| |
| // Sort first by name, then by ID. |
| const auto& sorted_sinks = ui_->GetEnabledSinks(); |
| EXPECT_EQ("sink2", sorted_sinks[0].sink.id()); |
| EXPECT_EQ("sink1", sorted_sinks[1].sink.id()); |
| EXPECT_EQ("sink3", sorted_sinks[2].sink.id()); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, SortSinksByIconType) { |
| NotifyUiOnSinksUpdated( |
| {{MediaSink{"id1", "B sink", SinkIconType::CAST_AUDIO_GROUP, |
| mojom::MediaRouteProviderId::CAST}, |
| {}}, |
| {MediaSink{"id2", "sink", SinkIconType::GENERIC, |
| mojom::MediaRouteProviderId::WIRED_DISPLAY}, |
| {}}, |
| {MediaSink{"id3", "A sink", SinkIconType::CAST_AUDIO_GROUP, |
| mojom::MediaRouteProviderId::CAST}, |
| {}}, |
| {MediaSink{"id4", "sink", SinkIconType::CAST_AUDIO, |
| mojom::MediaRouteProviderId::CAST}, |
| {}}, |
| {MediaSink{"id5", "sink", SinkIconType::CAST, |
| mojom::MediaRouteProviderId::CAST}, |
| {}}}); |
| |
| // The sorted order is CAST, CAST_AUDIO_GROUP "A", CAST_AUDIO_GROUP "B", |
| // CAST_AUDIO, HANGOUT, GENERIC. |
| const auto& sorted_sinks = ui_->GetEnabledSinks(); |
| EXPECT_EQ("id5", sorted_sinks[0].sink.id()); |
| EXPECT_EQ("id3", sorted_sinks[1].sink.id()); |
| EXPECT_EQ("id1", sorted_sinks[2].sink.id()); |
| EXPECT_EQ("id4", sorted_sinks[3].sink.id()); |
| EXPECT_EQ("id2", sorted_sinks[4].sink.id()); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, NotFoundErrorOnCloseWithNoSinks) { |
| auto request_callbacks = ExpectPresentationError( |
| blink::mojom::PresentationErrorType::NO_AVAILABLE_SCREENS, |
| "No screens found."); |
| // Destroying the UI should return the expected error from above to the error |
| // callback. |
| ui_.reset(); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, NotFoundErrorOnCloseWithNoCompatibleSinks) { |
| auto request_callbacks = ExpectPresentationError( |
| blink::mojom::PresentationErrorType::NO_AVAILABLE_SCREENS, |
| "No screens found."); |
| // Send a sink to the UI that is compatible with sources other than the |
| // presentation url to cause a NotFoundError. |
| std::vector<MediaSink> sinks = {CreateDialSink(kSinkId, kSinkName)}; |
| auto presentation_source = MediaSource::ForPresentationUrl( |
| presentation_request_.presentation_urls[0]); |
| for (MediaSinksObserver* sinks_observer : media_sinks_observers_) { |
| if (!(sinks_observer->source() == presentation_source)) { |
| sinks_observer->OnSinksUpdated(sinks, {}); |
| } |
| } |
| // Destroying the UI should return the expected error from above to the error |
| // callback. |
| ui_.reset(); |
| } |
| |
| TEST_F(MediaRouterViewsUITest, AbortErrorOnClose) { |
| auto request_callbacks = ExpectPresentationError( |
| blink::mojom::PresentationErrorType::PRESENTATION_REQUEST_CANCELLED, |
| "Dialog closed."); |
| // Send a sink to the UI that is compatible with the presentation url to avoid |
| // a NotFoundError. |
| std::vector<MediaSink> sinks = {CreateDialSink(kSinkId, kSinkName)}; |
| auto presentation_source = MediaSource::ForPresentationUrl( |
| presentation_request_.presentation_urls[0]); |
| for (MediaSinksObserver* sinks_observer : media_sinks_observers_) { |
| if (sinks_observer->source() == presentation_source) { |
| sinks_observer->OnSinksUpdated(sinks, {}); |
| } |
| } |
| // Destroying the UI should return the expected error from above to the error |
| // callback. |
| ui_.reset(); |
| } |
| |
| // A wired display sink should not be on the sinks list when the dialog is on |
| // that display, to prevent showing a fullscreen presentation window over the |
| // controlling window. |
| TEST_F(MediaRouterViewsUITest, UpdateSinksWhenDialogMovesToAnotherDisplay) { |
| NiceMock<MockControllerObserver> observer(ui_.get()); |
| const display::Display display1(1000001); |
| const display::Display display2(1000002); |
| const std::string display_sink_id1 = |
| WiredDisplayMediaRouteProvider::GetSinkIdForDisplay(display1); |
| const std::string display_sink_id2 = |
| WiredDisplayMediaRouteProvider::GetSinkIdForDisplay(display2); |
| |
| auto display_observer_unique = |
| std::make_unique<TestWebContentsDisplayObserver>(display1); |
| TestWebContentsDisplayObserver* display_observer = |
| display_observer_unique.get(); |
| ui_->display_observer_ = std::move(display_observer_unique); |
| |
| NotifyUiOnSinksUpdated( |
| {{CreateWiredDisplaySink(display_sink_id1, "sink"), {}}, |
| {CreateWiredDisplaySink(display_sink_id2, "sink"), {}}, |
| {CreateDialSink("id3", "sink"), {}}}); |
| |
| // Initially |display_sink_id1| should not be on the sinks list because we are |
| // on |display1|. |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>([&](const CastDialogModel& model) { |
| const auto& sinks = model.media_sinks(); |
| EXPECT_EQ(2u, sinks.size()); |
| EXPECT_TRUE(std::find_if(sinks.begin(), sinks.end(), |
| [&](const UIMediaSink& sink) { |
| return sink.id == display_sink_id1; |
| }) == sinks.end()); |
| })); |
| ui_->UpdateSinks(); |
| Mock::VerifyAndClearExpectations(&observer); |
| |
| // Change the display to |display2|. Now |display_sink_id2| should be removed |
| // from the list of sinks. |
| EXPECT_CALL(observer, OnModelUpdated(_)) |
| .WillOnce(WithArg<0>([&](const CastDialogModel& model) { |
| const auto& sinks = model.media_sinks(); |
| EXPECT_EQ(2u, sinks.size()); |
| EXPECT_TRUE(std::find_if(sinks.begin(), sinks.end(), |
| [&](const UIMediaSink& sink) { |
| return sink.id == display_sink_id2; |
| }) == sinks.end()); |
| })); |
| display_observer->set_display(display2); |
| ui_->UpdateSinks(); |
| } |
| |
| class MediaRouterViewsUIIncognitoTest : public MediaRouterViewsUITest { |
| protected: |
| void SetMediaRouterFactory() override { |
| // We must set the factory on the non-incognito browser context. |
| MediaRouterFactory::GetInstance()->SetTestingFactory( |
| MediaRouterViewsUITest::GetBrowserContext(), |
| base::BindRepeating(&MockMediaRouter::Create)); |
| } |
| |
| content::BrowserContext* GetBrowserContext() override { |
| return static_cast<Profile*>(MediaRouterViewsUITest::GetBrowserContext()) |
| ->GetPrimaryOTRProfile(/*create_if_needed=*/true); |
| } |
| }; |
| |
| TEST_F(MediaRouterViewsUIIncognitoTest, RouteRequestFromIncognito) { |
| StartTabCasting(true); |
| } |
| |
| } // namespace media_router |