chromium/chrome/browser/ui/ash/system/system_tray_tray_cast_browsertest_media_router_chromeos.cc

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>
#include <vector>

#include "ash/public/cpp/ash_view_ids.h"
#include "ash/public/cpp/cast_config_controller.h"
#include "ash/public/cpp/system_tray_test_api.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/system/cast/cast_detailed_view.h"
#include "ash/system/cast/unified_cast_detailed_view_controller.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ash/login/login_manager_test.h"
#include "chrome/browser/ash/login/session/user_session_manager.h"
#include "chrome/browser/ash/login/session/user_session_manager_test_api.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/cast_config/cast_config_controller_media_router.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/media_router/access_code_cast/access_code_cast_integration_browsertest.h"
#include "chromeos/ash/components/login/auth/public/key.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "components/account_id/account_id.h"
#include "components/media_router/browser/media_routes_observer.h"
#include "components/media_router/browser/media_sinks_observer.h"
#include "components/media_router/browser/test/mock_media_router.h"
#include "components/media_router/common/media_source.h"
#include "components/media_router/common/test/test_helper.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/test/event_generator.h"
#include "ui/message_center/message_center.h"

using ::ash::ProfileHelper;
using testing::_;
using user_manager::UserManager;

namespace {

const char kNotificationId[] = "chrome://cast";

const char kEndpointResponseSuccess[] =
    R"({
    "device": {
      "displayName": "test_device",
      "id": "1234",
      "deviceCapabilities": {
        "videoOut": true,
        "videoIn": true,
        "audioOut": true,
        "audioIn": true,
        "devMode": true
      },
      "networkInfo": {
        "hostName": "GoogleNet",
        "port": "666",
        "ipV4Address": "192.0.2.146",
        "ipV6Address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
      }
    }
  })";

// Helper to create a MediaRoute instance.
media_router::MediaRoute MakeRoute(const std::string& route_id,
                                   const std::string& sink_id,
                                   bool is_local) {
  return media_router::MediaRoute(
      route_id, media_router::MediaSource::ForUnchosenDesktop(), sink_id,
      "description", is_local);
}

class SystemTrayTrayCastMediaRouterChromeOSTest : public InProcessBrowserTest {
 public:
  SystemTrayTrayCastMediaRouterChromeOSTest(
      const SystemTrayTrayCastMediaRouterChromeOSTest&) = delete;
  SystemTrayTrayCastMediaRouterChromeOSTest& operator=(
      const SystemTrayTrayCastMediaRouterChromeOSTest&) = delete;

 protected:
  SystemTrayTrayCastMediaRouterChromeOSTest() = default;

  ~SystemTrayTrayCastMediaRouterChromeOSTest() override = default;

  void ShowBubble() { tray_test_api_->ShowBubble(); }

  bool IsViewDrawn(int view_id) {
    return tray_test_api_->IsBubbleViewVisible(view_id, false /* open_tray */);
  }

  bool IsTrayVisible() { return IsViewDrawn(ash::VIEW_ID_FEATURE_TILE_CAST); }

  bool IsCastingNotificationVisible() {
    return !GetNotificationString().empty();
  }

  std::u16string GetNotificationString() {
    message_center::NotificationList::Notifications notification_set =
        message_center::MessageCenter::Get()->GetVisibleNotifications();
    for (message_center::Notification* notification : notification_set) {
      if (notification->id() == kNotificationId) {
        return notification->title();
      }
    }
    return std::u16string();
  }

  media_router::MediaSinksObserver* media_sinks_observer() const {
    DCHECK(media_sinks_observer_);
    return media_sinks_observer_;
  }

  media_router::MediaRoutesObserver* media_routes_observer() const {
    return &(*media_router_->routes_observers().begin());
  }

 private:
  // InProcessBrowserTest:
  void SetUp() override {
    // This makes sure CastDeviceCache is not initialized until after the
    // MockMediaRouter is ready. (MockMediaRouter can't be constructed yet.)
    CastConfigControllerMediaRouter::SetMediaRouterForTest(nullptr);
    InProcessBrowserTest::SetUp();
  }

  void PreRunTestOnMainThread() override {
    media_router_ = std::make_unique<media_router::MockMediaRouter>();
    ON_CALL(*media_router_, RegisterMediaSinksObserver(_))
        .WillByDefault(Invoke(
            this, &SystemTrayTrayCastMediaRouterChromeOSTest::CaptureSink));
    CastConfigControllerMediaRouter::SetMediaRouterForTest(media_router_.get());
    InProcessBrowserTest::PreRunTestOnMainThread();
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    tray_test_api_ = ash::SystemTrayTestApi::Create();
  }

  void PostRunTestOnMainThread() override {
    InProcessBrowserTest::PostRunTestOnMainThread();
    CastConfigControllerMediaRouter::SetMediaRouterForTest(nullptr);
  }

  bool CaptureSink(media_router::MediaSinksObserver* media_sinks_observer) {
    media_sinks_observer_ = media_sinks_observer;
    return true;
  }

  std::unique_ptr<media_router::MockMediaRouter> media_router_;
  raw_ptr<media_router::MediaSinksObserver, DanglingUntriaged>
      media_sinks_observer_ = nullptr;
  std::unique_ptr<ash::SystemTrayTestApi> tray_test_api_;
};

}  // namespace

// Verifies that we only show the tray view if there are available cast
// targets/sinks.
IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastMediaRouterChromeOSTest,
                       VerifyCorrectVisiblityWithSinks) {
  ShowBubble();

    // The tray defaults to visible.
    EXPECT_TRUE(IsTrayVisible());

  std::vector<media_router::MediaSink> zero_sinks;
  std::vector<media_router::MediaSink> one_sink;
  std::vector<media_router::MediaSink> two_sinks;
  one_sink.push_back(media_router::CreateCastSink("id1", "name"));
  two_sinks.push_back(media_router::CreateCastSink("id1", "name"));
  two_sinks.push_back(media_router::CreateCastSink("id2", "name"));

  media_sinks_observer()->OnSinksUpdated(zero_sinks,
                                         std::vector<url::Origin>());
  // The tray is always visible.
  EXPECT_TRUE(IsTrayVisible());

  // The tray should be visible with any more than zero sinks.
  media_sinks_observer()->OnSinksUpdated(one_sink, std::vector<url::Origin>());
  EXPECT_TRUE(IsTrayVisible());
  media_sinks_observer()->OnSinksUpdated(two_sinks, std::vector<url::Origin>());
  EXPECT_TRUE(IsTrayVisible());

  // And if all of the sinks go away, it should be hidden again.
  media_sinks_observer()->OnSinksUpdated(zero_sinks,
                                         std::vector<url::Origin>());
  // The tray is always visible.
  EXPECT_TRUE(IsTrayVisible());
}

// Verifies that we show the cast view when we start a casting session, and that
// we display the correct cast session if there are multiple active casting
// sessions.
IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastMediaRouterChromeOSTest,
                       VerifyCastingShowsCastView) {
  ShowBubble();

  // Setup the sinks.
  const std::vector<media_router::MediaSink> sinks = {
      media_router::CreateCastSink("remote_sink", "Remote Sink"),
      media_router::CreateCastSink("local_sink", "Local Sink")};
  media_sinks_observer()->OnSinksUpdated(sinks, std::vector<url::Origin>());

  // Create route combinations. More details below.
  const media_router::MediaRoute non_local_route =
      MakeRoute("remote_route", "remote_sink", false /*is_local*/);
  const media_router::MediaRoute local_route =
      MakeRoute("local_route", "local_sink", true /*is_local*/);
  const std::vector<media_router::MediaRoute> no_routes;
  const std::vector<media_router::MediaRoute> non_local_routes{non_local_route};
  // We put the non-local route first to make sure that we prefer the local one.
  const std::vector<media_router::MediaRoute> multiple_routes{non_local_route,
                                                              local_route};

  // We do not show the cast view for non-local routes.
  media_routes_observer()->OnRoutesUpdated(non_local_routes);
  EXPECT_FALSE(IsCastingNotificationVisible());

  // If there are multiple routes active at the same time, then we need to
  // display the local route over a non-local route. This also verifies that we
  // display the cast view when we're casting.
  media_routes_observer()->OnRoutesUpdated(multiple_routes);
  EXPECT_TRUE(IsCastingNotificationVisible());
  EXPECT_NE(std::u16string::npos, GetNotificationString().find(u"Local Sink"));

  // When a casting session stops, we shouldn't display the cast view.
  media_routes_observer()->OnRoutesUpdated(no_routes);
  EXPECT_FALSE(IsCastingNotificationVisible());
}

class SystemTrayTrayCastAccessCodeChromeOSTest
    : public media_router::AccessCodeCastIntegrationBrowserTest {
 public:
  SystemTrayTrayCastAccessCodeChromeOSTest() {
    // Use consumer emails to avoid having to fake a policy fetch.
    login_mixin_.AppendRegularUsers(2);
    login_mixin_.SetShouldLaunchBrowser(true);
    account_id1_ = login_mixin_.users()[0].account_id;
    account_id2_ = login_mixin_.users()[1].account_id;
  }

  SystemTrayTrayCastAccessCodeChromeOSTest(
      const SystemTrayTrayCastAccessCodeChromeOSTest&) = delete;
  SystemTrayTrayCastAccessCodeChromeOSTest& operator=(
      const SystemTrayTrayCastAccessCodeChromeOSTest&) = delete;

  ~SystemTrayTrayCastAccessCodeChromeOSTest() override = default;

  void PreRunTestOnMainThread() override {
    CastConfigControllerMediaRouter::SetMediaRouterForTest(media_router_);
    InProcessBrowserTest::PreRunTestOnMainThread();
  }

  void SetUpOnMainThread() override {
    ash::test::UserSessionManagerTestApi session_manager_test_api(
        ash::UserSessionManager::GetInstance());
    session_manager_test_api.SetShouldLaunchBrowserInTests(true);
    session_manager_test_api.SetShouldObtainTokenHandleInTests(false);

    AccessCodeCastIntegrationBrowserTest::SetUpOnMainThread();
    MixinBasedInProcessBrowserTest::SetUpOnMainThread();
    tray_test_api_ = ash::SystemTrayTestApi::Create();

    event_generator_ = std::make_unique<ui::test::EventGenerator>(  // IN-TEST
        ash::Shell::GetPrimaryRootWindow());
  }

  ui::test::EventGenerator* GetEventGenerator() {
    return event_generator_.get();
  }

 protected:
  void SetupUserProfile(const AccountId& account_id, bool allow_access_code) {
    user_ = UserManager::Get()->FindUser(account_id);
    Profile* profile = ProfileHelper::Get()->GetProfileByUser(user_);
    profile->GetPrefs()->SetBoolean(media_router::prefs::kAccessCodeCastEnabled,
                                    allow_access_code);
    content::RunAllTasksUntilIdle();
  }

  ash::UserContext CreateUserContext(const AccountId& account_id,
                                     const std::string& password) {
    ash::UserContext user_context(user_manager::UserType::kRegular, account_id);
    user_context.SetKey(ash::Key(password));
    user_context.SetPasswordKey(ash::Key(password));
    if (account_id.GetUserEmail() == FakeGaiaMixin::kEnterpriseUser1) {
      user_context.SetRefreshToken(FakeGaiaMixin::kTestRefreshToken1);
    } else if (account_id.GetUserEmail() == FakeGaiaMixin::kEnterpriseUser2) {
      user_context.SetRefreshToken(FakeGaiaMixin::kTestRefreshToken2);
    }
    return user_context;
  }

  void ShowBubble() { tray_test_api_->ShowBubble(); }

  void CloseBubble() { tray_test_api_->CloseBubble(); }

  bool IsViewDrawn(int view_id) {
    return tray_test_api_->IsBubbleViewVisible(view_id, /* open_tray */ false);
  }

  void ClickView(int view_id) { tray_test_api_->ClickBubbleView(view_id); }

  bool IsTrayVisible() { return IsViewDrawn(ash::VIEW_ID_FEATURE_TILE_CAST); }

  // Returns the status area widget.
  ash::StatusAreaWidget* FindStatusAreaWidget() {
    return ash::Shelf::ForWindow(ash::Shell::GetRootWindowForNewWindows())
        ->shelf_widget()
        ->status_area_widget();
  }

  // Performs a tap of the specified |view|.
  void TapOn(const views::View* view) {
    GetEventGenerator()->MoveTouch(view->GetBoundsInScreen().CenterPoint());
    GetEventGenerator()->ClickLeftButton();
  }

  ash::CastDetailedView* GetCastDetailedView() {
    return GetUnifiedCastDetailedViewController()
        ->get_cast_detailed_view_for_testing();  // IN-TEST
  }

  ash::UnifiedSystemTrayController* GetUnifiedSystemTrayController() {
    return FindStatusAreaWidget()
        ->unified_system_tray()
        ->bubble()
        ->unified_system_tray_controller();
  }

  ash::UnifiedCastDetailedViewController*
  GetUnifiedCastDetailedViewController() {
    return static_cast<ash::UnifiedCastDetailedViewController*>(
        GetUnifiedSystemTrayController()->detailed_view_controller());
  }

  AccountId account_id1_;
  AccountId account_id2_;

  ash::LoginManagerMixin login_mixin_{&mixin_host_};

  std::unique_ptr<ui::test::EventGenerator> event_generator_;
  raw_ptr<const user_manager::User, DanglingUntriaged> user_;

 private:
  std::unique_ptr<ash::SystemTrayTestApi> tray_test_api_;
};

IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastAccessCodeChromeOSTest,
                       PolicyOffNoSinksNoVisibleTray) {
  const ash::UserContext user_context =
      CreateUserContext(account_id1_, "password");

  // Login a user that does not have access code casting enabled.
  ASSERT_TRUE(login_mixin_.LoginAndWaitForActiveSession(user_context));
  SetupUserProfile(account_id1_, /* allow_access_code */ false);

  ShowBubble();

    // The tray is always visible.
  EXPECT_TRUE(IsTrayVisible());
}

IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastAccessCodeChromeOSTest,
                       PolicyOnNoSinksVisibleTray) {
  const ash::UserContext user_context =
      CreateUserContext(account_id2_, "password");

  // Login a user that does have access code casting enabled.
  ASSERT_TRUE(login_mixin_.LoginAndWaitForActiveSession(user_context));
  SetupUserProfile(account_id2_, /* allow_access_code */ true);

  ShowBubble();

  // Even though there are no sinks, since this user does have access code
  // casting enabled, the tray should be visible.
  EXPECT_TRUE(IsTrayVisible());
}

IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastAccessCodeChromeOSTest,
                       SimulateValidCastingWorkflow) {
  AddScreenplayTag(AccessCodeCastIntegrationBrowserTest::
                       kAccessCodeCastNewDeviceScreenplayTag);

  const ash::UserContext user_context =
      CreateUserContext(account_id2_, "password");

  // Login a user that does have access code casting enabled.
  ASSERT_TRUE(login_mixin_.LoginAndWaitForActiveSession(user_context));
  SetupUserProfile(account_id2_, /* allow_access_code */ true);

  ShowBubble();

  // Show the Cast detailed view menu.
  GetUnifiedSystemTrayController()->ShowCastDetailedView();

  auto* detailed_cast_view = GetCastDetailedView();
  ASSERT_TRUE(detailed_cast_view);

  auto* access_code_cast_button =
      detailed_cast_view->get_add_access_code_device_for_testing();  // IN-TEST
  ASSERT_TRUE(access_code_cast_button);
  ASSERT_TRUE(access_code_cast_button->GetEnabled());

  // Mock a successful fetch from our server.
  SetEndpointFetcherMockResponse(kEndpointResponseSuccess, net::HTTP_OK,
                                 net::OK);

  // Simulate a successful opening of the channel.
  SetMockOpenChannelCallbackResponse(true);

  SetUpPrimaryAccountWithHostedDomain(
      signin::ConsentLevel::kSync,
      ProfileHelper::Get()->GetProfileByUser(user_), /*sign_in_account=*/false);

  content::WebContentsAddedObserver observer;
  TapOn(access_code_cast_button);

  content::WebContents* dialog_contents = observer.GetWebContents();
  ASSERT_TRUE(content::WaitForLoadStop(dialog_contents));

  SetAccessCode("abcdef", dialog_contents);

  // TODO(crbug.com/1291738): There is a validation process with desktop media
  // requests which are unnecessary for the complexity of this browsertest. We
  // are just passing in a hardcoded magic string instead.
  ExpectStartRouteCallFromTabMirroring(
      "cast:<1234>", "urn:x-org.chromium.media:source:desktop", nullptr,
      base::Seconds(120), media_router_);

  PressSubmitAndWaitForClose(dialog_contents);
}

// First open the cast dialog from browser, then open another cast dialog from
// the system tray. Before the change, such behavior will cause a crash. After
// the change, the first dialog will close when the second dialog opens.
IN_PROC_BROWSER_TEST_F(SystemTrayTrayCastAccessCodeChromeOSTest,
                       BrowserAndSystemTrayCasting) {
  const ash::UserContext user_context =
      CreateUserContext(account_id2_, "password");

  // Login a user that does have access code casting enabled.
  ASSERT_TRUE(login_mixin_.LoginAndWaitForActiveSession(user_context));
  SetupUserProfile(account_id2_, /* allow_access_code */ true);

  // Show the first cast dialog from the browser.
  SelectFirstBrowser();
  EnableAccessCodeCasting();
  ASSERT_TRUE(ShowDialog());

  ShowBubble();

  // Show the Cast detailed view menu.
  GetUnifiedSystemTrayController()->ShowCastDetailedView();

  auto* detailed_cast_view = GetCastDetailedView();
  ASSERT_TRUE(detailed_cast_view);

  auto* access_code_cast_button =
      detailed_cast_view->get_add_access_code_device_for_testing();  // IN-TEST
  ASSERT_TRUE(access_code_cast_button);
  ASSERT_TRUE(access_code_cast_button->GetEnabled());

  // Mock a successful fetch from our server.
  SetEndpointFetcherMockResponse(kEndpointResponseSuccess, net::HTTP_OK,
                                 net::OK);

  // Simulate a successful opening of the channel.
  SetMockOpenChannelCallbackResponse(true);

  SetUpPrimaryAccountWithHostedDomain(
      signin::ConsentLevel::kSync,
      ProfileHelper::Get()->GetProfileByUser(user_), /*sign_in_account=*/false);

  content::WebContentsAddedObserver observer;

  // Show the second dialog from the system tray.
  TapOn(access_code_cast_button);

  content::WebContents* dialog_contents = observer.GetWebContents();
  ASSERT_TRUE(content::WaitForLoadStop(dialog_contents));

  SetAccessCode("abcdef", dialog_contents);

  // TODO(crbug.com/1291738): There is a validation process with desktop media
  // requests which are unnecessary for the complexity of this browsertest. We
  // are just passing in a hardcoded magic string instead.
  ExpectStartRouteCallFromTabMirroring(
      "cast:<1234>", "urn:x-org.chromium.media:source:desktop", nullptr,
      base::Seconds(120), media_router_);

  PressSubmitAndWaitForClose(dialog_contents);
}