chromium/ash/system/video_conference/bubble/bubble_view_unittest.cc

// Copyright 2022 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/video_conference/bubble/bubble_view.h"

#include <string_view>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/tab_slider.h"
#include "ash/style/tab_slider_button.h"
#include "ash/system/camera/camera_effects_controller.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/bubble/set_value_effects_view.h"
#include "ash/system/video_conference/effects/fake_video_conference_effects.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_delegate.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager_types.h"
#include "ash/system/video_conference/fake_video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_common.h"
#include "ash/system/video_conference/video_conference_tray.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/unguessable_token.h"
#include "chromeos/crosapi/mojom/video_conference.mojom-shared.h"
#include "chromeos/crosapi/mojom/video_conference.mojom.h"

namespace ash::video_conference {

namespace {

constexpr int kSquareCinnamonCerealViewId = 1235;
constexpr int kSnackNationForeverViewId = 1236;

// A fake `kToggle` effect.
class SquareCinnamonCereal : public VcEffectsDelegate {
 public:
  explicit SquareCinnamonCereal(
      VcHostedEffect::ResourceDependencyFlags dependency_flags) {
    auto effect = std::make_unique<VcHostedEffect>(
        VcEffectType::kToggle,
        base::BindRepeating(&SquareCinnamonCereal::GetEffectState,
                            base::Unretained(this),
                            /*effect_id=*/VcEffectId::kTestEffect),
        VcEffectId::kTestEffect);
    effect->set_container_id(kSquareCinnamonCerealViewId);
    effect->set_dependency_flags(dependency_flags);

    auto state = std::make_unique<VcEffectState>(
        &kVideoConferenceBackgroundBlurMaximumIcon, u"Square Cinnamon Cereal",
        IDS_PRIVACY_INDICATORS_STATUS_CAMERA,
        base::BindRepeating(&SquareCinnamonCereal::OnEffectControlActivated,
                            base::Unretained(this),
                            /*effect_id=*/VcEffectId::kTestEffect,
                            /*value=*/std::nullopt));
    effect->AddState(std::move(state));

    AddEffect(std::move(effect));
  }
  SquareCinnamonCereal(const SquareCinnamonCereal&) = delete;
  SquareCinnamonCereal& operator=(const SquareCinnamonCereal&) = delete;
  ~SquareCinnamonCereal() override = default;

  // VcEffectsDelegate:
  std::optional<int> GetEffectState(VcEffectId effect_id) override { return 0; }
  void OnEffectControlActivated(VcEffectId effect_id,
                                std::optional<int> state) override {}
};

// A fake `kSetValue` effect.
class SnackNationForever : public VcEffectsDelegate {
 public:
  explicit SnackNationForever(
      VcHostedEffect::ResourceDependencyFlags dependency_flags) {
    auto effect = std::make_unique<VcHostedEffect>(
        VcEffectType::kSetValue,
        base::BindRepeating(&SnackNationForever::GetEffectState,
                            base::Unretained(this),
                            /*effect_id=*/VcEffectId::kTestEffect),
        VcEffectId::kTestEffect);
    auto state = std::make_unique<VcEffectState>(
        &ash::kPrivacyIndicatorsCameraIcon, u"Snack Nation",
        IDS_PRIVACY_INDICATORS_STATUS_CAMERA,
        base::BindRepeating(&SnackNationForever::OnEffectControlActivated,
                            base::Unretained(this),
                            /*effect_id=*/VcEffectId::kTestEffect,
                            /*value=*/0),
        /*state=*/0);
    effect->AddState(std::move(state));
    effect->set_container_id(kSnackNationForeverViewId);
    effect->set_dependency_flags(dependency_flags);
    AddEffect(std::move(effect));
  }
  SnackNationForever(const SnackNationForever&) = delete;
  SnackNationForever& operator=(const SnackNationForever&) = delete;
  ~SnackNationForever() override = default;

  // VcEffectsDelegate:
  std::optional<int> GetEffectState(VcEffectId effect_id) override { return 0; }
  void OnEffectControlActivated(VcEffectId effect_id,
                                std::optional<int> state) override {}
};

crosapi::mojom::VideoConferenceMediaAppInfoPtr CreateFakeMediaApp(
    const crosapi::mojom::VideoConferenceAppType app_type) {
  return crosapi::mojom::VideoConferenceMediaAppInfo::New(
      base::UnguessableToken::Create(),
      /*last_activity_time=*/base::Time::Now(), /*is_capturing_camera=*/true,
      /*is_capturing_microphone=*/true, /*is_capturing_screen=*/false,
      u"Test App Name",
      /*url=*/GURL(), app_type);
}

}  // namespace

class BubbleViewTest : public AshTestBase {
 public:
  BubbleViewTest() = default;
  BubbleViewTest(const BubbleViewTest&) = delete;
  BubbleViewTest& operator=(const BubbleViewTest&) = delete;
  ~BubbleViewTest() override = default;

  // AshTestBase:
  void SetUp() override {
    scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
    scoped_feature_list_->InitAndEnableFeature(
        features::kFeatureManagementVideoConference);

    // Instantiates a fake controller (the real one is created in
    // `ChromeBrowserMainExtraPartsAsh::PreProfileInit()` which is not called in
    // ash unit tests).
    controller_ = std::make_unique<FakeVideoConferenceTrayController>();

    // Instantiate these fake effects, to be registered/unregistered as needed.
    office_bunny_ =
        std::make_unique<fake_video_conference::OfficeBunnyEffect>();
    shaggy_fur_ = std::make_unique<fake_video_conference::ShaggyFurEffect>();
    super_cuteness_ =
        std::make_unique<fake_video_conference::SuperCutnessEffect>();

    AshTestBase::SetUp();

    // Make the video conference tray visible for testing.
    video_conference_tray()->SetVisiblePreferred(true);

    // For historical reason, all BubbleViewTest tests are written with the
    // assumption that CameraEffectsController is not registered to the
    // EffectsManager by default. It is not the case anymore since we removed
    // the old Flags. The fix for that is easy: we just need to manually
    // unregister CameraEffectsController in these tests.
    controller()->GetEffectsManager().UnregisterDelegate(
        Shell::Get()->camera_effects_controller());
  }

  void TearDown() override {
    AshTestBase::TearDown();
    office_bunny_.reset();
    shaggy_fur_.reset();
    super_cuteness_.reset();
    controller_.reset();
    scoped_feature_list_.reset();
  }

  VideoConferenceTray* video_conference_tray() {
    return StatusAreaWidgetTestHelper::GetStatusAreaWidget()
        ->video_conference_tray();
  }

  IconButton* toggle_bubble_button() {
    return video_conference_tray()->toggle_bubble_button_;
  }

  views::View* bubble_view() {
    return video_conference_tray()->GetBubbleView();
  }

  FakeVideoConferenceTrayController* controller() { return controller_.get(); }

  views::View* toggle_effects_view() {
    return bubble_view()->GetViewByID(
        video_conference::BubbleViewID::kToggleEffectsView);
  }

  views::View* set_value_effects_view() {
    return bubble_view()->GetViewByID(
        video_conference::BubbleViewID::kSetValueEffectsView);
  }

  views::View* single_set_value_effect_view() {
    return bubble_view()->GetViewByID(
        video_conference::BubbleViewID::kSingleSetValueEffectView);
  }

  views::View* return_to_app() {
    return bubble_view()->GetViewByID(
        video_conference::BubbleViewID::kReturnToApp);
  }

  views::View* toggle_effect_button() {
    return bubble_view()->GetViewByID(
        video_conference::BubbleViewID::kToggleEffectsButton);
  }

  views::View* linux_app_warning_view() {
    return bubble_view()->GetViewByID(
        video_conference::BubbleViewID::kLinuxAppWarningView);
  }

  ash::fake_video_conference::OfficeBunnyEffect* office_bunny() {
    return office_bunny_.get();
  }

  ash::fake_video_conference::ShaggyFurEffect* shaggy_fur() {
    return shaggy_fur_.get();
  }

  ash::fake_video_conference::SuperCutnessEffect* super_cuteness() {
    return super_cuteness_.get();
  }

 private:
  std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_;
  std::unique_ptr<FakeVideoConferenceTrayController> controller_;
  std::unique_ptr<ash::fake_video_conference::OfficeBunnyEffect> office_bunny_;
  std::unique_ptr<ash::fake_video_conference::ShaggyFurEffect> shaggy_fur_;
  std::unique_ptr<ash::fake_video_conference::SuperCutnessEffect>
      super_cuteness_;
};

// TODO(b/273604669): Move tests that are only related to `toggle_effects_view`
// to its own unit test file.
TEST_F(BubbleViewTest, NoEffects) {
  EXPECT_FALSE(bubble_view());

  // Clicking the toggle button should construct and open up the bubble.
  LeftClickOn(toggle_bubble_button());
  EXPECT_TRUE(bubble_view());
  EXPECT_TRUE(bubble_view()->GetVisible());

  // "Return to app" is present and visible.
  EXPECT_TRUE(return_to_app());
  EXPECT_TRUE(return_to_app()->GetVisible());

  // No effects added, no effects view(s) present.
  EXPECT_FALSE(toggle_effects_view());

  // Click the toggle button, bubble is taken down.
  LeftClickOn(toggle_bubble_button());
  EXPECT_FALSE(bubble_view());
}

TEST_F(BubbleViewTest, RegisterToggleEffect) {
  // Open up the bubble, no toggle effects present.
  LeftClickOn(toggle_bubble_button());
  EXPECT_FALSE(toggle_effects_view());

  // Close the bubble.
  LeftClickOn(toggle_bubble_button());

  // Add one toggle effect.
  controller()->GetEffectsManager().RegisterDelegate(office_bunny());

  // Open up the bubble, toggle effects container view is present/visible.
  LeftClickOn(toggle_bubble_button());
  EXPECT_TRUE(toggle_effects_view());
  EXPECT_TRUE(toggle_effects_view()->GetVisible());
}

TEST_F(BubbleViewTest, UnregisterToggleEffect) {
  // Add one toggle effect.
  controller()->GetEffectsManager().RegisterDelegate(office_bunny());

  // Open up the bubble, toggle effects are present/visible.
  LeftClickOn(toggle_bubble_button());
  EXPECT_TRUE(toggle_effects_view());
  EXPECT_TRUE(toggle_effects_view()->GetVisible());

  // Take down the bubble.
  LeftClickOn(toggle_bubble_button());

  // Remove the toggle effect.
  controller()->GetEffectsManager().UnregisterDelegate(office_bunny());

  // Open up the bubble again, no effects present.
  LeftClickOn(toggle_bubble_button());
  EXPECT_FALSE(toggle_effects_view());
}

TEST_F(BubbleViewTest, ToggleButtonClicked) {
  // Add one toggle effect.
  controller()->GetEffectsManager().RegisterDelegate(office_bunny());

  // Click to open the bubble, toggle effect button is present/visible.
  LeftClickOn(toggle_bubble_button());
  EXPECT_TRUE(toggle_effect_button());
  EXPECT_TRUE(toggle_effect_button()->GetVisible());

  // Toggle effect button has not yet been clicked.
  EXPECT_EQ(office_bunny()->num_activations_for_testing(), 0);

  // Click the toggle effect button, verify that the effect has been "activated"
  // once.
  LeftClickOn(toggle_effect_button());
  EXPECT_EQ(office_bunny()->num_activations_for_testing(), 1);
}

TEST_F(BubbleViewTest, RegisterSetValueEffect) {
  // Open up the bubble, no set-value effects present.
  LeftClickOn(toggle_bubble_button());
  EXPECT_FALSE(toggle_effects_view());

  // Close the bubble.
  LeftClickOn(toggle_bubble_button());

  // Add one set-value effect.
  controller()->GetEffectsManager().RegisterDelegate(shaggy_fur());

  // Open up the bubble, set-value effects container view is present/visible.
  LeftClickOn(toggle_bubble_button());
  EXPECT_TRUE(set_value_effects_view());
  EXPECT_TRUE(set_value_effects_view()->GetVisible());
}

TEST_F(BubbleViewTest, UnregisterSetValueEffect) {
  // Add one set-value effect.
  controller()->GetEffectsManager().RegisterDelegate(shaggy_fur());

  // Open up the bubble, set-value effects are present/visible.
  LeftClickOn(toggle_bubble_button());
  EXPECT_TRUE(set_value_effects_view());
  EXPECT_TRUE(set_value_effects_view()->GetVisible());

  // Take down the bubble.
  LeftClickOn(toggle_bubble_button());

  // Remove the set-value effect.
  controller()->GetEffectsManager().UnregisterDelegate(shaggy_fur());

  // Open up the bubble again, no effects present.
  LeftClickOn(toggle_bubble_button());
  EXPECT_FALSE(set_value_effects_view());
}

TEST_F(BubbleViewTest, SetValueButtonClicked) {
  // Verify that the delegate hosts a single effect which has at least two
  // values.
  EXPECT_EQ(shaggy_fur()->GetNumEffects(), 1);
  EXPECT_GE(
      shaggy_fur()->GetEffectById(VcEffectId::kTestEffect)->GetNumStates(), 2);

  // Add one set-value effect.
  controller()->GetEffectsManager().RegisterDelegate(shaggy_fur());

  // Ensures initial states are correct.
  EXPECT_EQ(shaggy_fur()->GetNumActivationsForTesting(/*state_value=*/0), 0);
  EXPECT_EQ(shaggy_fur()->GetNumActivationsForTesting(/*state_value=*/1), 0);

  // Click to open the bubble. There should be 1 view in the set-value effects
  // view, which belongs to the added test effect.
  LeftClickOn(toggle_bubble_button());

  EXPECT_EQ(1u, set_value_effects_view()->children().size());

  auto* shaggy_fur_slider =
      static_cast<video_conference::SetValueEffectSlider*>(
          set_value_effects_view()->children()[0]);
  EXPECT_EQ(VcEffectId::kTestEffect, shaggy_fur_slider->effect_id());

  auto* tab_slider = shaggy_fur_slider->tab_slider();

  // Click the effect value 1 button, verify that the value has been "activated"
  // once.
  LeftClickOn(tab_slider->GetButtonAtIndex(1));
  EXPECT_EQ(shaggy_fur()->GetNumActivationsForTesting(/*state_value=*/1), 1);

  // Click the effect value 0 button, verify that value 0 has been "activated"
  // once, and confirm that value 1 has still only been activated once i.e. we
  // just activated value 0 and not value 1.
  LeftClickOn(tab_slider->GetButtonAtIndex(0));
  EXPECT_EQ(shaggy_fur()->GetNumActivationsForTesting(/*state_value=*/0), 1);
  EXPECT_EQ(shaggy_fur()->GetNumActivationsForTesting(/*state_value=*/1), 1);
}

TEST_F(BubbleViewTest, ValidEffectState) {
  // Verify that the delegate hosts a single effect which has at least two
  // values.
  EXPECT_EQ(super_cuteness()->GetNumEffects(), 1);
  EXPECT_GE(
      super_cuteness()->GetEffectById(VcEffectId::kTestEffect)->GetNumStates(),
      2);

  // Add one set-value effect.
  controller()->GetEffectsManager().RegisterDelegate(super_cuteness());

  // Effect will NOT return an invalid state.
  super_cuteness()->set_has_invalid_effect_state_for_testing(false);

  // Click to open the bubble, a single set-value effect view is
  // present/visible.
  LeftClickOn(toggle_bubble_button());
  views::View* effect_view = single_set_value_effect_view();
  EXPECT_TRUE(effect_view);
  EXPECT_TRUE(effect_view->GetVisible());
}

TEST_F(BubbleViewTest, InvalidEffectState) {
  // Verify that the delegate hosts a single effect which has at least two
  // values.
  EXPECT_EQ(super_cuteness()->GetNumEffects(), 1);
  EXPECT_GE(
      super_cuteness()->GetEffectById(VcEffectId::kTestEffect)->GetNumStates(),
      2);

  // Add one set-value effect.
  controller()->GetEffectsManager().RegisterDelegate(super_cuteness());

  // Effect WILL return an invalid state.
  super_cuteness()->set_has_invalid_effect_state_for_testing(true);

  // Click to open the bubble, a single set-value effect view is NOT present.
  LeftClickOn(toggle_bubble_button());
  EXPECT_FALSE(single_set_value_effect_view());
}

TEST_F(BubbleViewTest, LinuxAppWarningView) {
  controller()->ClearMediaApps();
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*app_type=*/crosapi::mojom::VideoConferenceAppType::kChromeApp));

  // Click to open the bubble, the linux app warning view is NOT present.
  LeftClickOn(toggle_bubble_button());
  EXPECT_FALSE(linux_app_warning_view());

  // Close the bubble.
  LeftClickOn(toggle_bubble_button());

  controller()->AddMediaApp(CreateFakeMediaApp(
      /*app_type=*/crosapi::mojom::VideoConferenceAppType::kCrostiniVm));

  // When there's a linux app alongside a non-linux app, the linux app warning
  // view is present only when there's effect(s) available.
  LeftClickOn(toggle_bubble_button());
  EXPECT_FALSE(linux_app_warning_view());

  LeftClickOn(toggle_bubble_button());

  controller()->GetEffectsManager().RegisterDelegate(office_bunny());
  LeftClickOn(toggle_bubble_button());
  ASSERT_TRUE(linux_app_warning_view());
  EXPECT_TRUE(linux_app_warning_view()->GetVisible());
}

class DLCBubbleViewTest : public BubbleViewTest {
 public:
  DLCBubbleViewTest() = default;
  DLCBubbleViewTest(const DLCBubbleViewTest&) = delete;
  DLCBubbleViewTest& operator=(const DLCBubbleViewTest&) = delete;
  ~DLCBubbleViewTest() override = default;

  // AshTestBase:
  void SetUp() override {
    scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
    scoped_feature_list_->InitAndEnableFeature(features::kVcDlcUi);
    BubbleViewTest::SetUp();
  }

  void TearDown() override {
    // Unregister toggle effects that were registered to ensure the
    // `VcTileUiControllers` are reset like they are in the production version.
    controller()->GetEffectsManager().UnregisterDelegate(office_bunny());
    BubbleViewTest::TearDown();
    scoped_feature_list_.reset();
  }

 private:
  std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_;
};

// Tests that an initialized BubbleView with no errors shows no warning label.
TEST_F(DLCBubbleViewTest, NoError) {
  LeftClickOn(toggle_bubble_button());
  ASSERT_TRUE(bubble_view());
  ASSERT_TRUE(bubble_view()->GetVisible());

  // The error view is not added with no toggle effects.
  EXPECT_FALSE(bubble_view()->GetViewByID(kDLCDownloadsInErrorView));

  LeftClickOn(toggle_bubble_button());

  // Add one toggle effect and reshow, the error view should be present but not
  // visible.
  controller()->GetEffectsManager().RegisterDelegate(office_bunny());
  LeftClickOn(toggle_bubble_button());

  auto* error_view = bubble_view()->GetViewByID(kDLCDownloadsInErrorView);
  EXPECT_TRUE(error_view);
  EXPECT_FALSE(error_view->GetVisible());
}

// Tests adding and removing one error.
TEST_F(DLCBubbleViewTest, OneError) {
  controller()->GetEffectsManager().RegisterDelegate(office_bunny());
  LeftClickOn(toggle_bubble_button());

  BubbleView* bubble_view_ptr = static_cast<BubbleView*>(bubble_view());
  // Pass one error to the bubble view, it should get a warning label.
  std::u16string test_feature_name = u"test_feature_name";
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/true,
                                             test_feature_name);
  auto* error_container_view =
      bubble_view()->GetViewByID(kDLCDownloadsInErrorView);
  auto* dlc_error_label = static_cast<views::Label*>(
      error_container_view->GetViewByID(BubbleViewID::kWarningViewLabel));
  EXPECT_TRUE(error_container_view);
  EXPECT_TRUE(dlc_error_label);

  const std::u16string label_contents = dlc_error_label->GetText();
  // Try to add a second error for `test_feature_name`, there should be no
  // change.
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/true,
                                             test_feature_name);

  EXPECT_EQ(label_contents, dlc_error_label->GetText());

  // Remove the error, there should be a change.
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/false,
                                             test_feature_name);

  EXPECT_EQ(std::u16string(), dlc_error_label->GetText());
  EXPECT_FALSE(error_container_view->GetVisible());
}

// Tests adding/removing two+ errors.
TEST_F(DLCBubbleViewTest, TwoPlusErrors) {
  controller()->GetEffectsManager().RegisterDelegate(office_bunny());
  LeftClickOn(toggle_bubble_button());

  BubbleView* bubble_view_ptr = static_cast<BubbleView*>(bubble_view());
  // Pass one error to the bubble view, it should get a warning label.
  std::u16string test_feature_name_1 = u"test_feature_name_1";
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/true,
                                             test_feature_name_1);
  auto* error_container_view =
      bubble_view()->GetViewByID(kDLCDownloadsInErrorView);
  auto* dlc_error_label = static_cast<views::Label*>(
      error_container_view->GetViewByID(BubbleViewID::kWarningViewLabel));
  EXPECT_TRUE(error_container_view);
  EXPECT_TRUE(dlc_error_label);

  const std::u16string one_error_label = dlc_error_label->GetText();

  // Add a second error for `test_feature_name_2`.
  std::u16string test_feature_name_2 = u"test_feature_name_2";
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/true,
                                             test_feature_name_2);
  const std::u16string two_error_label = dlc_error_label->GetText();

  EXPECT_NE(one_error_label, two_error_label);

  // Add a third error, there should be no effect as only two are supported.
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/true,
                                             u"feature_name_3");
  EXPECT_EQ(dlc_error_label->GetText(), two_error_label);

  // Remove the error, the label should return to the first one.
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/false,
                                             test_feature_name_2);

  EXPECT_EQ(one_error_label, dlc_error_label->GetText());

  // Remove the first error, the label should be empty.
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/false,
                                             test_feature_name_1);

  EXPECT_EQ(std::u16string(), dlc_error_label->GetText());
  EXPECT_FALSE(dlc_error_label->GetVisible());
}

// Tests removing errors that are not there does not crash.
TEST_F(DLCBubbleViewTest, ErrorsRemoved) {
  controller()->GetEffectsManager().RegisterDelegate(office_bunny());
  LeftClickOn(toggle_bubble_button());

  // Try to remove errors that don't exist.
  BubbleView* bubble_view_ptr = static_cast<BubbleView*>(bubble_view());
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/false,
                                             u"one");
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/false,
                                             u"two");
  bubble_view_ptr->OnDLCDownloadStateInError(/*add_warning_view=*/false,
                                             u"three");

  auto* error_container_view =
      bubble_view()->GetViewByID(kDLCDownloadsInErrorView);
  auto* dlc_error_label = static_cast<views::Label*>(
      error_container_view->GetViewByID(BubbleViewID::kWarningViewLabel));

  EXPECT_FALSE(error_container_view->GetVisible());
  EXPECT_EQ(dlc_error_label->GetText(), std::u16string());
}

// The four `bool` params are as follows, if 'true':
//    0 - The test effects depend on the camera being enabled.
//    1 - The test effects depend on the microphone being enabled.
//    2 - The camera is enabled.
//    3 - The microphone is enabled.
//    4 - The camera has granted permission to running media app(s).
//    5 - The microphone has granted permission to running media app(s).
class ResourceDependencyTest
    : public AshTestBase,
      public testing::WithParamInterface<
          std::tuple<bool, bool, bool, bool, bool, bool>> {
 public:
  ResourceDependencyTest() = default;
  ResourceDependencyTest(const ResourceDependencyTest&) = delete;
  ResourceDependencyTest& operator=(const ResourceDependencyTest&) = delete;
  ~ResourceDependencyTest() override = default;

  // AshTestBase:
  void SetUp() override {
    scoped_feature_list_.InitAndEnableFeature(
        features::kFeatureManagementVideoConference);

    // Here we have to create the global instance of `CrasAudioHandler` before
    // `FakeVideoConferenceTrayController`, so we do it here and not do it in
    // `AshTestBase`.
    CrasAudioClient::InitializeFake();
    CrasAudioHandler::InitializeForTesting();

    // Instantiates a fake controller (the real one is created in
    // ChromeBrowserMainExtraPartsAsh::PreProfileInit() which is not called in
    // ash unit tests).
    controller_ = std::make_unique<FakeVideoConferenceTrayController>();

    set_create_global_cras_audio_handler(false);
    AshTestBase::SetUp();

    // Make the video conference tray visible for testing.
    video_conference_tray()->SetVisiblePreferred(true);

    // The test is executed with each possible combination of these.
    has_camera_dependency_ = std::get<0>(GetParam());
    has_microphone_dependency_ = std::get<1>(GetParam());
    camera_enabled_ = std::get<2>(GetParam());
    microphone_enabled_ = std::get<3>(GetParam());
    has_camera_permission_ = std::get<4>(GetParam());
    has_microphone_permission_ = std::get<5>(GetParam());
  }

  void TearDown() override {
    AshTestBase::TearDown();
    toggle_effect_.reset();
    set_value_effect_.reset();
    controller_.reset();
    CrasAudioHandler::Shutdown();
    CrasAudioClient::Shutdown();
  }

  void CreateTestEffects(
      VcHostedEffect::ResourceDependencyFlags dependency_flags) {
    toggle_effect_ = std::make_unique<SquareCinnamonCereal>(dependency_flags);
    controller()->GetEffectsManager().RegisterDelegate(toggle_effect_.get());
    set_value_effect_ = std::make_unique<SnackNationForever>(dependency_flags);
    controller()->GetEffectsManager().RegisterDelegate(set_value_effect_.get());
  }

  VideoConferenceTray* video_conference_tray() {
    return StatusAreaWidgetTestHelper::GetStatusAreaWidget()
        ->video_conference_tray();
  }

  IconButton* toggle_bubble_button() {
    return video_conference_tray()->toggle_bubble_button_;
  }

  views::View* bubble_view() {
    return video_conference_tray()->GetBubbleView();
  }

  FakeVideoConferenceTrayController* controller() { return controller_.get(); }

  views::View* effects_view() {
    return bubble_view()->GetViewByID(
        video_conference::BubbleViewID::kToggleEffectsView);
  }

  SquareCinnamonCereal* toggle_effect() { return toggle_effect_.get(); }

  SnackNationForever* set_value_effect() { return set_value_effect_.get(); }

  views::View* toggle_effect_container_view() {
    return bubble_view()->GetViewByID(kSquareCinnamonCerealViewId);
  }

  views::View* set_value_effect_container_view() {
    return bubble_view()->GetViewByID(kSnackNationForeverViewId);
  }

  bool has_camera_dependency() const { return has_camera_dependency_; }
  bool has_microphone_dependency() const { return has_microphone_dependency_; }
  bool camera_enabled() const { return camera_enabled_; }
  bool microphone_enabled() const { return microphone_enabled_; }
  bool has_camera_permission() const { return has_camera_permission_; }
  bool has_microphone_permission() const { return has_microphone_permission_; }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<FakeVideoConferenceTrayController> controller_;
  std::unique_ptr<SquareCinnamonCereal> toggle_effect_;
  std::unique_ptr<SnackNationForever> set_value_effect_;

  bool has_camera_dependency_ = false;
  bool has_microphone_dependency_ = false;
  bool camera_enabled_ = false;
  bool microphone_enabled_ = false;
  bool has_camera_permission_ = false;
  bool has_microphone_permission_ = false;
};

INSTANTIATE_TEST_SUITE_P(BubbleViewResourceDependency,
                         ResourceDependencyTest,
                         testing::Combine(testing::Bool(),
                                          testing::Bool(),
                                          testing::Bool(),
                                          testing::Bool(),
                                          testing::Bool(),
                                          testing::Bool()));

TEST_P(ResourceDependencyTest, ResourceDependency) {
  // Initialize with no dependencies.
  VcHostedEffect::ResourceDependencyFlags flags =
      VcHostedEffect::ResourceDependency::kNone;

  // Set camera dependency.
  if (has_camera_dependency()) {
    flags |= VcHostedEffect::ResourceDependency::kCamera;
  }

  // Set microphone dependency.
  if (has_microphone_dependency()) {
    flags |= VcHostedEffect::ResourceDependency::kMicrophone;
  }

  // Instantiate/register the fake effects.
  CreateTestEffects(flags);

  // Camera is enabled.
  controller()->SetCameraMuted(!camera_enabled());

  // Microphone is enabled.
  controller()->SetMicrophoneMuted(!microphone_enabled());

  VideoConferenceMediaState state;
  state.has_camera_permission = has_camera_permission();
  state.has_microphone_permission = has_microphone_permission();
  controller()->UpdateWithMediaState(state);

  // Click to open the bubble, bubble is present/visible.
  LeftClickOn(toggle_bubble_button());
  EXPECT_TRUE(bubble_view());
  EXPECT_TRUE(bubble_view()->GetVisible());

  // Effect container view is present/visible if its dependencies are
  // satisfied. A dependency on a resource is considered "satfisfied" if (1)
  // there is no dependency on the resource or (2) there is a dependency on the
  // resource, the resource is enabled, and there's at least one running media
  // app(s) has been granted permission to use the resource.
  const bool camera_satisfied =
      !has_camera_dependency() ||
      (has_camera_dependency() && has_camera_permission());
  const bool microphone_satisfied =
      !has_microphone_dependency() ||
      (has_microphone_dependency() && has_microphone_permission());

  if (camera_satisfied && microphone_satisfied) {
    EXPECT_TRUE(toggle_effect_container_view());
    EXPECT_TRUE(toggle_effect_container_view()->GetVisible());
    EXPECT_TRUE(set_value_effect_container_view());
    EXPECT_TRUE(set_value_effect_container_view()->GetVisible());
  } else {
    EXPECT_FALSE(toggle_effect_container_view());
    EXPECT_FALSE(set_value_effect_container_view());
  }
}

}  // namespace ash::video_conference