chromium/ash/system/cast/media_cast_list_view_unittest.cc

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

#include "ash/system/cast/media_cast_list_view.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/style/pill_button.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/test/ash_test_base.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "media/base/media_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

namespace ash {

class MediaCastListViewTest : public AshTestBase {
 public:
  // Mock callbacks:
  MOCK_METHOD(void, OnStopCasting, (), ());
  MOCK_METHOD(void, OnCastDeviceSelected, (const std::string& device_id), ());
  MOCK_METHOD(void, OnDeviceListUpdated, (const bool has_devices), ());

  // AshTestBase:
  void SetUp() override {
    feature_list_.InitAndEnableFeature(media::kBackgroundListening);
    AshTestBase::SetUp();
    // Create a widget so tests can click on views.
    widget_ = CreateFramelessTestWidget();
    widget_->SetFullscreen(true);
    view_ = widget_->SetContentsView(std::make_unique<MediaCastListView>(
        base::BindRepeating(&MediaCastListViewTest::OnStopCasting,
                            base::Unretained(this)),
        base::BindRepeating(&MediaCastListViewTest::OnCastDeviceSelected,
                            base::Unretained(this)),
        base::BindRepeating(&MediaCastListViewTest::OnDeviceListUpdated,
                            base::Unretained(this)),
        client_remote_.BindNewPipeAndPassReceiver()));
  }

  void TearDown() override {
    view_ = nullptr;
    widget_.reset();
    AshTestBase::TearDown();
  }

  MediaCastListView* media_cast_list_view() { return view_; }
  std::vector<raw_ptr<views::View, VectorExperimental>>
  GetContainerChildViews() {
    return view_->item_container_->children();
  }

  views::View* GetStopCastingButton() {
    return view_->GetViewByID(kStopCastingButtonId);
  }

  // Adds 3 simulated cast devices.
  void AddCastDevices() {
    std::vector<global_media_controls::mojom::DevicePtr> devices;
    global_media_controls::mojom::DevicePtr device1 =
        global_media_controls::mojom::Device::New(
            /*id=*/"fake_sink_id_1",
            /*name=*/"Sink Name 1",
            /*status_text=*/"",
            /*icon=*/global_media_controls::mojom::IconType::kTv);
    devices.push_back(std::move(device1));
    global_media_controls::mojom::DevicePtr device2 =
        global_media_controls::mojom::Device::New(
            /*id=*/"fake_sink_id_2",
            /*name=*/"Sink Name 2",
            /*status_text=*/"",
            /*icon=*/global_media_controls::mojom::IconType::kSpeaker);
    devices.push_back(std::move(device2));
    global_media_controls::mojom::DevicePtr device3 =
        global_media_controls::mojom::Device::New(
            /*id=*/"fake_sink_id_3",
            /*name=*/"Sink Name 3",
            /*status_text=*/"",
            /*icon=*/global_media_controls::mojom::IconType::kSpeakerGroup);
    devices.push_back(std::move(device3));
    view_->OnDevicesUpdated(std::move(devices));
  }

  // Removes simulated cast devices.
  void ResetCastDevices() { view_->OnDevicesUpdated({}); }

  // The entries may not be rendered immediately due to the async call of
  // `InvalidateLayout`. Here we manually call `layout` to make sure the views
  // are rendered.
  void LayoutEntriesIfNecessary() {
    view_->GetWidget()->LayoutRootViewIfNecessary();
  }

  base::test::ScopedFeatureList feature_list_;
  std::unique_ptr<views::Widget> widget_;
  raw_ptr<MediaCastListView> view_ = nullptr;
  bool stop_casting_ = false;
  bool start_casting_ = false;
  bool update_devices_callback_ = false;
  bool is_updated_devices_empty_ = false;
  std::string current_device_id_;
  mojo::Remote<global_media_controls::mojom::DeviceListClient> client_remote_;
};

TEST_F(MediaCastListViewTest, ViewsCreatedForCastDevices) {
  // Adding cast devices creates views.
  AddCastDevices();
  LayoutEntriesIfNecessary();

  // One header row and 3 device row.
  ASSERT_EQ(GetContainerChildViews().size(), 4u);

  HoverHighlightView* row1 =
      views::AsViewClass<HoverHighlightView>(GetContainerChildViews()[1]);
  EXPECT_EQ(row1->text_label()->GetText(), u"Sink Name 1");
  EXPECT_CALL(*this, OnCastDeviceSelected("fake_sink_id_1")).Times(1);
  LeftClickOn(row1);
  testing::Mock::VerifyAndClearExpectations(this);

  HoverHighlightView* row2 =
      static_cast<HoverHighlightView*>(GetContainerChildViews()[2]);
  EXPECT_EQ(row2->text_label()->GetText(), u"Sink Name 2");
  EXPECT_CALL(*this, OnCastDeviceSelected("fake_sink_id_2")).Times(1);
  LeftClickOn(row2);
  testing::Mock::VerifyAndClearExpectations(this);

  HoverHighlightView* row3 =
      static_cast<HoverHighlightView*>(GetContainerChildViews()[3]);
  EXPECT_EQ(row3->text_label()->GetText(), u"Sink Name 3");
  EXPECT_CALL(*this, OnCastDeviceSelected("fake_sink_id_3")).Times(1);
  LeftClickOn(row3);
  testing::Mock::VerifyAndClearExpectations(this);
}

TEST_F(MediaCastListViewTest, WithNoDevices) {
  // Updates with an default device list.
  EXPECT_CALL(*this, OnDeviceListUpdated).Times(1);
  AddCastDevices();
  EXPECT_EQ(GetContainerChildViews().size(), 4u);
  ASSERT_TRUE(GetStopCastingButton());
  testing::Mock::VerifyAndClearExpectations(this);

  // Updates with an empty device list. Should not show `StopCastingButton`.
  EXPECT_CALL(*this, OnDeviceListUpdated).Times(1);
  ResetCastDevices();
  EXPECT_EQ(GetContainerChildViews().size(), 0u);
  EXPECT_EQ(GetStopCastingButton(), nullptr);
  testing::Mock::VerifyAndClearExpectations(this);
}

TEST_F(MediaCastListViewTest, StartAndStopCasting) {
  AddCastDevices();
  LayoutEntriesIfNecessary();

  EXPECT_FALSE(GetContainerChildViews().empty());
  views::View* first_view = GetContainerChildViews()[1];

  // Clicking on an entry triggers a cast session.
  EXPECT_CALL(*this, OnCastDeviceSelected("fake_sink_id_1")).Times(1);
  LeftClickOn(first_view);
  testing::Mock::VerifyAndClearExpectations(this);

  // Clicking on stop button triggers stop casting.
  EXPECT_CALL(*this, OnStopCasting).Times(1);
  LeftClickOn(GetStopCastingButton());
  testing::Mock::VerifyAndClearExpectations(this);
}

}  // namespace ash