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