1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/ui/media_router/media_router_ui.h"
6
7 #include <initializer_list>
8 #include <memory>
9 #include <utility>
10 #include <vector>
11
12 #include "base/strings/utf_string_conversions.h"
13 #include "build/build_config.h"
14 #include "chrome/browser/media/router/chrome_media_router_factory.h"
15 #include "chrome/browser/media/router/providers/wired_display/wired_display_media_route_provider.h"
16 #include "chrome/browser/sessions/session_tab_helper_factory.h"
17 #include "chrome/browser/ui/media_router/cast_dialog_controller.h"
18 #include "chrome/browser/ui/media_router/media_cast_mode.h"
19 #include "chrome/common/webui_url_constants.h"
20 #include "chrome/grit/generated_resources.h"
21 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
22 #include "components/media_router/browser/media_router_factory.h"
23 #include "components/media_router/browser/media_sinks_observer.h"
24 #include "components/media_router/browser/presentation/presentation_service_delegate_impl.h"
25 #include "components/media_router/browser/test/mock_media_router.h"
26 #include "components/media_router/browser/test/test_helper.h"
27 #include "components/media_router/common/media_source.h"
28 #include "components/media_router/common/route_request_result.h"
29 #include "components/sessions/content/session_tab_helper.h"
30 #include "content/public/browser/browser_context.h"
31 #include "testing/gmock/include/gmock/gmock.h"
32 #include "testing/gtest/include/gtest/gtest.h"
33 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/display/display.h"
35 #include "url/origin.h"
36
37 #if defined(OS_MAC)
38 #include "base/mac/mac_util.h"
39 #include "ui/base/cocoa/permissions_utils.h"
40 #endif
41
42 using testing::_;
43 using testing::Invoke;
44 using testing::Mock;
45 using testing::Return;
46 using testing::WithArg;
47
48 namespace media_router {
49
50 namespace {
51
52 constexpr char kPseudoSinkId[] = "pseudo:sink";
53 constexpr char kRouteId[] = "route1";
54 constexpr char kSinkDescription[] = "description";
55 constexpr char kSinkId[] = "sink1";
56 constexpr char kSinkName[] = "sink name";
57 constexpr char kSourceId[] = "source1";
58
ACTION_TEMPLATE(SaveArgWithMove,HAS_1_TEMPLATE_PARAMS (int,k),AND_1_VALUE_PARAMS (pointer))59 ACTION_TEMPLATE(SaveArgWithMove,
60 HAS_1_TEMPLATE_PARAMS(int, k),
61 AND_1_VALUE_PARAMS(pointer)) {
62 *pointer = std::move(::testing::get<k>(args));
63 }
64
65 class MockControllerObserver : public CastDialogController::Observer {
66 public:
67 MockControllerObserver() = default;
MockControllerObserver(CastDialogController * controller)68 explicit MockControllerObserver(CastDialogController* controller)
69 : controller_(controller) {
70 controller_->AddObserver(this);
71 }
72
~MockControllerObserver()73 ~MockControllerObserver() override {
74 if (controller_)
75 controller_->RemoveObserver(this);
76 }
77
78 MOCK_METHOD1(OnModelUpdated, void(const CastDialogModel& model));
OnControllerInvalidated()79 void OnControllerInvalidated() {
80 controller_ = nullptr;
81 OnControllerInvalidatedInternal();
82 }
83 MOCK_METHOD0(OnControllerInvalidatedInternal, void());
84
85 private:
86 CastDialogController* controller_ = nullptr;
87 };
88
89 class MockMediaRouterFileDialog : public MediaRouterFileDialog {
90 public:
MockMediaRouterFileDialog()91 MockMediaRouterFileDialog() : MediaRouterFileDialog(nullptr) {}
~MockMediaRouterFileDialog()92 ~MockMediaRouterFileDialog() override {}
93
94 MOCK_METHOD0(GetLastSelectedFileUrl, GURL());
95 MOCK_METHOD0(GetLastSelectedFileName, base::string16());
96 MOCK_METHOD1(OpenFileDialog, void(Browser* browser));
97 };
98
99 class PresentationRequestCallbacks {
100 public:
PresentationRequestCallbacks()101 PresentationRequestCallbacks() {}
102
PresentationRequestCallbacks(const blink::mojom::PresentationError & expected_error)103 explicit PresentationRequestCallbacks(
104 const blink::mojom::PresentationError& expected_error)
105 : expected_error_(expected_error) {}
106
Success(const blink::mojom::PresentationInfo &,mojom::RoutePresentationConnectionPtr,const MediaRoute &)107 void Success(const blink::mojom::PresentationInfo&,
108 mojom::RoutePresentationConnectionPtr,
109 const MediaRoute&) {}
110
Error(const blink::mojom::PresentationError & error)111 void Error(const blink::mojom::PresentationError& error) {
112 EXPECT_EQ(expected_error_.error_type, error.error_type);
113 EXPECT_EQ(expected_error_.message, error.message);
114 }
115
116 private:
117 blink::mojom::PresentationError expected_error_;
118 };
119
120 class TestWebContentsDisplayObserver : public WebContentsDisplayObserver {
121 public:
TestWebContentsDisplayObserver(const display::Display & display)122 explicit TestWebContentsDisplayObserver(const display::Display& display)
123 : display_(display) {}
~TestWebContentsDisplayObserver()124 ~TestWebContentsDisplayObserver() override {}
125
GetCurrentDisplay() const126 const display::Display& GetCurrentDisplay() const override {
127 return display_;
128 }
129
set_display(const display::Display & display)130 void set_display(const display::Display& display) { display_ = display; }
131
132 private:
133 display::Display display_;
134 };
135
136 } // namespace
137
138 class MediaRouterViewsUITest : public ChromeRenderViewHostTestHarness {
139 public:
SetUp()140 void SetUp() override {
141 ChromeRenderViewHostTestHarness::SetUp();
142
143 SetMediaRouterFactory();
144 mock_router_ = static_cast<MockMediaRouter*>(
145 MediaRouterFactory::GetApiForBrowserContext(GetBrowserContext()));
146 logger_ = std::make_unique<LoggerImpl>();
147
148 // Store sink observers so that they can be notified in tests.
149 ON_CALL(*mock_router_, RegisterMediaSinksObserver(_))
150 .WillByDefault([this](MediaSinksObserver* observer) {
151 media_sinks_observers_.push_back(observer);
152 return true;
153 });
154 ON_CALL(*mock_router_, GetLogger()).WillByDefault(Return(logger_.get()));
155
156 CreateSessionServiceTabHelper(web_contents());
157 ui_ = std::make_unique<MediaRouterUI>(web_contents());
158 ui_->InitWithDefaultMediaSourceAndMirroring();
159 }
160
TearDown()161 void TearDown() override {
162 ui_.reset();
163 ChromeRenderViewHostTestHarness::TearDown();
164 }
165
SetMediaRouterFactory()166 virtual void SetMediaRouterFactory() {
167 ChromeMediaRouterFactory::GetInstance()->SetTestingFactory(
168 GetBrowserContext(), base::BindRepeating(&MockMediaRouter::Create));
169 }
170
CreateMediaRouterUIForURL(const GURL & url)171 void CreateMediaRouterUIForURL(const GURL& url) {
172 web_contents()->GetController().LoadURL(url, content::Referrer(),
173 ui::PAGE_TRANSITION_LINK, "");
174 content::RenderFrameHostTester::CommitPendingLoad(
175 &web_contents()->GetController());
176 CreateSessionServiceTabHelper(web_contents());
177 ui_ = std::make_unique<MediaRouterUI>(web_contents());
178 ui_->InitWithDefaultMediaSourceAndMirroring();
179 }
180
181 // These methods are used so that we don't have to friend each test case that
182 // calls the private methods.
NotifyUiOnResultsUpdated(const std::vector<MediaSinkWithCastModes> & sinks)183 void NotifyUiOnResultsUpdated(
184 const std::vector<MediaSinkWithCastModes>& sinks) {
185 ui_->OnResultsUpdated(sinks);
186 }
NotifyUiOnRoutesUpdated(const std::vector<MediaRoute> & routes,const std::vector<MediaRoute::Id> & joinable_route_ids)187 void NotifyUiOnRoutesUpdated(
188 const std::vector<MediaRoute>& routes,
189 const std::vector<MediaRoute::Id>& joinable_route_ids) {
190 ui_->OnRoutesUpdated(routes, joinable_route_ids);
191 }
192
StartTabCasting(bool is_incognito)193 void StartTabCasting(bool is_incognito) {
194 MediaSource media_source = MediaSource::ForTab(
195 sessions::SessionTabHelper::IdForTab(web_contents()).id());
196 EXPECT_CALL(
197 *mock_router_,
198 CreateRouteInternal(media_source.id(), kSinkId, _, web_contents(), _,
199 base::TimeDelta::FromSeconds(60), is_incognito));
200 MediaSink sink(kSinkId, kSinkName, SinkIconType::GENERIC);
201 for (MediaSinksObserver* sinks_observer : media_sinks_observers_)
202 sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>());
203 ui_->StartCasting(kSinkId, MediaCastMode::TAB_MIRROR);
204 }
205
StartCastingAndExpectTimeout(MediaCastMode cast_mode,const std::string & expected_issue_title,int timeout_seconds)206 void StartCastingAndExpectTimeout(MediaCastMode cast_mode,
207 const std::string& expected_issue_title,
208 int timeout_seconds) {
209 MockControllerObserver observer(ui_.get());
210 MediaSink sink(kSinkId, kSinkName, SinkIconType::CAST);
211 ui_->OnResultsUpdated({{sink, {cast_mode}}});
212 MediaRouteResponseCallback callback;
213 EXPECT_CALL(*mock_router_,
214 CreateRouteInternal(
215 _, _, _, _, _,
216 base::TimeDelta::FromSeconds(timeout_seconds), false))
217 .WillOnce(SaveArgWithMove<4>(&callback));
218 for (MediaSinksObserver* sinks_observer : media_sinks_observers_)
219 sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>());
220 ui_->StartCasting(kSinkId, cast_mode);
221 Mock::VerifyAndClearExpectations(mock_router_);
222
223 EXPECT_CALL(observer, OnModelUpdated(_))
224 .WillOnce(WithArg<0>([&](const CastDialogModel& model) {
225 EXPECT_EQ(model.media_sinks()[0].issue->info().title,
226 expected_issue_title);
227 }));
228 std::unique_ptr<RouteRequestResult> result = RouteRequestResult::FromError(
229 "Timed out", RouteRequestResult::TIMED_OUT);
230 std::move(callback).Run(nullptr, *result);
231 }
232
233 // The caller must hold on to PresentationRequestCallbacks returned so that
234 // a callback can later be called on it.
ExpectPresentationError(blink::mojom::PresentationErrorType error_type,const std::string & error_message)235 std::unique_ptr<PresentationRequestCallbacks> ExpectPresentationError(
236 blink::mojom::PresentationErrorType error_type,
237 const std::string& error_message) {
238 blink::mojom::PresentationError expected_error(error_type, error_message);
239 auto request_callbacks =
240 std::make_unique<PresentationRequestCallbacks>(expected_error);
241 start_presentation_context_ = std::make_unique<StartPresentationContext>(
242 presentation_request_,
243 base::Bind(&PresentationRequestCallbacks::Success,
244 base::Unretained(request_callbacks.get())),
245 base::Bind(&PresentationRequestCallbacks::Error,
246 base::Unretained(request_callbacks.get())));
247 StartPresentationContext* context_ptr = start_presentation_context_.get();
248 ui_->set_start_presentation_context_for_test(
249 std::move(start_presentation_context_));
250 ui_->OnDefaultPresentationChanged(&context_ptr->presentation_request());
251 return request_callbacks;
252 }
253
254 protected:
255 std::vector<MediaSinksObserver*> media_sinks_observers_;
256 MockMediaRouter* mock_router_ = nullptr;
257 std::unique_ptr<MediaRouterUI> ui_;
258 std::unique_ptr<StartPresentationContext> start_presentation_context_;
259 std::unique_ptr<LoggerImpl> logger_;
260 content::PresentationRequest presentation_request_{
261 {0, 0},
262 {GURL("https://google.com/presentation")},
263 url::Origin::Create(GURL("http://google.com"))};
264 };
265
TEST_F(MediaRouterViewsUITest,NotifyObserver)266 TEST_F(MediaRouterViewsUITest, NotifyObserver) {
267 MockControllerObserver observer;
268
269 EXPECT_CALL(observer, OnModelUpdated(_))
270 .WillOnce(WithArg<0>(Invoke([](const CastDialogModel& model) {
271 EXPECT_TRUE(model.media_sinks().empty());
272 })));
273 ui_->AddObserver(&observer);
274
275 MediaSink sink(kSinkId, kSinkName, SinkIconType::CAST_AUDIO);
276 MediaSinkWithCastModes sink_with_cast_modes(sink);
277 sink_with_cast_modes.cast_modes = {MediaCastMode::TAB_MIRROR};
278 EXPECT_CALL(observer, OnModelUpdated(_))
279 .WillOnce(WithArg<0>(Invoke([&sink](const CastDialogModel& model) {
280 EXPECT_EQ(1u, model.media_sinks().size());
281 const UIMediaSink& ui_sink = model.media_sinks()[0];
282 EXPECT_EQ(sink.id(), ui_sink.id);
283 EXPECT_EQ(base::UTF8ToUTF16(sink.name()), ui_sink.friendly_name);
284 EXPECT_EQ(UIMediaSinkState::AVAILABLE, ui_sink.state);
285 EXPECT_TRUE(
286 base::Contains(ui_sink.cast_modes, MediaCastMode::TAB_MIRROR));
287 EXPECT_EQ(sink.icon_type(), ui_sink.icon_type);
288 })));
289 NotifyUiOnResultsUpdated({sink_with_cast_modes});
290
291 MediaRoute route(kRouteId, MediaSource(kSourceId), kSinkId, "", true, true);
292 EXPECT_CALL(observer, OnModelUpdated(_))
293 .WillOnce(
294 WithArg<0>(Invoke([&sink, &route](const CastDialogModel& model) {
295 EXPECT_EQ(1u, model.media_sinks().size());
296 const UIMediaSink& ui_sink = model.media_sinks()[0];
297 EXPECT_EQ(sink.id(), ui_sink.id);
298 EXPECT_EQ(UIMediaSinkState::CONNECTED, ui_sink.state);
299 EXPECT_EQ(route.media_route_id(), ui_sink.route->media_route_id());
300 })));
301 NotifyUiOnRoutesUpdated({route}, {});
302
303 EXPECT_CALL(observer, OnControllerInvalidatedInternal());
304 ui_.reset();
305 }
306
TEST_F(MediaRouterViewsUITest,SinkFriendlyName)307 TEST_F(MediaRouterViewsUITest, SinkFriendlyName) {
308 MockControllerObserver observer(ui_.get());
309
310 MediaSink sink(kSinkId, kSinkName, SinkIconType::CAST);
311 sink.set_description(kSinkDescription);
312 MediaSinkWithCastModes sink_with_cast_modes(sink);
313 const char* separator = u8" \u2010 ";
314 EXPECT_CALL(observer, OnModelUpdated(_))
315 .WillOnce(Invoke([&](const CastDialogModel& model) {
316 EXPECT_EQ(base::UTF8ToUTF16(sink.name() + separator +
317 sink.description().value()),
318 model.media_sinks()[0].friendly_name);
319 }));
320 NotifyUiOnResultsUpdated({sink_with_cast_modes});
321 }
322
TEST_F(MediaRouterViewsUITest,SetDialogHeader)323 TEST_F(MediaRouterViewsUITest, SetDialogHeader) {
324 MockControllerObserver observer;
325 // Initially, the dialog header should simply say "Cast".
326 EXPECT_CALL(observer, OnModelUpdated(_))
327 .WillOnce([&](const CastDialogModel& model) {
328 EXPECT_EQ(
329 l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_TAB_MIRROR_CAST_MODE),
330 model.dialog_header());
331 });
332 ui_->AddObserver(&observer);
333 // We temporarily remove the observer here because the implementation calls
334 // OnModelUpdated() multiple times when the presentation request gets set.
335 ui_->RemoveObserver(&observer);
336
337 GURL gurl("https://example.com");
338 url::Origin origin = url::Origin::Create(gurl);
339 content::PresentationRequest presentation_request(
340 content::GlobalFrameRoutingId(), {gurl}, origin);
341 ui_->OnDefaultPresentationChanged(&presentation_request);
342
343 // Now that the presentation request has been set, the dialog header contains
344 // its origin.
345 EXPECT_CALL(observer, OnModelUpdated(_))
346 .WillOnce([&](const CastDialogModel& model) {
347 EXPECT_EQ(
348 l10n_util::GetStringFUTF16(IDS_MEDIA_ROUTER_PRESENTATION_CAST_MODE,
349 base::UTF8ToUTF16(origin.host())),
350 model.dialog_header());
351 });
352 ui_->AddObserver(&observer);
353 ui_->RemoveObserver(&observer);
354 }
355
TEST_F(MediaRouterViewsUITest,StartCasting)356 TEST_F(MediaRouterViewsUITest, StartCasting) {
357 StartTabCasting(false);
358 }
359
TEST_F(MediaRouterViewsUITest,StopCasting)360 TEST_F(MediaRouterViewsUITest, StopCasting) {
361 EXPECT_CALL(*mock_router_, TerminateRoute(kRouteId));
362 ui_->StopCasting(kRouteId);
363 }
364
TEST_F(MediaRouterViewsUITest,RemovePseudoSink)365 TEST_F(MediaRouterViewsUITest, RemovePseudoSink) {
366 MockControllerObserver observer(ui_.get());
367
368 MediaSink sink(kSinkId, kSinkName, SinkIconType::CAST_AUDIO);
369 MediaSinkWithCastModes sink_with_cast_modes(sink);
370 sink_with_cast_modes.cast_modes = {MediaCastMode::TAB_MIRROR};
371 MediaSink pseudo_sink(kPseudoSinkId, kSinkName, SinkIconType::MEETING);
372 MediaSinkWithCastModes pseudo_sink_with_cast_modes(pseudo_sink);
373 pseudo_sink_with_cast_modes.cast_modes = {MediaCastMode::TAB_MIRROR};
374
375 EXPECT_CALL(observer, OnModelUpdated(_))
376 .WillOnce(WithArg<0>(Invoke([&sink](const CastDialogModel& model) {
377 EXPECT_EQ(1u, model.media_sinks().size());
378 EXPECT_EQ(sink.id(), model.media_sinks()[0].id);
379 })));
380 NotifyUiOnResultsUpdated({sink_with_cast_modes, pseudo_sink_with_cast_modes});
381 }
382
TEST_F(MediaRouterViewsUITest,ConnectingState)383 TEST_F(MediaRouterViewsUITest, ConnectingState) {
384 MockControllerObserver observer(ui_.get());
385
386 MediaSink sink(kSinkId, kSinkName, SinkIconType::GENERIC);
387 for (MediaSinksObserver* sinks_observer : media_sinks_observers_)
388 sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>());
389
390 // When a request to Cast to a sink is made, its state should become
391 // CONNECTING.
392 EXPECT_CALL(observer, OnModelUpdated(_))
393 .WillOnce(WithArg<0>(Invoke([](const CastDialogModel& model) {
394 ASSERT_EQ(1u, model.media_sinks().size());
395 EXPECT_EQ(UIMediaSinkState::CONNECTING, model.media_sinks()[0].state);
396 })));
397 ui_->StartCasting(kSinkId, MediaCastMode::TAB_MIRROR);
398
399 // Once a route is created for the sink, its state should become CONNECTED.
400 EXPECT_CALL(observer, OnModelUpdated(_))
401 .WillOnce(WithArg<0>(Invoke([](const CastDialogModel& model) {
402 ASSERT_EQ(1u, model.media_sinks().size());
403 EXPECT_EQ(UIMediaSinkState::CONNECTED, model.media_sinks()[0].state);
404 })));
405 MediaRoute route(kRouteId, MediaSource(kSourceId), kSinkId, "", true, true);
406 NotifyUiOnRoutesUpdated({route}, {});
407 }
408
TEST_F(MediaRouterViewsUITest,DisconnectingState)409 TEST_F(MediaRouterViewsUITest, DisconnectingState) {
410 MockControllerObserver observer(ui_.get());
411
412 MediaSink sink(kSinkId, kSinkName, SinkIconType::GENERIC);
413 MediaRoute route(kRouteId, MediaSource(kSourceId), kSinkId, "", true, true);
414 for (MediaSinksObserver* sinks_observer : media_sinks_observers_)
415 sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>());
416 NotifyUiOnRoutesUpdated({route}, {});
417
418 // When a request to stop casting to a sink is made, its state should become
419 // DISCONNECTING.
420 EXPECT_CALL(observer, OnModelUpdated(_))
421 .WillOnce(WithArg<0>(Invoke([](const CastDialogModel& model) {
422 ASSERT_EQ(1u, model.media_sinks().size());
423 EXPECT_EQ(UIMediaSinkState::DISCONNECTING,
424 model.media_sinks()[0].state);
425 })));
426 ui_->StopCasting(kRouteId);
427
428 // Once the route is removed, the sink's state should become AVAILABLE.
429 EXPECT_CALL(observer, OnModelUpdated(_))
430 .WillOnce(WithArg<0>(Invoke([](const CastDialogModel& model) {
431 ASSERT_EQ(1u, model.media_sinks().size());
432 EXPECT_EQ(UIMediaSinkState::AVAILABLE, model.media_sinks()[0].state);
433 })));
434 NotifyUiOnRoutesUpdated({}, {});
435 }
436
TEST_F(MediaRouterViewsUITest,AddAndRemoveIssue)437 TEST_F(MediaRouterViewsUITest, AddAndRemoveIssue) {
438 MediaSink sink1("sink_id1", "Sink 1", SinkIconType::CAST_AUDIO);
439 MediaSink sink2("sink_id2", "Sink 2", SinkIconType::CAST_AUDIO);
440 NotifyUiOnResultsUpdated({{sink1, {MediaCastMode::TAB_MIRROR}},
441 {sink2, {MediaCastMode::TAB_MIRROR}}});
442
443 MockControllerObserver observer(ui_.get());
444 MockIssuesObserver issues_observer(mock_router_->GetIssueManager());
445 issues_observer.Init();
446 const std::string issue_title("Issue 1");
447 IssueInfo issue(issue_title, IssueInfo::Action::DISMISS,
448 IssueInfo::Severity::WARNING);
449 issue.sink_id = sink2.id();
450 Issue::Id issue_id = -1;
451
452 EXPECT_CALL(issues_observer, OnIssue)
453 .WillOnce(
454 Invoke([&issue_id](const Issue& issue) { issue_id = issue.id(); }));
455 EXPECT_CALL(observer, OnModelUpdated(_))
456 .WillOnce(WithArg<0>(
457 Invoke([&sink1, &sink2, &issue_title](const CastDialogModel& model) {
458 EXPECT_EQ(2u, model.media_sinks().size());
459 EXPECT_EQ(model.media_sinks()[0].id, sink1.id());
460 EXPECT_FALSE(model.media_sinks()[0].issue.has_value());
461 EXPECT_EQ(model.media_sinks()[1].id, sink2.id());
462 EXPECT_EQ(model.media_sinks()[1].issue->info().title, issue_title);
463 })));
464 mock_router_->GetIssueManager()->AddIssue(issue);
465
466 EXPECT_CALL(observer, OnModelUpdated(_))
467 .WillOnce(WithArg<0>(Invoke([&sink2](const CastDialogModel& model) {
468 EXPECT_EQ(2u, model.media_sinks().size());
469 EXPECT_EQ(model.media_sinks()[1].id, sink2.id());
470 EXPECT_FALSE(model.media_sinks()[1].issue.has_value());
471 })));
472 mock_router_->GetIssueManager()->ClearIssue(issue_id);
473 }
474
TEST_F(MediaRouterViewsUITest,ShowDomainForHangouts)475 TEST_F(MediaRouterViewsUITest, ShowDomainForHangouts) {
476 const std::string domain1 = "domain1.com";
477 const std::string domain2 = "domain2.com";
478 MediaSinkWithCastModes available_hangout(
479 MediaSink("sink1", "Hangout 1", SinkIconType::HANGOUT));
480 MediaSinkWithCastModes connected_hangout(
481 MediaSink("sink2", "Hangout 2", SinkIconType::HANGOUT));
482 available_hangout.sink.set_domain(domain1);
483 connected_hangout.sink.set_domain(domain2);
484 available_hangout.cast_modes = {MediaCastMode::TAB_MIRROR};
485 connected_hangout.cast_modes = {MediaCastMode::TAB_MIRROR};
486
487 MockControllerObserver observer(ui_.get());
488 const std::string route_description = "route 1";
489 MediaRoute route(kRouteId, MediaSource(kSourceId), "sink2", route_description,
490 true, true);
491 NotifyUiOnRoutesUpdated({route}, {});
492
493 // The domain should be used as the status text only if the sink is available.
494 // If the sink has a route, the route description is used.
495 EXPECT_CALL(observer, OnModelUpdated(_))
496 .WillOnce(WithArg<0>([&](const CastDialogModel& model) {
497 EXPECT_EQ(2u, model.media_sinks().size());
498 EXPECT_EQ(model.media_sinks()[0].id, available_hangout.sink.id());
499 EXPECT_EQ(base::UTF8ToUTF16(domain1),
500 model.media_sinks()[0].status_text);
501 EXPECT_EQ(model.media_sinks()[1].id, connected_hangout.sink.id());
502 EXPECT_EQ(base::UTF8ToUTF16(route_description),
503 model.media_sinks()[1].status_text);
504 }));
505 NotifyUiOnResultsUpdated({available_hangout, connected_hangout});
506 }
507
TEST_F(MediaRouterViewsUITest,RouteCreationTimeoutForTab)508 TEST_F(MediaRouterViewsUITest, RouteCreationTimeoutForTab) {
509 StartCastingAndExpectTimeout(
510 MediaCastMode::TAB_MIRROR,
511 l10n_util::GetStringUTF8(
512 IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_TIMEOUT_FOR_TAB),
513 60);
514 }
515
TEST_F(MediaRouterViewsUITest,RouteCreationTimeoutForDesktop)516 TEST_F(MediaRouterViewsUITest, RouteCreationTimeoutForDesktop) {
517 #if defined(OS_MAC)
518 if (base::mac::IsAtLeastOS10_15())
519 ui_->set_screen_capture_allowed_for_testing(true);
520 #endif
521
522 StartCastingAndExpectTimeout(
523 MediaCastMode::DESKTOP_MIRROR,
524 l10n_util::GetStringUTF8(
525 IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_TIMEOUT_FOR_DESKTOP),
526 120);
527 }
528
TEST_F(MediaRouterViewsUITest,RouteCreationTimeoutForPresentation)529 TEST_F(MediaRouterViewsUITest, RouteCreationTimeoutForPresentation) {
530 content::PresentationRequest presentation_request(
531 {0, 0}, {GURL("https://presentationurl.com")},
532 url::Origin::Create(GURL("https://frameurl.fakeurl")));
533 ui_->OnDefaultPresentationChanged(&presentation_request);
534 StartCastingAndExpectTimeout(
535 MediaCastMode::PRESENTATION,
536 l10n_util::GetStringFUTF8(IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_TIMEOUT,
537 base::UTF8ToUTF16("frameurl.fakeurl")),
538 20);
539 }
540
541 #if defined(OS_MAC)
TEST_F(MediaRouterViewsUITest,DesktopMirroringFailsWhenDisallowedOnMac)542 TEST_F(MediaRouterViewsUITest, DesktopMirroringFailsWhenDisallowedOnMac) {
543 // Failure due to a lack of screen capture permissions only happens on macOS
544 // 10.15 or later. See crbug.com/1087236 for more info.
545 if (!base::mac::IsAtLeastOS10_15())
546 return;
547
548 ui_->set_screen_capture_allowed_for_testing(false);
549 MockControllerObserver observer(ui_.get());
550 MediaSink sink(kSinkId, kSinkName, SinkIconType::CAST);
551 ui_->OnResultsUpdated({{sink, {MediaCastMode::DESKTOP_MIRROR}}});
552 for (MediaSinksObserver* sinks_observer : media_sinks_observers_)
553 sinks_observer->OnSinksUpdated({sink}, std::vector<url::Origin>());
554
555 EXPECT_CALL(observer, OnModelUpdated(_))
556 .WillOnce(WithArg<0>([&](const CastDialogModel& model) {
557 EXPECT_EQ(
558 model.media_sinks()[0].issue->info().title,
559 l10n_util::GetStringUTF8(
560 IDS_MEDIA_ROUTER_ISSUE_MAC_SCREEN_CAPTURE_PERMISSION_ERROR));
561 }));
562 ui_->StartCasting(kSinkId, MediaCastMode::DESKTOP_MIRROR);
563 }
564 #endif
565
566 // Tests that if a local file CreateRoute call is made from a new tab, the
567 // file will be opened in the new tab.
TEST_F(MediaRouterViewsUITest,RouteCreationLocalFileModeInTab)568 TEST_F(MediaRouterViewsUITest, RouteCreationLocalFileModeInTab) {
569 const GURL empty_tab = GURL(chrome::kChromeUINewTabURL);
570 const std::string file_url = "file:///some/url/for/a/file.mp3";
571 CreateMediaRouterUIForURL(empty_tab);
572 auto file_dialog = std::make_unique<MockMediaRouterFileDialog>();
573 auto* file_dialog_ptr = file_dialog.get();
574 ui_->set_media_router_file_dialog_for_test(std::move(file_dialog));
575
576 EXPECT_CALL(*file_dialog_ptr, GetLastSelectedFileUrl())
577 .WillOnce(Return(GURL(file_url)));
578 content::WebContents* location_file_opened = nullptr;
579 EXPECT_CALL(*mock_router_, CreateRouteInternal(_, _, _, _, _, _, _))
580 .WillOnce(SaveArgWithMove<3>(&location_file_opened));
581 ui_->CreateRoute(kSinkId, MediaCastMode::LOCAL_FILE);
582 ui_->SimulateDocumentAvailableForTest();
583
584 ASSERT_EQ(location_file_opened, web_contents());
585 ASSERT_EQ(location_file_opened->GetVisibleURL(), file_url);
586 }
587
TEST_F(MediaRouterViewsUITest,SortedSinks)588 TEST_F(MediaRouterViewsUITest, SortedSinks) {
589 NotifyUiOnResultsUpdated(
590 {{MediaSink("sink3", "B sink", SinkIconType::CAST), {}},
591 {MediaSink("sink2", "A sink", SinkIconType::CAST), {}},
592 {MediaSink("sink1", "B sink", SinkIconType::CAST), {}}});
593
594 // Sort first by name, then by ID.
595 const auto& sorted_sinks = ui_->GetEnabledSinks();
596 EXPECT_EQ("sink2", sorted_sinks[0].sink.id());
597 EXPECT_EQ("sink1", sorted_sinks[1].sink.id());
598 EXPECT_EQ("sink3", sorted_sinks[2].sink.id());
599 }
600
TEST_F(MediaRouterViewsUITest,SortSinksByIconType)601 TEST_F(MediaRouterViewsUITest, SortSinksByIconType) {
602 NotifyUiOnResultsUpdated(
603 {{MediaSink("id1", "sink", SinkIconType::HANGOUT), {}},
604 {MediaSink("id2", "B sink", SinkIconType::CAST_AUDIO_GROUP), {}},
605 {MediaSink("id3", "sink", SinkIconType::GENERIC), {}},
606 {MediaSink("id4", "A sink", SinkIconType::CAST_AUDIO_GROUP), {}},
607 {MediaSink("id5", "sink", SinkIconType::CAST_AUDIO), {}},
608 {MediaSink("id6", "sink", SinkIconType::CAST), {}}});
609
610 // The sorted order is CAST, CAST_AUDIO_GROUP "A", CAST_AUDIO_GROUP "B",
611 // CAST_AUDIO, HANGOUT, GENERIC.
612 const auto& sorted_sinks = ui_->GetEnabledSinks();
613 EXPECT_EQ("id6", sorted_sinks[0].sink.id());
614 EXPECT_EQ("id4", sorted_sinks[1].sink.id());
615 EXPECT_EQ("id2", sorted_sinks[2].sink.id());
616 EXPECT_EQ("id5", sorted_sinks[3].sink.id());
617 EXPECT_EQ("id1", sorted_sinks[4].sink.id());
618 EXPECT_EQ("id3", sorted_sinks[5].sink.id());
619 }
620
TEST_F(MediaRouterViewsUITest,FilterNonDisplayRoutes)621 TEST_F(MediaRouterViewsUITest, FilterNonDisplayRoutes) {
622 MediaSource media_source("mediaSource");
623 MediaRoute display_route_1("routeId1", media_source, "sinkId1", "desc 1",
624 true, true);
625 MediaRoute non_display_route_1("routeId2", media_source, "sinkId2", "desc 2",
626 true, false);
627 MediaRoute display_route_2("routeId3", media_source, "sinkId2", "desc 2",
628 true, true);
629
630 NotifyUiOnRoutesUpdated(
631 {display_route_1, non_display_route_1, display_route_2}, {});
632 ASSERT_EQ(2u, ui_->routes().size());
633 EXPECT_EQ(display_route_1, ui_->routes()[0]);
634 EXPECT_TRUE(ui_->routes()[0].for_display());
635 EXPECT_EQ(display_route_2, ui_->routes()[1]);
636 EXPECT_TRUE(ui_->routes()[1].for_display());
637 }
638
TEST_F(MediaRouterViewsUITest,NotFoundErrorOnCloseWithNoSinks)639 TEST_F(MediaRouterViewsUITest, NotFoundErrorOnCloseWithNoSinks) {
640 auto request_callbacks = ExpectPresentationError(
641 blink::mojom::PresentationErrorType::NO_AVAILABLE_SCREENS,
642 "No screens found.");
643 // Destroying the UI should return the expected error from above to the error
644 // callback.
645 ui_.reset();
646 }
647
TEST_F(MediaRouterViewsUITest,NotFoundErrorOnCloseWithNoCompatibleSinks)648 TEST_F(MediaRouterViewsUITest, NotFoundErrorOnCloseWithNoCompatibleSinks) {
649 auto request_callbacks = ExpectPresentationError(
650 blink::mojom::PresentationErrorType::NO_AVAILABLE_SCREENS,
651 "No screens found.");
652 // Send a sink to the UI that is compatible with sources other than the
653 // presentation url to cause a NotFoundError.
654 std::vector<MediaSink> sinks = {{kSinkId, kSinkName, SinkIconType::GENERIC}};
655 auto presentation_source = MediaSource::ForPresentationUrl(
656 presentation_request_.presentation_urls[0]);
657 for (MediaSinksObserver* sinks_observer : media_sinks_observers_) {
658 if (!(sinks_observer->source() == presentation_source)) {
659 sinks_observer->OnSinksUpdated(sinks, {});
660 }
661 }
662 // Destroying the UI should return the expected error from above to the error
663 // callback.
664 ui_.reset();
665 }
666
TEST_F(MediaRouterViewsUITest,AbortErrorOnClose)667 TEST_F(MediaRouterViewsUITest, AbortErrorOnClose) {
668 auto request_callbacks = ExpectPresentationError(
669 blink::mojom::PresentationErrorType::PRESENTATION_REQUEST_CANCELLED,
670 "Dialog closed.");
671 // Send a sink to the UI that is compatible with the presentation url to avoid
672 // a NotFoundError.
673 std::vector<MediaSink> sinks = {{kSinkId, kSinkName, SinkIconType::GENERIC}};
674 auto presentation_source = MediaSource::ForPresentationUrl(
675 presentation_request_.presentation_urls[0]);
676 for (MediaSinksObserver* sinks_observer : media_sinks_observers_) {
677 if (sinks_observer->source() == presentation_source) {
678 sinks_observer->OnSinksUpdated(sinks, {});
679 }
680 }
681 // Destroying the UI should return the expected error from above to the error
682 // callback.
683 ui_.reset();
684 }
685
686 // A wired display sink should not be on the sinks list when the dialog is on
687 // that display, to prevent showing a fullscreen presentation window over the
688 // controlling window.
TEST_F(MediaRouterViewsUITest,UpdateSinksWhenDialogMovesToAnotherDisplay)689 TEST_F(MediaRouterViewsUITest, UpdateSinksWhenDialogMovesToAnotherDisplay) {
690 MockControllerObserver observer(ui_.get());
691 const display::Display display1(1000001);
692 const display::Display display2(1000002);
693 const std::string display_sink_id1 =
694 WiredDisplayMediaRouteProvider::GetSinkIdForDisplay(display1);
695 const std::string display_sink_id2 =
696 WiredDisplayMediaRouteProvider::GetSinkIdForDisplay(display2);
697
698 auto display_observer_unique =
699 std::make_unique<TestWebContentsDisplayObserver>(display1);
700 TestWebContentsDisplayObserver* display_observer =
701 display_observer_unique.get();
702 ui_->display_observer_ = std::move(display_observer_unique);
703
704 NotifyUiOnResultsUpdated(
705 {{MediaSink(display_sink_id1, "sink", SinkIconType::GENERIC), {}},
706 {MediaSink(display_sink_id2, "sink", SinkIconType::GENERIC), {}},
707 {MediaSink("id3", "sink", SinkIconType::GENERIC), {}}});
708
709 // Initially |display_sink_id1| should not be on the sinks list because we are
710 // on |display1|.
711 EXPECT_CALL(observer, OnModelUpdated(_))
712 .WillOnce(WithArg<0>([&](const CastDialogModel& model) {
713 const auto& sinks = model.media_sinks();
714 EXPECT_EQ(2u, sinks.size());
715 EXPECT_TRUE(std::find_if(sinks.begin(), sinks.end(),
716 [&](const UIMediaSink& sink) {
717 return sink.id == display_sink_id1;
718 }) == sinks.end());
719 }));
720 ui_->UpdateSinks();
721 Mock::VerifyAndClearExpectations(&observer);
722
723 // Change the display to |display2|. Now |display_sink_id2| should be removed
724 // from the list of sinks.
725 EXPECT_CALL(observer, OnModelUpdated(_))
726 .WillOnce(WithArg<0>([&](const CastDialogModel& model) {
727 const auto& sinks = model.media_sinks();
728 EXPECT_EQ(2u, sinks.size());
729 EXPECT_TRUE(std::find_if(sinks.begin(), sinks.end(),
730 [&](const UIMediaSink& sink) {
731 return sink.id == display_sink_id2;
732 }) == sinks.end());
733 }));
734 display_observer->set_display(display2);
735 ui_->UpdateSinks();
736 }
737
738 class MediaRouterViewsUIIncognitoTest : public MediaRouterViewsUITest {
739 protected:
SetMediaRouterFactory()740 void SetMediaRouterFactory() override {
741 // We must set the factory on the non-incognito browser context.
742 MediaRouterFactory::GetInstance()->SetTestingFactory(
743 MediaRouterViewsUITest::GetBrowserContext(),
744 base::BindRepeating(&MockMediaRouter::Create));
745 }
746
GetBrowserContext()747 content::BrowserContext* GetBrowserContext() override {
748 return static_cast<Profile*>(MediaRouterViewsUITest::GetBrowserContext())
749 ->GetPrimaryOTRProfile();
750 }
751 };
752
TEST_F(MediaRouterViewsUIIncognitoTest,HidesCloudSinksForIncognito)753 TEST_F(MediaRouterViewsUIIncognitoTest, HidesCloudSinksForIncognito) {
754 const std::string domain1 = "domain1.com";
755 MediaSinkWithCastModes hangout(
756 MediaSink("sink1", "Hangout", SinkIconType::HANGOUT));
757 MediaSinkWithCastModes meeting(
758 MediaSink("sink2", "Meeting", SinkIconType::MEETING));
759 MediaSinkWithCastModes eduReceiver(
760 MediaSink("sink3", "Cast for EDU", SinkIconType::EDUCATION));
761 MediaSinkWithCastModes chromeCast(
762 MediaSink("sink4", "Living Room TV", SinkIconType::CAST));
763 chromeCast.cast_modes = {MediaCastMode::TAB_MIRROR};
764 for (MediaSinkWithCastModes* sink :
765 std::initializer_list<MediaSinkWithCastModes*>{&hangout, &meeting,
766 &eduReceiver}) {
767 sink->sink.set_domain(domain1);
768 sink->cast_modes = {MediaCastMode::TAB_MIRROR};
769 }
770
771 NotifyUiOnResultsUpdated({hangout, meeting, eduReceiver, chromeCast});
772
773 EXPECT_EQ(std::vector<MediaSinkWithCastModes>{chromeCast},
774 ui_->GetEnabledSinks());
775 }
776
TEST_F(MediaRouterViewsUIIncognitoTest,RouteRequestFromIncognito)777 TEST_F(MediaRouterViewsUIIncognitoTest, RouteRequestFromIncognito) {
778 StartTabCasting(true);
779 }
780
781 } // namespace media_router
782