chromium/ash/system/video_conference/bubble/return_to_app_panel_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/return_to_app_panel.h"

#include <memory>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/fake_video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_tray.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chromeos/crosapi/mojom/video_conference.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/test/layer_animation_stopped_waiter.h"
#include "ui/compositor/test/test_utils.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"

namespace {

crosapi::mojom::VideoConferenceMediaAppInfoPtr CreateFakeMediaApp(
    bool is_capturing_camera,
    bool is_capturing_microphone,
    bool is_capturing_screen,
    const std::u16string& title,
    std::string url,
    const crosapi::mojom::VideoConferenceAppType app_type =
        crosapi::mojom::VideoConferenceAppType::kChromeTab,
    const base::UnguessableToken& id = base::UnguessableToken::Create()) {
  return crosapi::mojom::VideoConferenceMediaAppInfo::New(
      id,
      /*last_activity_time=*/base::Time::Now(), is_capturing_camera,
      is_capturing_microphone, is_capturing_screen, title,
      /*url=*/GURL(url), app_type);
}

// Verifies the information of `ReturnToAppButton`.
void VerifyReturnToAppButtonInfo(
    ash::video_conference::ReturnToAppButton* button,
    bool is_capturing_camera,
    bool is_capturing_microphone,
    bool is_capturing_screen,
    const std::u16string& display_text) {
  EXPECT_EQ(is_capturing_camera, button->is_capturing_camera());
  EXPECT_EQ(is_capturing_microphone, button->is_capturing_microphone());
  EXPECT_EQ(is_capturing_screen, button->is_capturing_screen());
  EXPECT_EQ(display_text, button->label()->GetText());
}

// Used for verifying displayed url.
const std::string kMeetTestUrl = "https://meet.google.com/abc-xyz/ab-123";
const std::u16string kExpectedMeetDisplayedUrl =
    u"meet.google.com/abc-xyz/ab-123";

}  // namespace

namespace ash::video_conference {

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

  // AshTestBase:
  void SetUp() override {
    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>();

    AshTestBase::SetUp();

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

  void TearDown() override {
    AshTestBase::TearDown();
    controller_.reset();
  }

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

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

  // Get the `ReturnToAppPanel` from the test `StatusAreaWidget`.
  ReturnToAppPanel* GetReturnToAppPanel() {
    return static_cast<ReturnToAppPanel*>(
        video_conference_tray()->GetBubbleView()->GetViewByID(
            BubbleViewID::kReturnToApp));
  }

  ReturnToAppPanel::ReturnToAppContainer* GetReturnToAppContainer(
      ReturnToAppPanel* panel) {
    return panel->container_view_;
  }

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

  // Get the instance that handle bounds change for the expand/collapse
  // animation.
  gfx::LinearAnimation* GetBoundsChangeAnimation() {
    return GetReturnToAppContainer(GetReturnToAppPanel())->animation_.get();
  }

  void AnimateToValue(double animation_value) {
    auto* animation = GetBoundsChangeAnimation();
    EXPECT_TRUE(animation->is_animating());
    animation->SetCurrentValue(animation_value);
    GetReturnToAppContainer(GetReturnToAppPanel())
        ->AnimationProgressed(animation);
  }

  // Wait until the bounds change animation is completed.
  void WaitForAnimation() {
    do {
      base::RunLoop().RunUntilIdle();
    } while (GetBoundsChangeAnimation()->is_animating());
  }

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

TEST_F(ReturnToAppPanelTest, NoApp) {
  MediaApps apps;

  // The view should not be visible when there's no app.
  auto return_to_app_panel = std::make_unique<ReturnToAppPanel>(apps);
  EXPECT_FALSE(return_to_app_panel->GetVisible());
}

TEST_F(ReturnToAppPanelTest, OneApp) {
  bool is_capturing_camera = true;
  bool is_capturing_microphone = false;
  bool is_capturing_screen = false;
  auto* title = u"Meet";

  MediaApps apps;
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, title,
      /*url=*/kMeetTestUrl));

  // There should be one child representing the only one running media app.
  auto panel = std::make_unique<ReturnToAppPanel>(apps);
  auto* return_to_app_container = GetReturnToAppContainer(panel.get());

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

  auto* app_button = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());
  EXPECT_FALSE(app_button->expand_indicator_for_testing()->GetVisible());
  VerifyReturnToAppButtonInfo(app_button, is_capturing_camera,
                              is_capturing_microphone, is_capturing_screen,
                              /*display_text=*/title);
}

TEST_F(ReturnToAppPanelTest, MultipleApps) {
  auto* title = u"Meet";

  MediaApps apps;
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, title,
      /*url=*/kMeetTestUrl));
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"",
      /*url=*/kMeetTestUrl));

  // There should be three children, one representing the summary row and two
  // for two running media apps.
  auto panel = std::make_unique<ReturnToAppPanel>(apps);
  auto* return_to_app_container = GetReturnToAppContainer(panel.get());
  EXPECT_EQ(3u, return_to_app_container->children().size());

  // The first row should be the summary row, representing the state of
  // capturing from all apps and showing that 2 apps are running.
  auto* summary_row = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());
  VerifyReturnToAppButtonInfo(
      summary_row, /*is_capturing_camera=*/true,
      /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true,
      l10n_util::GetStringFUTF16Int(
          IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SUMMARY_TEXT, 2));

  // Verify the next 2 rows, representing the 2 running apps.
  auto* first_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[1]);
  VerifyReturnToAppButtonInfo(first_app_row, /*is_capturing_camera=*/true,
                              /*is_capturing_microphone=*/false,
                              /*is_capturing_screen=*/false,
                              /*display_text=*/title);

  // If the title is empty, the button should display the app url.
  auto* second_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[2]);
  VerifyReturnToAppButtonInfo(second_app_row, /*is_capturing_camera=*/false,
                              /*is_capturing_microphone=*/true,
                              /*is_capturing_screen=*/true,
                              /*display_text=*/kExpectedMeetDisplayedUrl);
}

TEST_F(ReturnToAppPanelTest, ExpandCollapse) {
  MediaApps apps;
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));

  auto panel = std::make_unique<ReturnToAppPanel>(apps);
  auto* return_to_app_container = GetReturnToAppContainer(panel.get());
  auto* summary_row = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());
  EXPECT_TRUE(summary_row->expand_indicator_for_testing()->GetVisible());

  auto* first_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[1]);
  auto* second_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[2]);

  // The panel should be collapsed by default.
  EXPECT_FALSE(summary_row->expanded());

  // Verify the views in collapsed state:
  EXPECT_TRUE(summary_row->icons_container()->GetVisible());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SHOW_TOOLTIP),
            summary_row->expand_indicator_for_testing()->GetTooltipText());
  EXPECT_FALSE(first_app_row->GetVisible());
  EXPECT_FALSE(second_app_row->GetVisible());

  // Clicking the summary row should expand the panel.
  summary_row->OnButtonClicked(
      /*id=*/base::UnguessableToken::Null(),
      /*app_type=*/crosapi::mojom::VideoConferenceAppType::kDefaultValue);
  EXPECT_TRUE(summary_row->expanded());

  // Verify the views in expanded state:
  EXPECT_FALSE(summary_row->icons_container()->GetVisible());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_HIDE_TOOLTIP),
            summary_row->expand_indicator_for_testing()->GetTooltipText());
  EXPECT_TRUE(first_app_row->GetVisible());
  EXPECT_TRUE(second_app_row->GetVisible());

  // Click again. Should be in collapsed state.
  summary_row->OnButtonClicked(
      /*id=*/base::UnguessableToken::Null(),
      /*app_type=*/crosapi::mojom::VideoConferenceAppType::kDefaultValue);
  EXPECT_FALSE(summary_row->expanded());
}

TEST_F(ReturnToAppPanelTest, MaxCapturingCount) {
  // Test the panel's `max_capturing_count_` to make sure the buttons are
  // aligned correctly.
  MediaApps apps;
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));
  auto return_to_app_panel = std::make_unique<ReturnToAppPanel>(apps);
  EXPECT_EQ(1, return_to_app_panel->max_capturing_count());

  apps.clear();
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));
  return_to_app_panel = std::make_unique<ReturnToAppPanel>(apps);
  EXPECT_EQ(2, return_to_app_panel->max_capturing_count());

  apps.clear();
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  apps.emplace_back(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));
  return_to_app_panel = std::make_unique<ReturnToAppPanel>(apps);
  EXPECT_EQ(3, return_to_app_panel->max_capturing_count());
}

TEST_F(ReturnToAppPanelTest, ReturnToApp) {
  ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
      ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);

  base::HistogramTester histogram_tester;

  auto app_id1 = base::UnguessableToken::Create();
  auto app_id2 = base::UnguessableToken::Create();

  controller()->ClearMediaApps();
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl,
      /*app_type=*/crosapi::mojom::VideoConferenceAppType::kChromeApp,
      /*id=*/app_id1));
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/"", /*app_type=*/crosapi::mojom::VideoConferenceAppType::kArcApp,
      /*id=*/app_id2));

  LeftClickOn(toggle_bubble_button());
  auto* return_to_app_panel = GetReturnToAppPanel();
  auto* return_to_app_container = GetReturnToAppContainer(return_to_app_panel);

  auto* summary_row = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());
  auto* first_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[1]);
  auto* second_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[2]);

  // Clicking on the summary row should not launch any apps (it switched the
  // panel to expanded state).
  LeftClickOn(summary_row);
  ASSERT_TRUE(summary_row->expanded());
  EXPECT_FALSE(controller()->app_to_launch_state_[app_id1]);
  EXPECT_FALSE(controller()->app_to_launch_state_[app_id2]);

  // Clicking each row should open the corresponding app.
  LeftClickOn(first_app_row);
  EXPECT_TRUE(controller()->app_to_launch_state_[app_id1]);
  EXPECT_FALSE(controller()->app_to_launch_state_[app_id2]);
  histogram_tester.ExpectBucketCount(
      "Ash.VideoConference.ReturnToApp.Click",
      crosapi::mojom::VideoConferenceAppType::kChromeApp, 1);
  histogram_tester.ExpectBucketCount(
      "Ash.VideoConference.ReturnToApp.Click",
      crosapi::mojom::VideoConferenceAppType::kArcApp, 0);

  LeftClickOn(second_app_row);
  EXPECT_TRUE(controller()->app_to_launch_state_[app_id2]);
  histogram_tester.ExpectBucketCount(
      "Ash.VideoConference.ReturnToApp.Click",
      crosapi::mojom::VideoConferenceAppType::kArcApp, 1);
}

TEST_F(ReturnToAppPanelTest, ExpandAnimation) {
  ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  controller()->ClearMediaApps();
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));

  LeftClickOn(toggle_bubble_button());

  auto* return_to_app_panel = GetReturnToAppPanel();
  auto* return_to_app_container = GetReturnToAppContainer(return_to_app_panel);
  auto* summary_row = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());
  ASSERT_FALSE(summary_row->expanded());

  auto panel_initial_height = return_to_app_panel->size().height();
  auto* vc_bubble = video_conference_tray()->GetBubbleView();
  auto bubble_initial_height = vc_bubble->size().height();

  // The animation should start after we click the summary row to expand the
  // panel.
  LeftClickOn(summary_row);
  EXPECT_TRUE(GetBoundsChangeAnimation()->is_animating());

  AnimateToValue(0.5);

  auto panel_mid_animation_height = return_to_app_panel->size().height();
  auto bubble_mid_animation_height = vc_bubble->size().height();

  // Make sure that the panel is expanding and the bubble is also expanding in
  // the same amount.
  EXPECT_GT(panel_mid_animation_height, panel_initial_height);
  EXPECT_EQ(panel_mid_animation_height - panel_initial_height,
            bubble_mid_animation_height - bubble_initial_height);

  // Test the same thing when animation ends.
  WaitForAnimation();

  auto panel_end_animation_height = return_to_app_panel->size().height();
  auto bubble_end_animation_height = vc_bubble->size().height();

  EXPECT_GT(panel_end_animation_height, panel_mid_animation_height);
  EXPECT_EQ(panel_end_animation_height - panel_mid_animation_height,
            bubble_end_animation_height - bubble_mid_animation_height);
}

TEST_F(ReturnToAppPanelTest, CollapseAnimation) {
  ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  controller()->ClearMediaApps();
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));

  LeftClickOn(toggle_bubble_button());

  auto* return_to_app_panel = GetReturnToAppPanel();
  auto* return_to_app_container = GetReturnToAppContainer(return_to_app_panel);
  auto* summary_row = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());

  LeftClickOn(summary_row);
  WaitForAnimation();

  ASSERT_TRUE(summary_row->expanded());

  auto panel_initial_height = return_to_app_panel->size().height();
  auto* vc_bubble = video_conference_tray()->GetBubbleView();
  auto bubble_initial_height = vc_bubble->size().height();

  // The animation should start after we click the summary row again to collapse
  // the panel.
  LeftClickOn(summary_row);
  EXPECT_TRUE(GetBoundsChangeAnimation()->is_animating());

  // Normally, a layer animation will be performed to fade out the return to app
  // buttons. However, since we are simulating different stage of the bounds
  // change animation, we will set visibility right away here to prevent the
  // layer animation from interfering with the bounds change animation
  // simulation.
  auto* first_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[1]);
  auto* second_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[2]);
  first_app_row->SetVisible(false);
  second_app_row->SetVisible(false);

  AnimateToValue(0.5);

  auto panel_mid_animation_height = return_to_app_panel->size().height();
  auto bubble_mid_animation_height = vc_bubble->size().height();

  // Make sure that the panel is collapsing and the bubble is also collapsing in
  // the same amount.
  EXPECT_LT(panel_mid_animation_height, panel_initial_height);
  EXPECT_EQ(panel_mid_animation_height - panel_initial_height,
            bubble_mid_animation_height - bubble_initial_height);

  // Test the same thing when animation ends.
  WaitForAnimation();

  auto panel_end_animation_height = return_to_app_panel->size().height();
  auto bubble_end_animation_height = vc_bubble->size().height();

  EXPECT_LT(panel_end_animation_height, panel_mid_animation_height);
  EXPECT_EQ(panel_end_animation_height - panel_mid_animation_height,
            bubble_end_animation_height - bubble_mid_animation_height);
}

// Verify that the layer animations to show/hide the view are performed with
// the expected visibility and opacity before and after the animation.
TEST_F(ReturnToAppPanelTest, LayerAnimations) {
  ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  controller()->ClearMediaApps();
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));

  LeftClickOn(toggle_bubble_button());

  auto* return_to_app_panel = GetReturnToAppPanel();
  auto* return_to_app_container = GetReturnToAppContainer(return_to_app_panel);
  auto* summary_row = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());

  // Expand animation: The return to app buttons should fade in.
  LeftClickOn(summary_row);

  auto* first_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[1]);
  auto* second_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[2]);

  EXPECT_EQ(0, first_app_row->layer()->opacity());
  EXPECT_EQ(0, second_app_row->layer()->opacity());

  ui::LayerAnimationStoppedWaiter layer_animation_waiter;
  layer_animation_waiter.Wait(first_app_row->layer());
  layer_animation_waiter.Wait(second_app_row->layer());

  EXPECT_EQ(1, first_app_row->layer()->opacity());
  EXPECT_EQ(1, second_app_row->layer()->opacity());

  // End the rest of the animation to test collapse animation.
  WaitForAnimation();
  ASSERT_TRUE(summary_row->expanded());

  // Collapse animation: The return to app buttons should fade out and the
  // summary icons should fade in.
  LeftClickOn(summary_row);
  EXPECT_TRUE(GetBoundsChangeAnimation()->is_animating());

  auto* summary_icons = summary_row->icons_container();
  EXPECT_EQ(0, summary_icons->layer()->opacity());

  EXPECT_TRUE(first_app_row->GetVisible());
  EXPECT_TRUE(second_app_row->GetVisible());

  layer_animation_waiter.Wait(summary_icons->layer());
  layer_animation_waiter.Wait(first_app_row->layer());
  layer_animation_waiter.Wait(second_app_row->layer());

  EXPECT_EQ(1, summary_icons->layer()->opacity());
  EXPECT_FALSE(first_app_row->GetVisible());
  EXPECT_FALSE(second_app_row->GetVisible());
}

TEST_F(ReturnToAppPanelTest, ReturnToAppButtonTextElide) {
  controller()->ClearMediaApps();
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false,
      /*title=*/
      u"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
      u"eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      /*url=*/""));

  LeftClickOn(toggle_bubble_button());

  auto* return_to_app_container =
      GetReturnToAppContainer(GetReturnToAppPanel());
  auto* app_button = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());
  auto* app_button_label = app_button->label();

  // With a long title, the app title should still fit inside the button (the
  // width of the label should still be smaller).
  EXPECT_LT(app_button_label->width(), app_button->width());

  const char16_t kEllipsisString[] = {0x2026, 0};

  // The display text should end with the ellipsis.
  EXPECT_TRUE(base::EndsWith(app_button_label->GetDisplayTextForTesting(),
                             kEllipsisString));
}

TEST_F(ReturnToAppPanelTest, ReturnToAppButtonAccessibleName) {
  controller()->ClearMediaApps();
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));

  LeftClickOn(toggle_bubble_button());
  auto* return_to_app_panel = GetReturnToAppPanel();
  auto* return_to_app_container = GetReturnToAppContainer(return_to_app_panel);

  auto* first_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[1]);
  auto* second_app_row =
      static_cast<ReturnToAppButton*>(return_to_app_container->children()[2]);

  auto expected_camera_text = l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_RETURN_TO_APP_PERIPHERALS_ACCESSIBLE_NAME,
      l10n_util::GetStringUTF16(VIDEO_CONFERENCE_TOGGLE_BUTTON_TYPE_CAMERA));
  auto expected_microphone_text = l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_RETURN_TO_APP_PERIPHERALS_ACCESSIBLE_NAME,
      l10n_util::GetStringUTF16(
          VIDEO_CONFERENCE_TOGGLE_BUTTON_TYPE_MICROPHONE));
  auto expected_screen_share_text = l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_RETURN_TO_APP_PERIPHERALS_ACCESSIBLE_NAME,
      l10n_util::GetStringUTF16(
          VIDEO_CONFERENCE_TOGGLE_BUTTON_TYPE_SCREEN_SHARE));

  // Verify accessible name for each row.
  EXPECT_EQ(expected_camera_text + u"Meet",
            first_app_row->GetViewAccessibility().GetCachedName());
  EXPECT_EQ(expected_microphone_text + expected_screen_share_text + u"Zoom",
            second_app_row->GetViewAccessibility().GetCachedName());
}

TEST_F(ReturnToAppPanelTest, ReturnToAppButtonSummaryRowAccessibleName) {
  controller()->ClearMediaApps();
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
      /*is_capturing_screen=*/false, /*title=*/u"Meet",
      /*url=*/kMeetTestUrl));
  controller()->AddMediaApp(CreateFakeMediaApp(
      /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
      /*is_capturing_screen=*/true, /*title=*/u"Zoom",
      /*url=*/""));

  LeftClickOn(toggle_bubble_button());
  auto* return_to_app_panel = GetReturnToAppPanel();
  auto* return_to_app_container = GetReturnToAppContainer(return_to_app_panel);

  auto* summary_row = static_cast<ReturnToAppButton*>(
      return_to_app_container->children().front());

  auto expected_camera_text = l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_RETURN_TO_APP_PERIPHERALS_ACCESSIBLE_NAME,
      l10n_util::GetStringUTF16(VIDEO_CONFERENCE_TOGGLE_BUTTON_TYPE_CAMERA));
  auto expected_microphone_text = l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_RETURN_TO_APP_PERIPHERALS_ACCESSIBLE_NAME,
      l10n_util::GetStringUTF16(
          VIDEO_CONFERENCE_TOGGLE_BUTTON_TYPE_MICROPHONE));
  auto expected_screen_share_text = l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_RETURN_TO_APP_PERIPHERALS_ACCESSIBLE_NAME,
      l10n_util::GetStringUTF16(
          VIDEO_CONFERENCE_TOGGLE_BUTTON_TYPE_SCREEN_SHARE));
  auto expected_button_text =
      expected_camera_text + expected_microphone_text +
      expected_screen_share_text +
      l10n_util::GetStringFUTF16Int(
          IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SUMMARY_TEXT, 2);

  EXPECT_EQ(expected_button_text +
                l10n_util::GetStringUTF16(
                    VIDEO_CONFERENCE_RETURN_TO_APP_COLLAPSED_ACCESSIBLE_NAME),
            summary_row->GetViewAccessibility().GetCachedName());

  LeftClickOn(summary_row);

  EXPECT_EQ(expected_button_text +
                l10n_util::GetStringUTF16(
                    VIDEO_CONFERENCE_RETURN_TO_APP_EXPANDED_ACCESSIBLE_NAME),
            summary_row->GetViewAccessibility().GetCachedName());

  LeftClickOn(summary_row);
  EXPECT_EQ(expected_button_text +
                l10n_util::GetStringUTF16(
                    VIDEO_CONFERENCE_RETURN_TO_APP_COLLAPSED_ACCESSIBLE_NAME),
            summary_row->GetViewAccessibility().GetCachedName());
}

}  // namespace ash::video_conference