chromium/ash/capture_mode/gif_recording_unittests.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/capture_mode/capture_button_view.h"
#include "ash/capture_mode/capture_label_view.h"
#include "ash/capture_mode/capture_mode_bar_view.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_metrics.h"
#include "ash/capture_mode/capture_mode_session_test_api.h"
#include "ash/capture_mode/capture_mode_test_util.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/recording_type_menu_view.h"
#include "ash/capture_mode/test_capture_mode_delegate.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/capture_mode/capture_mode_test_api.h"
#include "ash/shell.h"
#include "ash/style/icon_button.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "base/test/gtest_tags.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/widget/widget.h"

namespace ash {

class GifRecordingTest : public AshTestBase {
 public:
  GifRecordingTest() : scoped_feature_list_(features::kGifRecording) {}
  GifRecordingTest(const GifRecordingTest&) = delete;
  GifRecordingTest& operator=(const GifRecordingTest&) = delete;
  ~GifRecordingTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();
    CaptureModeController::Get()->SetUserCaptureRegion(gfx::Rect(200, 200),
                                                       /*by_user=*/true);
  }

  CaptureModeController* StartRegionVideoCapture() {
    return StartCaptureSession(CaptureModeSource::kRegion,
                               CaptureModeType::kVideo);
  }

  CaptureLabelView* GetCaptureLabelView() {
    return CaptureModeSessionTestApi().GetCaptureLabelView();
  }

  RecordingTypeMenuView* GetRecordingTypeMenuView() {
    return CaptureModeSessionTestApi().GetRecordingTypeMenuView();
  }

  views::LabelButton* GetCaptureButton() {
    auto* label_view = GetCaptureLabelView();
    return label_view->capture_button_container()->capture_button();
  }

  views::ImageButton* GetRecordingTypeDropDownButton() {
    auto* label_view = GetCaptureLabelView();
    EXPECT_TRUE(label_view->IsRecordingTypeDropDownButtonVisible());
    return label_view->capture_button_container()->drop_down_button();
  }

  views::Widget* GetRecordingTypeMenuWidget() {
    return CaptureModeSessionTestApi().GetRecordingTypeMenuWidget();
  }

  views::Widget* GetSettingsMenuWidget() {
    return CaptureModeSessionTestApi().GetCaptureModeSettingsWidget();
  }

  void ClickOnDropDownButton() {
    LeftClickOn(GetRecordingTypeDropDownButton());
  }

  void ClickOnSettingsButton() {
    CaptureModeBarView* bar_view =
        CaptureModeSessionTestApi().GetCaptureModeBarView();
    LeftClickOn(bar_view->settings_button());
  }

 protected:
  base::HistogramTester histogram_tester_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_F(GifRecordingTest, DropDownButtonVisibility) {
  // With region video recording, the drop down button should be visible.
  auto* controller = StartRegionVideoCapture();
  auto* label_view = GetCaptureLabelView();
  EXPECT_TRUE(label_view->IsRecordingTypeDropDownButtonVisible());

  // It should hide, once we switch to image recording, but the label view
  // should remain interactable.
  controller->SetType(CaptureModeType::kImage);
  EXPECT_FALSE(label_view->IsRecordingTypeDropDownButtonVisible());
  EXPECT_TRUE(label_view->IsViewInteractable());

  // Switching to a fullscreen source, the label view becomes no longer
  // interactable, and the drop down button remains hidden.
  controller->SetSource(CaptureModeSource::kFullscreen);
  EXPECT_FALSE(label_view->IsRecordingTypeDropDownButtonVisible());
  EXPECT_FALSE(label_view->IsViewInteractable());

  // Even when we switch back to video recording.
  controller->SetType(CaptureModeType::kVideo);
  EXPECT_FALSE(label_view->IsRecordingTypeDropDownButtonVisible());
  EXPECT_FALSE(label_view->IsViewInteractable());

  // Only region recording in video mode, that the label view is interactable,
  // and the button is visible.
  controller->SetSource(CaptureModeSource::kRegion);
  EXPECT_TRUE(label_view->IsRecordingTypeDropDownButtonVisible());
  EXPECT_TRUE(label_view->IsViewInteractable());
}

TEST_F(GifRecordingTest, RecordingTypeMenuCreation) {
  // The drop down button acts as a toggle.
  StartRegionVideoCapture();
  ClickOnDropDownButton();
  EXPECT_TRUE(GetRecordingTypeMenuWidget());
  ClickOnDropDownButton();
  EXPECT_FALSE(GetRecordingTypeMenuWidget());

  // The settings menu and the recording type menu are mutually exclusive,
  // opening one closes the other.
  ClickOnSettingsButton();
  EXPECT_TRUE(GetSettingsMenuWidget());
  ClickOnDropDownButton();
  EXPECT_TRUE(GetRecordingTypeMenuWidget());
  EXPECT_FALSE(GetSettingsMenuWidget());
  ClickOnSettingsButton();
  EXPECT_TRUE(GetSettingsMenuWidget());
  EXPECT_FALSE(GetRecordingTypeMenuWidget());
}

TEST_F(GifRecordingTest, EscKeyClosesMenu) {
  // Hitting the ESC key closes the recording type menu, but the session remains
  // active.
  auto* controller = StartRegionVideoCapture();
  ClickOnDropDownButton();
  EXPECT_TRUE(GetRecordingTypeMenuWidget());
  PressAndReleaseKey(ui::VKEY_ESCAPE);
  EXPECT_FALSE(GetRecordingTypeMenuWidget());
  EXPECT_TRUE(controller->IsActive());
}

TEST_F(GifRecordingTest, EnterKeyHidesMenuAndStartsCountDown) {
  StartRegionVideoCapture();
  ClickOnDropDownButton();
  auto* recording_type_menu = GetRecordingTypeMenuWidget();
  EXPECT_TRUE(recording_type_menu);

  // Pressing the ENTER key starts the recording count down, at which point, the
  // menu remains open but fades out to an opacity of 0.
  PressAndReleaseKey(ui::VKEY_RETURN);
  EXPECT_TRUE(CaptureModeTestApi().IsInCountDownAnimation());
  ASSERT_EQ(recording_type_menu, GetRecordingTypeMenuWidget());
  EXPECT_FLOAT_EQ(recording_type_menu->GetLayer()->GetTargetOpacity(), 0);
}

TEST_F(GifRecordingTest, ClickingOutsideClosesMenu) {
  auto* controller = StartRegionVideoCapture();
  ClickOnDropDownButton();
  EXPECT_TRUE(GetRecordingTypeMenuWidget());

  // Clicking outside the menu widget should close it, but the region should not
  // change.
  const auto region = controller->user_capture_region();
  auto* generator = GetEventGenerator();
  generator->MoveMouseTo(region.bottom_right() + gfx::Vector2d(10, 10));
  generator->ClickLeftButton();
  EXPECT_FALSE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(region, controller->user_capture_region());
}

TEST_F(GifRecordingTest, ChangingTypeFromMenu) {
  auto* controller = StartRegionVideoCapture();
  EXPECT_EQ(RecordingType::kWebM, controller->recording_type());
  ClickOnDropDownButton();
  EXPECT_TRUE(GetRecordingTypeMenuWidget());

  // The WebM option should be selected and marked with a check. Once the GIF
  // option is clicked, the menu should close, and the recording type in the
  // controller is updated.
  auto* recording_type_menu_view = GetRecordingTypeMenuView();
  EXPECT_TRUE(
      recording_type_menu_view->IsOptionChecked(ToInt(RecordingType::kWebM)));
  LeftClickOn(recording_type_menu_view->GetGifOptionForTesting());
  EXPECT_FALSE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(RecordingType::kGif, controller->recording_type());
}

TEST_F(GifRecordingTest, MenuIsClosedWhenClickingCheckedOption) {
  auto* controller = StartRegionVideoCapture();
  EXPECT_EQ(RecordingType::kWebM, controller->recording_type());
  ClickOnDropDownButton();
  EXPECT_TRUE(GetRecordingTypeMenuWidget());

  // Clicking on the same checked option closes the menu even though there is no
  // change.
  auto* recording_type_menu_view = GetRecordingTypeMenuView();
  LeftClickOn(recording_type_menu_view->GetWebMOptionForTesting());
  EXPECT_FALSE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(RecordingType::kWebM, controller->recording_type());
}

TEST_F(GifRecordingTest, CaptureButtonStateUpdatedFromMenuSelection) {
  // Select GIF from the menu, the capture button label should be updated.
  StartRegionVideoCapture();
  ClickOnDropDownButton();
  LeftClickOn(GetRecordingTypeMenuView()->GetGifOptionForTesting());
  auto* capture_button = GetCaptureButton();
  EXPECT_EQ(capture_button->GetText(), u"Record GIF");

  // Select WebM from the menu, and expect the button label to be updated too.
  ClickOnDropDownButton();
  EXPECT_TRUE(GetRecordingTypeMenuWidget());
  LeftClickOn(GetRecordingTypeMenuView()->GetWebMOptionForTesting());
  EXPECT_EQ(capture_button->GetText(), u"Record video");
}

// When the recording type is set programmatically, the capture button should
// still get updated.
TEST_F(GifRecordingTest, CaptureButtonStateUpdatedFromController) {
  auto* controller = StartRegionVideoCapture();
  controller->SetRecordingType(RecordingType::kGif);
  auto* capture_button = GetCaptureButton();
  EXPECT_EQ(capture_button->GetText(), u"Record GIF");

  controller->SetRecordingType(RecordingType::kWebM);
  EXPECT_EQ(capture_button->GetText(), u"Record video");
}

// Recording type selection affects future capture sessions.
TEST_F(GifRecordingTest, FutureCaptureSessionsAffected) {
  auto* controller = StartRegionVideoCapture();
  ClickOnDropDownButton();
  LeftClickOn(GetRecordingTypeMenuView()->GetGifOptionForTesting());

  // Press the ESC key to exit the current session.
  PressAndReleaseKey(ui::VKEY_ESCAPE);
  EXPECT_FALSE(controller->IsActive());

  // Start a new session, and expect that the capture button should be labeled
  // correctly.
  StartRegionVideoCapture();
  EXPECT_EQ(GetCaptureButton()->GetText(), u"Record GIF");

  // When the menu is open, the correct option is marked as checked.
  ClickOnDropDownButton();
  EXPECT_TRUE(
      GetRecordingTypeMenuView()->IsOptionChecked(ToInt(RecordingType::kGif)));
}

TEST_F(GifRecordingTest, TabNavigation) {
  base::AddFeatureIdTagToTestResult(
      "screenplay-759f3130-3839-408a-8342-a373654e8927");

  auto* controller = StartRegionVideoCapture();

  // Tab 15 times until we reach the capture button.
  auto* event_generator = GetEventGenerator();
  SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/15);
  using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup;
  CaptureModeSessionTestApi test_api(controller->capture_mode_session());
  EXPECT_EQ(FocusGroup::kCaptureButton, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(0u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(GetCaptureButton(), test_api.GetCurrentFocusedView()->GetView());

  // Tab one more time to get to the drop down button.
  SendKey(ui::VKEY_TAB, event_generator);
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(GetRecordingTypeDropDownButton(),
            test_api.GetCurrentFocusedView()->GetView());

  // Pressing the spacebar should open the menu, and we should be in the
  // `kPendingRecordingType` focus group.
  SendKey(ui::VKEY_SPACE, event_generator);
  EXPECT_TRUE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(FocusGroup::kPendingRecordingType, test_api.GetCurrentFocusGroup());

  // The next tab will move the focus inside the menu.
  SendKey(ui::VKEY_TAB, event_generator);
  EXPECT_EQ(FocusGroup::kRecordingTypeMenu, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(0u, test_api.GetCurrentFocusIndex());
  // And the WebM option will be focused.
  auto* recording_type_menu_view = GetRecordingTypeMenuView();
  EXPECT_EQ(recording_type_menu_view->GetWebMOptionForTesting(),
            test_api.GetCurrentFocusedView()->GetView());

  // Tabbing again will focus on the GIF option.
  SendKey(ui::VKEY_TAB, event_generator);
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(recording_type_menu_view->GetGifOptionForTesting(),
            test_api.GetCurrentFocusedView()->GetView());

  // The next tab will focus on the settings button, but the recording type menu
  // stays open.
  SendKey(ui::VKEY_TAB, event_generator);
  EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(0u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(test_api.GetCaptureModeBarView()->settings_button(),
            test_api.GetCurrentFocusedView()->GetView());

  // Reverse tabbing should get us back to the GIF option.
  SendKey(ui::VKEY_TAB, event_generator, ui::EF_SHIFT_DOWN);
  EXPECT_EQ(FocusGroup::kRecordingTypeMenu, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(recording_type_menu_view->GetGifOptionForTesting(),
            test_api.GetCurrentFocusedView()->GetView());

  // Pressing the spacebar should select GIF, and close the menu.
  SendKey(ui::VKEY_SPACE, event_generator);
  EXPECT_FALSE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(RecordingType::kGif, controller->recording_type());

  // The focus is moved back to the drop down button.
  EXPECT_EQ(FocusGroup::kCaptureButton, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(GetRecordingTypeDropDownButton(),
            test_api.GetCurrentFocusedView()->GetView());
}

TEST_F(GifRecordingTest, PressingEnterOnAFocusedItemBehavesLikeSpace) {
  auto* controller = StartRegionVideoCapture();

  // Tab 16 times until we reach the drop down button.
  auto* event_generator = GetEventGenerator();
  SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/16);
  using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup;
  CaptureModeSessionTestApi test_api(controller->capture_mode_session());
  EXPECT_EQ(FocusGroup::kCaptureButton, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(GetRecordingTypeDropDownButton(),
            test_api.GetCurrentFocusedView()->GetView());

  // Pressing the enter should open the menu, and we should be in the
  // `kPendingRecordingType` focus group.
  SendKey(ui::VKEY_RETURN, event_generator);
  EXPECT_TRUE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(FocusGroup::kPendingRecordingType, test_api.GetCurrentFocusGroup());

  // Then tab twice to reach the GIF recording option.
  SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/2);
  EXPECT_EQ(FocusGroup::kRecordingTypeMenu, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  auto* recording_type_menu_view = GetRecordingTypeMenuView();
  EXPECT_EQ(recording_type_menu_view->GetGifOptionForTesting(),
            test_api.GetCurrentFocusedView()->GetView());

  // Pressing the enter key should select GIF, and close the menu.
  SendKey(ui::VKEY_RETURN, event_generator);
  EXPECT_FALSE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(RecordingType::kGif, controller->recording_type());

  // The focus is moved back to the drop down button.
  EXPECT_EQ(FocusGroup::kCaptureButton, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(GetRecordingTypeDropDownButton(),
            test_api.GetCurrentFocusedView()->GetView());
}
TEST_F(GifRecordingTest, CloseRecordingMenuWhileFocusIsSomewhereElse) {
  auto* controller = StartRegionVideoCapture();

  // Tab 16 times until we reach the drop down button.
  auto* event_generator = GetEventGenerator();
  SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/16);
  using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup;
  CaptureModeSessionTestApi test_api(controller->capture_mode_session());
  EXPECT_EQ(FocusGroup::kCaptureButton, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(GetRecordingTypeDropDownButton(),
            test_api.GetCurrentFocusedView()->GetView());

  // Pressing the spacebar should open the menu, and we should be in the
  // `kPendingRecordingType` focus group.
  SendKey(ui::VKEY_SPACE, event_generator);
  EXPECT_TRUE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(FocusGroup::kPendingRecordingType, test_api.GetCurrentFocusGroup());

  // Now tab 4 times to put the focus on the close button.
  SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/4);
  EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(test_api.GetCaptureModeBarView()->close_button(),
            test_api.GetCurrentFocusedView()->GetView());

  // Press the escape key, the menu should close, but the focus should not
  // change, since focus was not in or about to be in the menu.
  SendKey(ui::VKEY_ESCAPE, event_generator);
  EXPECT_FALSE(GetRecordingTypeMenuWidget());
  EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup());
  EXPECT_EQ(1u, test_api.GetCurrentFocusIndex());
  EXPECT_EQ(test_api.GetCaptureModeBarView()->close_button(),
            test_api.GetCurrentFocusedView()->GetView());
}

TEST_F(GifRecordingTest, GifIsNotSupportedForFullscreenOrWindow) {
  struct {
    const char* const scope_name;
    CaptureModeSource source;
  } kTestCases[] = {
      {"Testing fullscreen", CaptureModeSource::kFullscreen},
      {"Testing window", CaptureModeSource::kWindow},
  };

  auto window = CreateTestWindow(gfx::Rect(200, 200));

  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(test_case.scope_name);
    auto* controller = StartRegionVideoCapture();
    controller->SetRecordingType(RecordingType::kGif);

    // Audio recording is not supported for GIF, but switching to fullscreen or
    // window recording should switch to webm recording, which do support audio
    // recording, so we should expect that.
    controller->SetAudioRecordingMode(AudioRecordingMode::kMicrophone);

    // Switch to another source than region.
    controller->SetSource(test_case.source);
    // The recording type remains the same, and is still set as GIF. However,
    // the recording will be forced to webm, since GIF is only supported for
    // `kRegion`.
    EXPECT_EQ(controller->recording_type(), RecordingType::kGif);

    // This is needed for window recording.
    GetEventGenerator()->MoveMouseToCenterOf(window.get());

    StartVideoRecordingImmediately();

    EXPECT_TRUE(controller->is_recording_in_progress());
    auto* test_delegate = static_cast<TestCaptureModeDelegate*>(
        controller->delegate_for_testing());
    CaptureModeTestApi().FlushRecordingServiceForTesting();
    EXPECT_TRUE(test_delegate->IsDoingAudioRecording());
    controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);

    // The resulting file should have a ".webm" extension.
    const auto file = WaitForCaptureFileToBeSaved();
    EXPECT_TRUE(file.MatchesExtension(".webm"));
  }
}

TEST_F(GifRecordingTest, RecordingTypeIsRespected) {
  auto* controller = StartRegionVideoCapture();
  controller->SetRecordingType(RecordingType::kGif);

  // Even though audio recording is enabled, when performing a GIF recording,
  // the recording service should not be asked to connect to the audio streaming
  // factory and should not be doing any audio recording.
  controller->SetAudioRecordingMode(AudioRecordingMode::kMicrophone);
  StartVideoRecordingImmediately();

  // Test that the configuration histogram was reported correctly, and that the
  // audio histogram was never reported.
  histogram_tester_.ExpectUniqueSample(
      "Ash.CaptureModeController.CaptureConfiguration.ClamshellMode",
      CaptureModeConfiguration::kRegionGifRecording,
      /*expected_bucket_count=*/1);
  histogram_tester_.ExpectTotalCount(
      "Ash.CaptureModeController.CaptureAudioOnMetric.ClamshellMode",
      /*expected_count=*/0);

  EXPECT_TRUE(controller->is_recording_in_progress());
  auto* test_delegate =
      static_cast<TestCaptureModeDelegate*>(controller->delegate_for_testing());
  CaptureModeTestApi().FlushRecordingServiceForTesting();
  EXPECT_FALSE(test_delegate->IsDoingAudioRecording());

  // Record for one second so that we can test the recording length histogram.
  WaitForSeconds(1);
  controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);

  // The resulting file should have a ".gif" extension.
  const auto file = WaitForCaptureFileToBeSaved();
  EXPECT_TRUE(file.MatchesExtension(".gif"));

  histogram_tester_.ExpectUniqueSample(
      "Ash.CaptureModeController.GIFRecordingLength.ClamshellMode",
      /*sample=*/1,  // 1 second.
      /*expected_bucket_count=*/1);

  // Since getting the file size is an async operation, we have to run a loop
  // until the task that records the file size is done.
  base::RunLoop().RunUntilIdle();
  histogram_tester_.ExpectTotalCount(
      "Ash.CaptureModeController.GIFRecordingFileSize.ClamshellMode",
      /*expected_count=*/1);
}

TEST_F(GifRecordingTest, RegionToScreenRatioHistogram) {
  UpdateDisplay("900x600");

  // Contains 3 test cases where the user region areas are different percentages
  // of the full screen area.
  struct {
    const char* const scope_title;
    gfx::Rect user_region_bounds;
    int expected_percent_ratio;
  } kTestCases[] = {
      {"With region 450x300", gfx::Rect(450, 300), 25},   // 25%.
      {"With region 900x300", gfx::Rect(900, 300), 50},   // 50%.
      {"With region 900x600", gfx::Rect(900, 600), 100},  // 100%.
  };

  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(test_case.scope_title);
    auto* controller = CaptureModeController::Get();
    controller->SetUserCaptureRegion(test_case.user_region_bounds,
                                     /*by_user=*/true);
    controller->SetRecordingType(RecordingType::kGif);

    StartRegionVideoCapture();
    StartVideoRecordingImmediately();

    histogram_tester_.ExpectBucketCount(
        "Ash.CaptureModeController.GIFRecordingRegionToScreenRatio."
        "ClamshellMode",
        /*sample=*/test_case.expected_percent_ratio,
        /*expected_count=*/1);

    controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
    WaitForCaptureFileToBeSaved();
  }
}

// Regression test for b/293340894. When the region is selected in a such a way
// that will cause the default bounds of the recording type menu to go outside
// the display bounds, the bounds should be adjusted such that it remains within
// the target display.
TEST_F(GifRecordingTest, RecordingMenuOutsideOfBounds) {
  UpdateDisplay("800x700,801+0-800x700");
  auto* controller = CaptureModeController::Get();
  controller->SetUserCaptureRegion(gfx::Rect(1550, 650, 50, 50),
                                   /*by_user=*/true);
  controller->SetRecordingType(RecordingType::kGif);

  auto* event_generator = GetEventGenerator();
  // Move cursor to the second display so capture mode is created there when it
  // starts.
  event_generator->MoveMouseTo(gfx::Point(1000, 500));
  StartRegionVideoCapture();
  ClickOnDropDownButton();

  // The menu should be created without any crashes and should be contained
  // within the bounds of the external display.
  auto* recording_type_menu_widget = GetRecordingTypeMenuWidget();
  ASSERT_TRUE(recording_type_menu_widget);
  const gfx::Rect display_bounds{801, 0, 800, 700};
  EXPECT_TRUE(display_bounds.Contains(
      recording_type_menu_widget->GetWindowBoundsInScreen()));
}

// Regression test for b/319551191.
TEST_F(GifRecordingTest, RecordingMenuAtTheLeftOrRightEdge) {
  // Set a region touching the left edge of the screen.
  const gfx::Size region_size(177, 165);
  auto* controller = CaptureModeController::Get();
  controller->SetUserCaptureRegion(gfx::Rect(gfx::Point(0, 0), region_size),
                                   /*by_user=*/true);

  // There should be no crashes when we open the recording type menu.
  StartRegionVideoCapture();
  ClickOnDropDownButton();

  // Restart the session with a region touching the right edge of the screen.
  controller->Stop();
  const auto root_bounds = Shell::GetPrimaryRootWindow()->bounds();
  controller->SetUserCaptureRegion(
      gfx::Rect(gfx::Point(root_bounds.right() - region_size.width(), 0),
                region_size),
      /*by_user=*/true);

  // Similarly, there should be no crashes.
  StartRegionVideoCapture();
  ClickOnDropDownButton();
}

// -----------------------------------------------------------------------------
// ProjectorGifRecordingTest:

class ProjectorGifRecordingTest : public GifRecordingTest {
 public:
  ProjectorGifRecordingTest() = default;
  ~ProjectorGifRecordingTest() override = default;

  // ProjectorGifRecordingTest:
  void SetUp() override {
    GifRecordingTest::SetUp();
    projector_helper_.SetUp();
  }

  void StartProjectorModeSession() {
    projector_helper_.StartProjectorModeSession();
  }

 private:
  ProjectorCaptureModeIntegrationHelper projector_helper_;
};

TEST_F(ProjectorGifRecordingTest, ProjectorRecordingType) {
  // Start a normal session and enable GIF recording.
  auto* controller = StartRegionVideoCapture();
  controller->SetRecordingType(RecordingType::kGif);

  // Exit this session and start a new projector-initiated session. The active
  // recording type should be `kWebM`.
  controller->Stop();
  StartProjectorModeSession();
  EXPECT_TRUE(controller->IsActive());
  EXPECT_EQ(controller->recording_type(), RecordingType::kWebM);

  // By default, the capture source is fullscreen in projector sessions. Switch
  // to `kRegion` and expect that the capture button will show the correct text.
  controller->SetSource(CaptureModeSource::kRegion);
  EXPECT_EQ(GetCaptureButton()->GetText(), u"Record video");

  // The drop-down button should not be created in this case.
  EXPECT_FALSE(
      GetCaptureLabelView()->capture_button_container()->drop_down_button());

  // Exit this session and start a new normal session with the most recent
  // values and expect that the pre-projector-session recording type was
  // restored.
  controller->Stop();
  controller->Start(CaptureModeEntryType::kQuickSettings);
  EXPECT_EQ(controller->recording_type(), RecordingType::kGif);
  EXPECT_EQ(GetCaptureButton()->GetText(), u"Record GIF");
  EXPECT_TRUE(
      GetCaptureLabelView()->capture_button_container()->drop_down_button());
}

}  // namespace ash