chromium/ash/system/focus_mode/focus_mode_detailed_view_unittest.cc

// Copyright 2023 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/focus_mode/focus_mode_detailed_view.h"

#include <memory>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/api/tasks/fake_tasks_client.h"
#include "ash/capture_mode/capture_mode_test_util.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/glanceables/common/glanceables_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/pill_button.h"
#include "ash/style/rounded_container.h"
#include "ash/style/switch.h"
#include "ash/style/system_textfield.h"
#include "ash/system/focus_mode/focus_mode_chip_carousel.h"
#include "ash/system/focus_mode/focus_mode_controller.h"
#include "ash/system/focus_mode/focus_mode_countdown_view.h"
#include "ash/system/focus_mode/focus_mode_feature_pod_controller.h"
#include "ash/system/focus_mode/focus_mode_histogram_names.h"
#include "ash/system/focus_mode/focus_mode_task_test_utils.h"
#include "ash/system/focus_mode/focus_mode_task_view.h"
#include "ash/system/focus_mode/focus_mode_util.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/tray/fake_detailed_view_delegate.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/unified/feature_tile.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "ash/test/ash_test_base.h"
#include "base/i18n/time_formatting.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chromeos/ash/components/settings/scoped_timezone_settings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/message_center/message_center.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"

namespace ash {

namespace {

constexpr base::TimeDelta kStartAnimationDelay = base::Milliseconds(300);

}  // namespace

class FocusModeDetailedViewTest : public AshTestBase {
 public:
  FocusModeDetailedViewTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
        scoped_feature_(features::kFocusMode) {}
  ~FocusModeDetailedViewTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();

    // `g_network_handler` is null in tests, we need to manually set the network
    // connected state. Also, the buttons or textfield under the tasks view will
    // be enabled and the playlists under the sounds view will exists only when
    // the user is online.
    glanceables_util::SetIsNetworkConnectedForTest(true);

    widget_ = CreateFramelessTestWidget();
    widget_->SetFullscreen(true);

    // Focus Mode considers it to be a first time user flow if
    // `kFocusModeDoNotDisturb` has never been set by the user before. For
    // normal feature testing purposes, we will intentionally set it so that the
    // pref will not be marked as using the default value.
    prefs()->SetBoolean(prefs::kFocusModeDoNotDisturb, true);

    auto& tasks_client =
        CreateFakeTasksClient(AccountId::FromUserEmail("user0@tray"));
    tasks_client.set_http_error(google_apis::ApiErrorCode::HTTP_SUCCESS);
    CreateFakeTasks(tasks_client);

    CreateFakeFocusModeDetailedView();
  }

  void TearDown() override {
    focus_mode_detailed_view_ = nullptr;
    widget_.reset();

    AshTestBase::TearDown();
  }

  void AdvanceClock(base::TimeDelta time_delta) {
    // Note that AdvanceClock() is used here instead of FastForwardBy() to
    // prevent long run time during an ash test session.
    task_environment()->AdvanceClock(time_delta);
    task_environment()->RunUntilIdle();
  }

  virtual void CreateFakeTasks(api::FakeTasksClient& tasks_client) {
    AddFakeTaskList(tasks_client, "default");
    AddFakeTask(tasks_client, "default", "task1", "Task 1");
  }

  void CreateFakeFocusModeDetailedView() {
    auto focus_mode_detailed_view =
        std::make_unique<FocusModeDetailedView>(&detailed_view_delegate_);
    focus_mode_detailed_view_ = focus_mode_detailed_view.get();

    widget_->SetContentsView(std::move(focus_mode_detailed_view));
  }

  void SetInactiveSessionDuration(SystemTextfield* timer_textfield) {
    DCHECK(!FocusModeController::Get()->in_focus_session());
    focus_mode_detailed_view_->SetInactiveSessionDuration(base::Minutes(
        focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield)));
  }

  // Scroll to the bottom of the defailed view.
  void ScrollToBottom() {
    auto* scroll_view = focus_mode_detailed_view_->scroll_view_for_testing();
    scroll_view->ScrollToPosition(scroll_view->vertical_scroll_bar(),
                                  scroll_view->GetVisibleRect().bottom());
  }

  views::ScrollView* GetScrollView() {
    return focus_mode_detailed_view_->scroll_view_for_testing();
  }

  views::Label* GetToggleRowLabel() {
    return focus_mode_detailed_view_->toggle_view_->text_label();
  }

  views::Label* GetToggleRowSubLabel() {
    return focus_mode_detailed_view_->toggle_view_->sub_text_label();
  }

  bool IsToggleRowSubLabelVisible() {
    return GetToggleRowSubLabel() && GetToggleRowSubLabel()->GetVisible();
  }

  HoverHighlightView* GetToggleView() {
    return focus_mode_detailed_view_->toggle_view_;
  }

  PillButton* GetToggleRowButton() {
    return views::AsViewClass<PillButton>(
        focus_mode_detailed_view_->toggle_view_->right_view());
  }

  views::BoxLayoutView* GetTimerSettingView() {
    return focus_mode_detailed_view_->timer_setting_view_;
  }

  SystemTextfield* GetTimerSettingTextfield() {
    CHECK(!FocusModeController::Get()->in_focus_session());
    return focus_mode_detailed_view_->timer_textfield_;
  }

  IconButton* GetTimerSettingIncrementButton() {
    return focus_mode_detailed_view_->timer_increment_button_;
  }

  IconButton* GetTimerSettingDecrementButton() {
    return focus_mode_detailed_view_->timer_decrement_button_;
  }

  RoundedContainer* GetDoNotDisturbContainer() {
    return focus_mode_detailed_view_->do_not_disturb_view_;
  }

  Switch* GetDoNotDisturbToggleButton() {
    CHECK(!FocusModeController::Get()->in_focus_session());
    return focus_mode_detailed_view_->do_not_disturb_toggle_button_;
  }

  FocusModeCountdownView* GetTimerCountdownView() {
    return focus_mode_detailed_view_->timer_countdown_view_;
  }

  views::Label* GetEndTimeLabel() {
    return focus_mode_detailed_view_->end_time_label_;
  }

  PrefService* prefs() {
    return Shell::Get()->session_controller()->GetActivePrefService();
  }

  FocusModeTaskView* GetTaskView() {
    return focus_mode_detailed_view_->focus_mode_task_view_;
  }

  RoundedContainer* GetTaskContainerView() {
    return focus_mode_detailed_view_->task_view_container_;
  }

  FakeDetailedViewDelegate detailed_view_delegate_;

 private:
  base::test::ScopedFeatureList scoped_feature_;
  std::unique_ptr<views::Widget> widget_;
  raw_ptr<FocusModeDetailedView> focus_mode_detailed_view_ = nullptr;
};

// Tests that the DND in Quick Settings is off and the toggle button is on/off
// respectively.
TEST_F(FocusModeDetailedViewTest, DndOffBeforeStart) {
  auto* message_center = message_center::MessageCenter::Get();
  auto* focus_mode_controller = FocusModeController::Get();
  Switch* toggle_button = GetDoNotDisturbToggleButton();

  // 1. Before turning on a focus session, the system do not disturb is off. The
  // default value for the toggle button is set to enabled.
  EXPECT_FALSE(message_center->IsQuietMode());
  EXPECT_TRUE(toggle_button->GetIsOn());

  // Start a focus session and verify that quiet mode is on.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_TRUE(focus_mode_controller->in_focus_session());
  EXPECT_TRUE(message_center->IsQuietMode());

  // End the focus session. The system do not disturb will be back to its
  // original state at the end of the current focus session. The toggle button's
  // state will be back to its state before the focus session.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_FALSE(focus_mode_controller->in_focus_session());
  EXPECT_FALSE(message_center->IsQuietMode());
  EXPECT_TRUE(toggle_button->GetIsOn());

  // 2. Before turning on a focus session, the system do not disturb is off. The
  // default value for the toggle button is set to disabled.
  // Scroll to the bottom of the focus panel to make the `toggle_button` visible
  // before clicking on it.
  ScrollToBottom();
  LeftClickOn(toggle_button);
  EXPECT_FALSE(toggle_button->GetIsOn());

  // Start a focus session and verify that quiet mode is off.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_TRUE(focus_mode_controller->in_focus_session());
  EXPECT_FALSE(message_center->IsQuietMode());

  // End the focus session. The system do not disturb will be back to its
  // original state at the end of the current focus session. The toggle button's
  // state will be back to its state before the focus session.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_FALSE(focus_mode_controller->in_focus_session());
  EXPECT_FALSE(message_center->IsQuietMode());
  EXPECT_FALSE(toggle_button->GetIsOn());
}

// Tests that the DND in Quick Settings is on and the toggle button is on/off
// respectively. We also test the behavior for user interactions during a focus
// session.
TEST_F(FocusModeDetailedViewTest, DndOnBeforeStart) {
  auto* message_center = message_center::MessageCenter::Get();
  auto* focus_mode_controller = FocusModeController::Get();
  Switch* toggle_button = GetDoNotDisturbToggleButton();

  // 1. Before turning on a focus session, the system do not disturb is on. The
  // default value for the toggle button is set to enabled.
  message_center->SetQuietMode(true);
  EXPECT_TRUE(message_center->IsQuietMode());

  EXPECT_TRUE(toggle_button->GetIsOn());

  // Start a focus session and verify that quiet mode is on.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_TRUE(focus_mode_controller->in_focus_session());
  EXPECT_TRUE(message_center->IsQuietMode());

  // During the focus session, the user turned off the DND.
  message_center->SetQuietMode(false);
  EXPECT_FALSE(message_center->IsQuietMode());

  // End the focus session. The system do not disturb will keep disabled at the
  // end of the current focus session. The toggle button's state will be back to
  // its state before the focus session.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_FALSE(focus_mode_controller->in_focus_session());
  EXPECT_FALSE(message_center->IsQuietMode());
  EXPECT_TRUE(toggle_button->GetIsOn());

  // 2. Before turning on a focus session, the system do not disturb is on. The
  // default value for the toggle button is set to disabled.
  message_center->SetQuietMode(true);
  EXPECT_TRUE(message_center->IsQuietMode());

  // Scroll to the bottom of the focus panel to make the `toggle_button` visible
  // before clicking on it.
  ScrollToBottom();
  LeftClickOn(toggle_button);
  EXPECT_FALSE(toggle_button->GetIsOn());

  // Start a focus session and verify that quiet mode is on.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_TRUE(focus_mode_controller->in_focus_session());
  EXPECT_TRUE(message_center->IsQuietMode());

  // End the focus session. The system do not disturb will be back to its
  // original state at the end of the current focus session. The toggle button's
  // state will be back to its state before the focus session.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_FALSE(focus_mode_controller->in_focus_session());
  EXPECT_TRUE(message_center->IsQuietMode());
  EXPECT_FALSE(toggle_button->GetIsOn());
}

// Tests label texts and start/stop functionalities for the toggle row.
TEST_F(FocusModeDetailedViewTest, ToggleRow) {
  auto* focus_mode_controller = FocusModeController::Get();

  auto validate_labels = [&](bool active, const std::string& trace_name) {
    SCOPED_TRACE(trace_name);
    EXPECT_EQ(active, focus_mode_controller->in_focus_session());
    EXPECT_EQ(active ? u"Focus is on" : u"Focus",
              GetToggleRowLabel()->GetText());

    EXPECT_EQ(active, IsToggleRowSubLabelVisible());

    if (active) {
      EXPECT_EQ(focus_mode_util::GetFormattedEndTimeString(
                    focus_mode_controller->GetActualEndTime()),
                GetToggleRowSubLabel()->GetText());
    }
    EXPECT_EQ(active ? u"Finish" : u"Start", GetToggleRowButton()->GetText());
  };

  validate_labels(/*active=*/false, "Initial state");

  // Starting the focus session closes the bubble, so we need to simulate
  // recreating the detailed view.
  LeftClickOn(GetToggleRowButton());
  CreateFakeFocusModeDetailedView();

  // Wait a minute to test that the time remaining label updates.
  AdvanceClock(base::Seconds(60));
  validate_labels(/*active=*/true, "Wait for a minute");

  LeftClickOn(GetToggleRowButton());
  validate_labels(/*active=*/false, "Toggle off session");

  // Verify that the time displays correctly in the 24-hour clock format.
  Shell::Get()->system_tray_model()->SetUse24HourClock(true);

  // Starting the focus session closes the bubble, so we need to simulate
  // recreating the detailed view.
  LeftClickOn(GetToggleRowButton());
  CreateFakeFocusModeDetailedView();

  // Wait a second to avoid the time remaining being either 1500 seconds or
  // 1499.99 seconds.
  AdvanceClock(base::Seconds(1));
  validate_labels(/*active=*/true, "Check time passed");

  LeftClickOn(GetToggleRowButton());
  validate_labels(/*active=*/false, "Toggle off session again");
}

// Tests how the textfield for the timer setting view handles valid and invalid
// inputs.
TEST_F(FocusModeDetailedViewTest, TimerSettingViewTextfield) {
  SystemTextfield* timer_textfield = GetTimerSettingTextfield();
  LeftClickOn(timer_textfield);
  ASSERT_TRUE(timer_textfield->IsActive());
  PressAndReleaseKey(ui::KeyboardCode::VKEY_DELETE);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_3);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_0);
  EXPECT_EQ(u"30", timer_textfield->GetText());

  // Test that we can not put in non-numeric characters.
  PressAndReleaseKey(ui::KeyboardCode::VKEY_A);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_OEM_PERIOD);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_OEM_PLUS);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_SPACE);
  EXPECT_EQ(u"30", timer_textfield->GetText());

  // Try pressing return with no text inside. Should return text to previous
  // value.
  PressAndReleaseKey(ui::KeyboardCode::VKEY_DELETE);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_DELETE);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
  EXPECT_EQ(u"30", timer_textfield->GetText());

  // Try setting an invalid value.
  LeftClickOn(timer_textfield);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_3);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_3);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_3);
  EXPECT_EQ(u"333", timer_textfield->GetText());
  PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
  EXPECT_EQ(u"300", timer_textfield->GetText());
}

// Tests how the textfield for the timer setting view handles valid and invalid
// inputs from virtual keyboard.
TEST_F(FocusModeDetailedViewTest, TimerSettingViewTextfieldVK) {
  SystemTextfield* timer_textfield = GetTimerSettingTextfield();
  LeftClickOn(timer_textfield);
  ASSERT_TRUE(timer_textfield->IsActive());

  auto* event_generator = GetEventGenerator();
  PressAndReleaseKeyOnVK(event_generator, ui::VKEY_DELETE);
  PressAndReleaseKeyOnVK(event_generator, ui::VKEY_2);
  PressAndReleaseKeyOnVK(event_generator, ui::VKEY_A);
  PressAndReleaseKeyOnVK(event_generator, ui::VKEY_A);
  PressAndReleaseKeyOnVK(event_generator, ui::VKEY_0);
  PressAndReleaseKeyOnVK(event_generator, ui::VKEY_0);
  PressAndReleaseKeyOnVK(event_generator, ui::VKEY_0);
  EXPECT_EQ(u"200", timer_textfield->GetText());
}

// Tests that incrementing the duration of an inactive focus session follows a
// pattern where, when we are starting with a value of:
// - 1 through 4, inclusive, will lead to an increment of 1.
// - 5 through 59, inclusive, will lead to an increment to the next multiple
//   of 5.
// - 60 through 299, inclusive, will lead to an increment to the next multiple
//   of 15.
// - 300, we will not increment further.
TEST_F(FocusModeDetailedViewTest, TimerSettingViewIncrements) {
  SystemTextfield* timer_textfield = GetTimerSettingTextfield();
  IconButton* decrement_button = GetTimerSettingDecrementButton();
  IconButton* increment_button = GetTimerSettingIncrementButton();

  // Check incrementing 1 through 5.
  timer_textfield->SetText(u"1");
  SetInactiveSessionDuration(timer_textfield);

  // The `decrement_button` will be disabled only when setting the duration to
  // the minimum duration.
  EXPECT_FALSE(decrement_button->GetEnabled());
  LeftClickOn(increment_button);
  int expected_next_value = 2;
  for (int i = 0; i < 3; i++) {
    EXPECT_EQ(
        expected_next_value,
        focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield));
    expected_next_value += 1;
    LeftClickOn(increment_button);
  }
  EXPECT_TRUE(decrement_button->GetEnabled());

  // Increment 5 to 10.
  EXPECT_EQ(u"5", timer_textfield->GetText());
  LeftClickOn(increment_button);
  EXPECT_EQ(u"10", timer_textfield->GetText());

  // Try incrementing 6 to 10, and then continue incrementing to 60.
  timer_textfield->SetText(u"6");
  LeftClickOn(increment_button);
  expected_next_value = 10;
  for (int i = 0; i < 10; i++) {
    EXPECT_EQ(
        expected_next_value,
        focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield));
    expected_next_value += 5;
    LeftClickOn(increment_button);
  }

  // Increment 60 to 75.
  EXPECT_EQ(u"60", timer_textfield->GetText());
  LeftClickOn(increment_button);
  EXPECT_EQ(u"75", timer_textfield->GetText());

  // Try incrementing 61 to 75, and then continue incrementing to 300.
  timer_textfield->SetText(u"61");
  LeftClickOn(increment_button);
  expected_next_value = 75;
  for (int i = 0; i < 15; i++) {
    EXPECT_EQ(
        expected_next_value,
        focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield));
    expected_next_value += 15;
    LeftClickOn(increment_button);
  }

  // Try incrementing past 300.
  EXPECT_EQ(u"300", timer_textfield->GetText());
  LeftClickOn(increment_button);
  EXPECT_EQ(u"300", timer_textfield->GetText());
}

// Tests that decrementing the duration of an inactive focus session follows a
// pattern where, when we are starting with a value of:
// - 1, we will not decrement further
// - 2 through 5, inclusive, will lead to a decrement of 1.
// - 6 through 60, inclusive, will lead to a decrement to the next lowest
//   multiple of 5.
// - 61 through 300, inclusive, will lead to a decrement to the next lowest
//   multiple of 15.
TEST_F(FocusModeDetailedViewTest, TimerSettingViewDecrements) {
  SystemTextfield* timer_textfield = GetTimerSettingTextfield();
  IconButton* decrement_button = GetTimerSettingDecrementButton();
  IconButton* increment_button = GetTimerSettingIncrementButton();

  // Decrement 300 to 285.
  timer_textfield->SetText(u"300");
  SetInactiveSessionDuration(timer_textfield);

  // The `increment_button` will be disabled only when setting the duration to
  // the maximum duration.
  EXPECT_FALSE(increment_button->GetEnabled());
  LeftClickOn(decrement_button);
  EXPECT_EQ(u"285", timer_textfield->GetText());
  EXPECT_TRUE(increment_button->GetEnabled());

  // Try decrementing 299 to 285, and then continue decrementing to 60.
  timer_textfield->SetText(u"299");
  LeftClickOn(decrement_button);
  int expected_next_value = 285;
  for (int i = 0; i < 15; i++) {
    EXPECT_EQ(
        expected_next_value,
        focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield));
    expected_next_value -= 15;
    LeftClickOn(decrement_button);
  }

  // Decrement 60 to 55.
  EXPECT_EQ(u"60", timer_textfield->GetText());
  LeftClickOn(decrement_button);
  EXPECT_EQ(u"55", timer_textfield->GetText());

  // Try decrementing 59 to 55, and then continue decrementing to 5.
  timer_textfield->SetText(u"59");
  LeftClickOn(decrement_button);
  expected_next_value = 55;
  for (int i = 0; i < 10; i++) {
    EXPECT_EQ(
        expected_next_value,
        focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield));
    expected_next_value -= 5;
    LeftClickOn(decrement_button);
  }

  // Check decrementing 5 to 1.
  EXPECT_EQ(u"5", timer_textfield->GetText());
  LeftClickOn(decrement_button);
  expected_next_value = 4;
  for (int i = 0; i < 3; i++) {
    EXPECT_EQ(
        expected_next_value,
        focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield));
    expected_next_value -= 1;
    LeftClickOn(decrement_button);
  }

  // Try decrementing past 1.
  EXPECT_EQ(u"1", timer_textfield->GetText());
  LeftClickOn(decrement_button);
  EXPECT_EQ(u"1", timer_textfield->GetText());
}

// Tests that the timer setting view is visible outside of a focus session and
// the countdown view is visible in a focus session.
TEST_F(FocusModeDetailedViewTest, TimerViewVisibility) {
  auto* focus_mode_controller = FocusModeController::Get();
  auto* timer_setting_view = GetTimerSettingView();
  auto* countdown_view = GetTimerCountdownView();

  // Before turning on a focus session both views should exist and the setting
  // view should be visible.
  ASSERT_TRUE(countdown_view);
  ASSERT_TRUE(timer_setting_view);
  EXPECT_FALSE(countdown_view->GetVisible());
  EXPECT_TRUE(timer_setting_view->GetVisible());

  const base::TimeDelta session_duration =
      focus_mode_controller->GetSessionDuration();
  EXPECT_EQ(focus_mode_util::GetFormattedEndTimeString(base::Time::Now() +
                                                       session_duration),
            GetEndTimeLabel()->GetText());

  // Wait a minute to test that the end time label updates.
  AdvanceClock(base::Seconds(60));
  EXPECT_EQ(focus_mode_util::GetFormattedEndTimeString(base::Time::Now() +
                                                       session_duration),
            GetEndTimeLabel()->GetText());

  // In a focus session the countdown view should be visible and the timer view
  // hidden.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_TRUE(focus_mode_controller->in_focus_session());
  // Starting the focus session closes the bubble, so we need to simulate
  // recreating the detailed view.
  CreateFakeFocusModeDetailedView();
  timer_setting_view = GetTimerSettingView();
  countdown_view = GetTimerCountdownView();
  EXPECT_TRUE(countdown_view->GetVisible());
  EXPECT_FALSE(timer_setting_view->GetVisible());

  // Turning the focus session back off should swap the visibilities again.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_FALSE(focus_mode_controller->in_focus_session());
  EXPECT_FALSE(countdown_view->GetVisible());
  EXPECT_TRUE(timer_setting_view->GetVisible());
  EXPECT_EQ(focus_mode_util::GetFormattedEndTimeString(base::Time::Now() +
                                                       session_duration),
            GetEndTimeLabel()->GetText());
}

// Verify that the toggle row sublabel is shown in the first time user flow.
TEST_F(FocusModeDetailedViewTest, FirstTimeUserFlow) {
  // Clear `kFocusModeDoNotDisturb` to trigger the first time user flow.
  prefs()->ClearPref(prefs::kFocusModeDoNotDisturb);

  // Recreate the detailed view so that the UI is updated after we set the user
  // pref.
  CreateFakeFocusModeDetailedView();

  // Verify that the first time user flow text is displayed.
  EXPECT_TRUE(IsToggleRowSubLabelVisible());
  EXPECT_EQ(GetToggleRowSubLabel()->GetText(),
            l10n_util::GetStringUTF16(
                IDS_ASH_STATUS_TRAY_FOCUS_MODE_FIRST_TIME_SUBLABEL));

  // Start and stop a focus session. This puts us back into the focus panel
  // outside of the first time user flow.
  LeftClickOn(GetToggleRowButton());
  CreateFakeFocusModeDetailedView();
  LeftClickOn(GetToggleRowButton());

  // Verify that the first time user flow text no longer is displayed.
  EXPECT_FALSE(IsToggleRowSubLabelVisible());
}

// Tests that changing the duration in the detailed view while the session is
// inactive changes the duration on the feature pod.
TEST_F(FocusModeDetailedViewTest,
       InactiveSessionDurationChangeSyncsWithFeaturePod) {
  GetPrimaryUnifiedSystemTray()->ShowBubble();
  auto controller = std::make_unique<FocusModeFeaturePodController>(
      GetPrimaryUnifiedSystemTray()
          ->bubble()
          ->unified_system_tray_controller());
  auto pod = controller->CreateTile();

  auto* timer_textfield = GetTimerSettingTextfield();
  auto textfield_text_before_increment = timer_textfield->GetText();
  LeftClickOn(GetTimerSettingIncrementButton());
  auto textfield_text_after_increment = timer_textfield->GetText();
  ASSERT_NE(textfield_text_before_increment, textfield_text_after_increment);
  EXPECT_EQ(base::StrCat({textfield_text_after_increment, u" min"}),
            pod->sub_label()->GetText());
}

// Tests that the toggle row sub label is only visible outside of a focus
// session.
TEST_F(FocusModeDetailedViewTest, SubLabelVisibility) {
  // Adjusting the focus duration should not make the sub label appear outside
  // of a focus session.
  EXPECT_FALSE(IsToggleRowSubLabelVisible());
  LeftClickOn(GetTimerSettingDecrementButton());
  EXPECT_FALSE(IsToggleRowSubLabelVisible());
  LeftClickOn(GetTimerSettingIncrementButton());
  EXPECT_FALSE(IsToggleRowSubLabelVisible());

  // The label should be visible inside of a focus session.
  auto* focus_mode_controller = FocusModeController::Get();
  focus_mode_controller->ToggleFocusMode();
  EXPECT_TRUE(focus_mode_controller->in_focus_session());
  CreateFakeFocusModeDetailedView();
  EXPECT_TRUE(IsToggleRowSubLabelVisible());

  focus_mode_controller->ToggleFocusMode();
  EXPECT_FALSE(IsToggleRowSubLabelVisible());
}

// Tests that when the user select a task or mark a selected task as completed,
// the task view container will shrink or expand.
TEST_F(FocusModeDetailedViewTest, ExpandOrShrinkTaskViewContainer) {
  auto* task_container_view = GetTaskContainerView();
  auto* task_view = GetTaskView();
  auto* chip_carousel = task_view->chip_carousel_for_testing();
  EXPECT_TRUE(chip_carousel->HasTasks());
  EXPECT_TRUE(chip_carousel->GetVisible());

  auto* complete_button = task_view->complete_button_for_testing();
  // `complete_button` is invisible before we select a task title.
  EXPECT_FALSE(complete_button->GetVisible());
  const int old_height_before_shrink = task_container_view->bounds().height();

  // 1. Shrink the `task_container_view`.
  task_view->CommitTextfieldContents(u"my task title");
  AdvanceClock(base::Milliseconds(10));
  views::test::RunScheduledLayout(task_container_view);
  EXPECT_TRUE(complete_button->GetVisible());
  EXPECT_FALSE(chip_carousel->GetVisible());
  EXPECT_GT(old_height_before_shrink, task_container_view->bounds().height());

  // 2. Expand the `task_container_view`.
  LeftClickOn(complete_button);
  AdvanceClock(kStartAnimationDelay);
  views::test::RunScheduledLayout(task_container_view);
  EXPECT_EQ(old_height_before_shrink, task_container_view->bounds().height());
}

// Test that after adding or updating a task, the focus should be either on the
// complete button or the deselect button. Note that this test should enable
// ChromeVox.
TEST_F(FocusModeDetailedViewTest, A11yFocusAfterTaskTextfield) {
  Shell::Get()->accessibility_controller()->spoken_feedback().SetEnabled(true);

  auto* task_view = GetTaskView();
  auto* task_textfield = task_view->GetTaskTextfieldForTesting();
  auto* complete_button = task_view->complete_button_for_testing();
  EXPECT_FALSE(complete_button->HasFocus());

  // 1. Add a new task. After the commit, the focus will be on the radio button.
  LeftClickOn(task_textfield);
  EXPECT_TRUE(task_textfield->HasFocus());
  task_textfield->SetText(u"task title1");
  EXPECT_TRUE(task_textfield->IsActive());

  PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
  task_environment()->RunUntilIdle();
  EXPECT_TRUE(complete_button->HasFocus());

  // 2. Tab to the textfield, which will let us edit the existing textfield
  // directly. Then, pressing the `Enter` key will bring the focus on the radio
  // button.
  PressAndReleaseKey(ui::KeyboardCode::VKEY_TAB);
  EXPECT_TRUE(task_textfield->HasFocus());
  EXPECT_TRUE(task_textfield->IsActive());
  task_textfield->SetText(u"task title2");
  PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
  task_environment()->RunUntilIdle();
  EXPECT_TRUE(complete_button->HasFocus());

  // 3. Tab to the textfield, which will let us edit the existing textfield
  // directly. Then, pressing the `TAB` key will bring the focus on the deselect
  // button.
  PressAndReleaseKey(ui::KeyboardCode::VKEY_TAB);
  EXPECT_TRUE(task_textfield->HasFocus());
  EXPECT_TRUE(task_textfield->IsActive());
  task_textfield->SetText(u"task title3");
  PressAndReleaseKey(ui::KeyboardCode::VKEY_TAB);
  task_environment()->RunUntilIdle();
  EXPECT_TRUE(task_view->deselect_button_for_testing()->HasFocus());
}

// Tests that the focus ring should be either on the textfield container or on
// the textfield depending on if there is a selected task.
TEST_F(FocusModeDetailedViewTest, RegularFocusRingCheckForTaskTextfield) {
  auto* task_container_view = GetTaskContainerView();
  auto* task_view = GetTaskView();
  auto* textfield_container = task_view->textfield_container_for_testing();
  auto* task_textfield = task_view->GetTaskTextfieldForTesting();
  auto* complete_button = task_view->complete_button_for_testing();
  auto* controller = FocusModeController::Get();

  // When there is no selected task, no focus ring will be painted before
  // clicking on the textfield.
  EXPECT_FALSE(task_textfield->HasFocus());
  auto* container_focus_ring = views::FocusRing::Get(textfield_container);
  auto* texfield_focus_ring = views::FocusRing::Get(task_textfield);
  EXPECT_FALSE(container_focus_ring->ShouldPaintForTesting());
  EXPECT_FALSE(texfield_focus_ring->ShouldPaintForTesting());

  // 1. Click the textfield and the focus ring should be painted on the
  // container instead of the textfield itself.
  LeftClickOn(task_textfield);
  EXPECT_TRUE(task_textfield->HasFocus());
  EXPECT_TRUE(container_focus_ring->ShouldPaintForTesting());
  EXPECT_FALSE(texfield_focus_ring->ShouldPaintForTesting());

  // 2. Set a selected task and tab to the textfield from the complete button.
  FocusModeTask task;
  task.task_id = {.list_id = "default", .id = "task1"};
  task.title = "task_name";
  task.updated = base::Time::Now();
  controller->SetSelectedTask(task);

  views::test::RunScheduledLayout(task_container_view);
  EXPECT_TRUE(complete_button->GetVisible());
  complete_button->RequestFocus();
  EXPECT_TRUE(complete_button->HasFocus());
  PressAndReleaseKey(ui::KeyboardCode::VKEY_TAB);

  // In the selected state, the focus ring should be on the textfield itself
  // instead of the container.
  EXPECT_FALSE(container_focus_ring->ShouldPaintForTesting());
  EXPECT_TRUE(texfield_focus_ring->ShouldPaintForTesting());
  EXPECT_TRUE(task_textfield->IsActive());
}

// Tests that tabbing to the timer decrease button after setting the time to 1
// does not cause a crash. Regression test for b/315358227.
TEST_F(FocusModeDetailedViewTest, TabToDisablingButton) {
  SystemTextfield* textfield = GetTimerSettingTextfield();
  LeftClickOn(textfield);
  ASSERT_TRUE(textfield->IsActive());
  PressAndReleaseKey(ui::KeyboardCode::VKEY_DELETE);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_1);
  ASSERT_EQ(u"1", textfield->GetText());
  PressAndReleaseKey(ui::KeyboardCode::VKEY_TAB);
  EXPECT_FALSE(GetTimerSettingDecrementButton()->GetEnabled());
}

// Tests that when clicking the `End` button during a focus session, the
// histogram will record the behavior.
TEST_F(FocusModeDetailedViewTest, CheckHistogramForToggleRowButton) {
  base::HistogramTester histogram_tester;

  auto* controller = FocusModeController::Get();
  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());

  auto* button = GetToggleRowButton();
  LeftClickOn(button);
  EXPECT_FALSE(controller->in_focus_session());
  histogram_tester.ExpectBucketCount(
      /*name=*/focus_mode_histogram_names::
          kToggleEndButtonDuringSessionHistogramName,
      /*sample=*/focus_mode_histogram_names::ToggleSource::kFocusPanel,
      /*expected_count=*/1);
}

// Tests that the "Until" end time labels update when the timezone updates.
// Regression test for b/319523086.
TEST_F(FocusModeDetailedViewTest, UpdateOnTimezoneChange) {
  ash::system::ScopedTimezoneSettings timezone1(u"GMT");
  auto* focus_mode_controller = FocusModeController::Get();
  EXPECT_EQ(focus_mode_util::GetFormattedEndTimeString(
                focus_mode_controller->session_duration() + base::Time::Now()),
            GetEndTimeLabel()->GetText());

  // Change the timezone without closing or opening the detailed view. The end
  // time label should update.
  ash::system::ScopedTimezoneSettings timezone2(u"GMT+3");
  EXPECT_EQ(focus_mode_util::GetFormattedEndTimeString(
                focus_mode_controller->session_duration() + base::Time::Now()),
            GetEndTimeLabel()->GetText());

  // Start a focus session to test the toggle row subtext.
  focus_mode_controller->ToggleFocusMode();
  EXPECT_TRUE(focus_mode_controller->in_focus_session());
  CreateFakeFocusModeDetailedView();
  EXPECT_EQ(focus_mode_util::GetFormattedEndTimeString(
                focus_mode_controller->GetActualEndTime()),
            GetToggleRowSubLabel()->GetText());

  ash::system::ScopedTimezoneSettings timezone3(u"GMT+6");
  EXPECT_EQ(focus_mode_util::GetFormattedEndTimeString(
                focus_mode_controller->GetActualEndTime()),
            GetToggleRowSubLabel()->GetText());
}

// Tests that starting a new focus session while the timer textfield is still
// active and the text in the textfield is different from the previous saved
// session duration.
TEST_F(FocusModeDetailedViewTest, StartSessionWithActiveTimerTextfield) {
  auto* controller = FocusModeController::Get();
  EXPECT_FALSE(controller->in_focus_session());

  // Click the timer textfield and type a digit `1` into it.
  SystemTextfield* timer_textfield = GetTimerSettingTextfield();
  LeftClickOn(timer_textfield);
  EXPECT_TRUE(timer_textfield->IsActive());
  PressAndReleaseKey(ui::KeyboardCode::VKEY_DELETE);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_1);
  EXPECT_EQ(u"1", timer_textfield->GetText());

  // Verify after starting a new focus session, the session duration is the one
  // we just typed and we have saved it.
  LeftClickOn(GetToggleRowButton());
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_EQ(base::Minutes(1), controller->session_duration());
  EXPECT_EQ(
      base::Minutes(1),
      Shell::Get()->session_controller()->GetActivePrefService()->GetTimeDelta(
          prefs::kFocusModeSessionDuration));
}

// Tests that when scrolling on the chip carousel, the scroll view of the focus
// panel will be scrolled.
TEST_F(FocusModeDetailedViewTest, ChipsNotAcceptVerticalScrollGesture) {
  auto* chip_carousel = GetTaskView()->chip_carousel_for_testing();
  EXPECT_TRUE(chip_carousel->HasTasks());
  EXPECT_TRUE(chip_carousel->GetVisible());

  // Before scrolling the focus panel, the visible rect for the scroll view
  // should be in an initialized state.
  auto* scroll_view = GetScrollView();
  EXPECT_EQ(scroll_view->GetVisibleRect().y(), 0);

  const auto center_point = chip_carousel->GetBoundsInScreen().CenterPoint();
  const gfx::Vector2d offset(0, -100);
  // Scroll down the chip carousel with `offset`.
  GetEventGenerator()->GestureScrollSequence(
      center_point, center_point + offset, base::Milliseconds(300), 3);

  // After scrolling up the focus panel, the visible rect for the scroll view
  // has been changed.
  EXPECT_GT(scroll_view->GetVisibleRect().y(), 0);
}

TEST_F(FocusModeDetailedViewTest,
       HoverHighlightViewAccessibleDefaultActionVerb) {
  auto* hover_highlight_view = GetToggleView();
  auto* right_view = GetToggleRowButton();
  ui::AXNodeData data;

  ASSERT_TRUE(hover_highlight_view);
  ASSERT_TRUE(right_view);
  ASSERT_TRUE(std::string(right_view->GetClassName()).find("Button") !=
              std::string::npos);

  hover_highlight_view->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetDefaultActionVerb(), ax::mojom::DefaultActionVerb::kClick);

  hover_highlight_view->SetRightViewVisible(false);
  data = ui::AXNodeData();
  hover_highlight_view->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetDefaultActionVerb(), ax::mojom::DefaultActionVerb::kPress);

  hover_highlight_view->SetRightViewVisible(true);
  data = ui::AXNodeData();
  hover_highlight_view->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetDefaultActionVerb(), ax::mojom::DefaultActionVerb::kClick);

  hover_highlight_view->Reset();
  data = ui::AXNodeData();
  hover_highlight_view->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetDefaultActionVerb(), ax::mojom::DefaultActionVerb::kPress);
}

class FocusModeDetailedViewWithLotsOfTasksTest
    : public FocusModeDetailedViewTest {
 public:
  void CreateFakeTasks(api::FakeTasksClient& tasks_client) override {
    // Creates five lists, each containing two tasks.
    for (int list_no = 0; list_no != 5; ++list_no) {
      std::string list_id = base::StringPrintf("L%d", list_no);
      AddFakeTaskList(tasks_client, list_id);

      for (int task_no = 0; task_no != 2; ++task_no) {
        std::string task_id = base::StringPrintf("T%d.%d", list_no, task_no);
        AddFakeTask(tasks_client, list_id, task_id, "Title");
      }
    }
  }
};

// Tests that the carousel only shows a limited number of tasks.
TEST_F(FocusModeDetailedViewWithLotsOfTasksTest, LimitTasks) {
  auto* task_view = GetTaskView();
  auto* chip_carousel = task_view->chip_carousel_for_testing();
  EXPECT_TRUE(chip_carousel->HasTasks());
  EXPECT_EQ(chip_carousel->GetTaskCountForTesting(), 5);
  EXPECT_TRUE(chip_carousel->GetVisible());
}

}  // namespace ash