chromium/chrome/browser/ui/ash/focus_mode/focus_mode_browsertest.cc

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

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/style/pill_button.h"
#include "ash/style/system_textfield.h"
#include "ash/system/focus_mode/focus_mode_controller.h"
#include "ash/system/focus_mode/focus_mode_detailed_view.h"
#include "ash/system/focus_mode/focus_mode_histogram_names.h"
#include "ash/system/focus_mode/focus_mode_util.h"
#include "ash/system/unified/quick_settings_view.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_test_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/ash/accessibility/spoken_feedback_browsertest.h"
#include "chrome/browser/ui/ash/web_view/ash_web_view_impl.h"
#include "chrome/test/base/ash/util/ash_test_util.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/test/browser_test.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

constexpr char kFocusModeMediaWidgetName[] = "FocusModeMediaWidget";

views::Widget* FindMediaWidgetFromWindow(aura::Window* search_root) {
  if (views::Widget* const widget =
          views::Widget::GetWidgetForNativeWindow(search_root);
      widget && widget->GetName() == kFocusModeMediaWidgetName) {
    return widget;
  }

  // Keep searching in children.
  for (aura::Window* const child : search_root->children()) {
    if (views::Widget* const found = FindMediaWidgetFromWindow(child)) {
      return found;
    }
  }

  return nullptr;
}

views::Widget* FindMediaWidget() {
  return FindMediaWidgetFromWindow(Shell::GetContainer(
      Shell::GetPrimaryRootWindow(), kShellWindowId_OverlayContainer));
}

void SimulatePlaybackState(bool is_playing) {
  media_session::mojom::MediaSessionInfoPtr session_info(
      media_session::mojom::MediaSessionInfo::New());

  session_info->state =
      media_session::mojom::MediaSessionInfo::SessionState::kActive;
  session_info->playback_state =
      is_playing ? media_session::mojom::MediaPlaybackState::kPlaying
                 : media_session::mojom::MediaPlaybackState::kPaused;

  FocusModeController::Get()
      ->focus_mode_sounds_controller()
      ->MediaSessionInfoChanged(std::move(session_info));
}

QuickSettingsView* OpenQuickSettings() {
  UnifiedSystemTray* system_tray = Shell::GetPrimaryRootWindowController()
                                       ->shelf()
                                       ->GetStatusAreaWidget()
                                       ->unified_system_tray();
  system_tray->ShowBubble();
  return system_tray->bubble()->quick_settings_view();
}

void ClickOnFocusTile(QuickSettingsView* panel) {
  views::Button* tile = views::AsViewClass<views::Button>(
      panel->GetViewByID(VIEW_ID_FEATURE_TILE_FOCUS_MODE));
  tile->button_controller()->NotifyClick();
  base::RunLoop().RunUntilIdle();
}

SystemTextfield* GetTimerTextfield(QuickSettingsView* quick_settings) {
  FocusModeDetailedView* detailed_view =
      quick_settings->GetDetailedViewForTest<FocusModeDetailedView>();
  EXPECT_TRUE(detailed_view);
  return views::AsViewClass<SystemTextfield>(detailed_view->GetViewByID(
      FocusModeDetailedView::ViewId::kTimerTextfield));
}

PillButton* GetToggleFocusButton(QuickSettingsView* quick_settings) {
  FocusModeDetailedView* detailed_view =
      quick_settings->GetDetailedViewForTest<FocusModeDetailedView>();
  EXPECT_TRUE(detailed_view);
  return views::AsViewClass<PillButton>(detailed_view->GetViewByID(
      FocusModeDetailedView::ViewId::kToggleFocusButton));
}

}  // namespace

class FocusModeBrowserTest : public InProcessBrowserTest {
 public:
  FocusModeBrowserTest() {
    feature_list_.InitWithFeatures(
        {features::kFocusMode, features::kFocusModeYTM}, {});
  }
  ~FocusModeBrowserTest() override = default;
  FocusModeBrowserTest(const FocusModeBrowserTest&) = delete;
  FocusModeBrowserTest& operator=(const FocusModeBrowserTest&) = delete;

 protected:
  base::test::ScopedFeatureList feature_list_;
};

// Tests basic create/close media widget functionality.
IN_PROC_BROWSER_TEST_F(FocusModeBrowserTest, MediaWidget) {
  auto* controller = FocusModeController::Get();
  EXPECT_FALSE(controller->in_focus_session());

  // Toggle on focus mode. Verify that there is no media widget since there is
  // no selected playlist.
  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());
  auto* sounds_controller = controller->focus_mode_sounds_controller();
  EXPECT_TRUE(sounds_controller->selected_playlist().empty());
  EXPECT_FALSE(FindMediaWidget());

  // Select a playlist with a type and verify that a media widget is created.
  focus_mode_util::SelectedPlaylist selected_playlist;
  selected_playlist.id = "id0";
  selected_playlist.type = focus_mode_util::SoundType::kSoundscape;
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_FALSE(sounds_controller->selected_playlist().empty());
  EXPECT_TRUE(FindMediaWidget());

  // Swap playlists, then verify that the media widget still exists.
  selected_playlist.id = "id1";
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_FALSE(sounds_controller->selected_playlist().empty());
  EXPECT_TRUE(FindMediaWidget());

  // The media widget should still exist when the ending moment is triggered.
  controller->TriggerEndingMomentImmediately();
  EXPECT_TRUE(controller->in_ending_moment());
  EXPECT_TRUE(FindMediaWidget());

  // If the user extends the time during the ending moment, the media widget
  // should be recreated.
  controller->ExtendSessionDuration();
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_TRUE(FindMediaWidget());

  // Toggling off focus mode should close the media widget.
  controller->ToggleFocusMode();
  EXPECT_FALSE(controller->in_focus_session());
  EXPECT_FALSE(FindMediaWidget());

  // Toggling on focus mode with a selected playlist should trigger creating a
  // media widget.
  EXPECT_FALSE(sounds_controller->selected_playlist().empty());
  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_TRUE(FindMediaWidget());
}

// Tests that the ending moment will pause the playlist without closing the
// media widget.
IN_PROC_BROWSER_TEST_F(FocusModeBrowserTest, PauseMusicDuringEndingMoment) {
  auto* controller = FocusModeController::Get();
  EXPECT_FALSE(controller->in_focus_session());

  // Toggle on focus mode. Verify that there is no media widget since there is
  // no selected playlist.
  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());
  auto* sounds_controller = controller->focus_mode_sounds_controller();
  sounds_controller->set_simulate_playback_for_testing();

  // Case 1. If the music is paused by the ending moment, when extending the
  // session, it should be resumed. Select a playlist with a type and verify
  // that a media widget is created.
  focus_mode_util::SelectedPlaylist selected_playlist;
  selected_playlist.id = "id0";
  selected_playlist.type = focus_mode_util::SoundType::kSoundscape;
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_TRUE(FindMediaWidget());
  // Simulate the playlist is playing.
  SimulatePlaybackState(/*is_playing=*/true);
  EXPECT_EQ(focus_mode_util::SoundState::kPlaying,
            sounds_controller->selected_playlist().state);

  controller->TriggerEndingMomentImmediately();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(controller->in_ending_moment());
  EXPECT_TRUE(FindMediaWidget());
  EXPECT_EQ(focus_mode_util::SoundState::kPaused,
            sounds_controller->selected_playlist().state);

  // Extending the session will resume the playlist.
  controller->ExtendSessionDuration();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_EQ(focus_mode_util::SoundState::kPlaying,
            sounds_controller->selected_playlist().state);

  // Case 2. If the user paused the playlist before ending moment, after
  // extending the session, the playlist should still in paused state.
  SimulatePlaybackState(/*is_playing=*/false);
  EXPECT_EQ(focus_mode_util::SoundState::kPaused,
            sounds_controller->selected_playlist().state);

  controller->TriggerEndingMomentImmediately();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(controller->in_ending_moment());
  EXPECT_TRUE(FindMediaWidget());

  controller->ExtendSessionDuration();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_EQ(focus_mode_util::SoundState::kPaused,
            sounds_controller->selected_playlist().state);

  // Case 3. If the user selected another playlist during the ending moment,
  // after extending the session, it should be the new playlist is playing.
  controller->TriggerEndingMomentImmediately();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(controller->in_ending_moment());
  const auto old_playlist_id = sounds_controller->selected_playlist().id;

  selected_playlist.id = "id1";
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_EQ(focus_mode_util::SoundState::kSelected,
            sounds_controller->selected_playlist().state);

  controller->ExtendSessionDuration();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_TRUE(FindMediaWidget());
  EXPECT_NE(old_playlist_id, sounds_controller->selected_playlist().id);
}

IN_PROC_BROWSER_TEST_F(FocusModeBrowserTest,
                       CheckSoundsPlayedDuringSessionHistogram) {
  base::HistogramTester histogram_tester;

  auto* controller = FocusModeController::Get();
  auto* sounds_controller = controller->focus_mode_sounds_controller();

  // 1. No playlist playing during the session.
  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_TRUE(sounds_controller->selected_playlist().empty());

  controller->ToggleFocusMode();
  EXPECT_FALSE(controller->in_focus_session());
  histogram_tester.ExpectBucketCount(
      /*name=*/focus_mode_histogram_names::kPlaylistTypesSelectedDuringSession,
      /*sample=*/
      focus_mode_histogram_names::PlaylistTypesSelectedDuringFocusSessionType::
          kNone,
      /*expected_count=*/1);

  // 2. Only the type of soundscape playlist playing during the session.
  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());

  focus_mode_util::SelectedPlaylist selected_playlist;
  selected_playlist.id = "id0";
  selected_playlist.type = focus_mode_util::SoundType::kSoundscape;
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_FALSE(sounds_controller->selected_playlist().empty());
  EXPECT_TRUE(FindMediaWidget());

  controller->ToggleFocusMode();
  EXPECT_FALSE(controller->in_focus_session());
  histogram_tester.ExpectBucketCount(
      /*name=*/focus_mode_histogram_names::kPlaylistTypesSelectedDuringSession,
      /*sample=*/
      focus_mode_histogram_names::PlaylistTypesSelectedDuringFocusSessionType::
          kSoundscapes,
      /*expected_count=*/1);

  // 3. Only the type of YouTube Music playlist playing during the session.
  selected_playlist.id = "id1";
  selected_playlist.type = focus_mode_util::SoundType::kYouTubeMusic;
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_FALSE(sounds_controller->selected_playlist().empty());

  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());

  controller->ToggleFocusMode();
  EXPECT_FALSE(controller->in_focus_session());
  histogram_tester.ExpectBucketCount(
      /*name=*/focus_mode_histogram_names::kPlaylistTypesSelectedDuringSession,
      /*sample=*/
      focus_mode_histogram_names::PlaylistTypesSelectedDuringFocusSessionType::
          kYouTubeMusic,
      /*expected_count=*/1);

  // 4. The two types of playlists playing during the session.
  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_FALSE(sounds_controller->selected_playlist().empty());

  selected_playlist.id = "id3";
  selected_playlist.type = focus_mode_util::SoundType::kSoundscape;
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_EQ(sounds_controller->selected_playlist().type,
            focus_mode_util::SoundType::kSoundscape);

  controller->ToggleFocusMode();
  EXPECT_FALSE(controller->in_focus_session());
  histogram_tester.ExpectBucketCount(
      /*name=*/focus_mode_histogram_names::kPlaylistTypesSelectedDuringSession,
      /*sample=*/
      focus_mode_histogram_names::PlaylistTypesSelectedDuringFocusSessionType::
          kYouTubeMusicAndSoundscapes,
      /*expected_count=*/1);
}

IN_PROC_BROWSER_TEST_F(FocusModeBrowserTest,
                       CheckPlaylistsPlayedDuringSessionHistogram) {
  base::HistogramTester histogram_tester;

  auto* controller = FocusModeController::Get();
  auto* sounds_controller = controller->focus_mode_sounds_controller();

  // 1. No playlist played during the session.
  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());
  EXPECT_TRUE(sounds_controller->selected_playlist().empty());

  controller->ToggleFocusMode();
  EXPECT_FALSE(controller->in_focus_session());
  histogram_tester.ExpectBucketCount(
      /*name=*/focus_mode_histogram_names::kCountPlaylistsPlayedDuringSession,
      /*sample=*/0, /*expected_count=*/1);

  // 2. Two playlists played during the session.
  focus_mode_util::SelectedPlaylist selected_playlist;
  selected_playlist.id = "id0";
  selected_playlist.type = focus_mode_util::SoundType::kYouTubeMusic;
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_FALSE(sounds_controller->selected_playlist().empty());

  controller->ToggleFocusMode();
  EXPECT_TRUE(controller->in_focus_session());

  selected_playlist.id = "id1";
  selected_playlist.type = focus_mode_util::SoundType::kSoundscape;
  sounds_controller->TogglePlaylist(selected_playlist);

  // De-select the playlist and the histogram will not record it.
  sounds_controller->TogglePlaylist(sounds_controller->selected_playlist());

  controller->ToggleFocusMode();
  EXPECT_FALSE(controller->in_focus_session());
  histogram_tester.ExpectBucketCount(
      /*name=*/focus_mode_histogram_names::kCountPlaylistsPlayedDuringSession,
      /*sample=*/2, /*expected_count=*/1);
}

// Tests that the source title shown in the media controls for the associated
// Focus Mode media widget is overridden and not empty.
IN_PROC_BROWSER_TEST_F(FocusModeBrowserTest, MediaSourceTitle) {
  // Toggle on focus mode.
  auto* focus_mode_controller = FocusModeController::Get();
  focus_mode_controller->ToggleFocusMode();
  EXPECT_TRUE(focus_mode_controller->in_focus_session());

  // Select a playlist and verify that a media widget is created.
  focus_mode_util::SelectedPlaylist selected_playlist;
  selected_playlist.id = "id0";
  selected_playlist.title = "Playlist Title";
  selected_playlist.type = focus_mode_util::SoundType::kYouTubeMusic;
  auto* sounds_controller =
      focus_mode_controller->focus_mode_sounds_controller();
  sounds_controller->TogglePlaylist(selected_playlist);
  EXPECT_FALSE(sounds_controller->selected_playlist().empty());

  auto* widget = FindMediaWidget();
  EXPECT_TRUE(widget);

  // Verify that there is a source title.
  AshWebViewImpl* web_view_impl =
      static_cast<AshWebViewImpl*>(widget->GetContentsView());
  std::string source_title =
      web_view_impl->GetTitleForMediaControls(web_view_impl->web_contents());
  EXPECT_FALSE(source_title.empty());
}

// Tests that during the overvide mode, clicking on the focus panel will not end
// the overview mode.
IN_PROC_BROWSER_TEST_F(FocusModeBrowserTest, ClickOnFocusPanelInOverviewMode) {
  // Enter overview mode and open the focus panel.
  ToggleOverview();
  WaitForOverviewEnterAnimation();

  auto* quick_settings = OpenQuickSettings();
  ClickOnFocusTile(quick_settings);
  EXPECT_TRUE(ash::OverviewController::Get()->InOverviewSession());

  // 1. Click the timer textfield on the focus panel and stay in overview mode.
  auto* timer_textfield = GetTimerTextfield(quick_settings);
  EXPECT_TRUE(timer_textfield->GetVisible());

  test::Click(timer_textfield);
  EXPECT_TRUE(timer_textfield->HasFocus());
  EXPECT_TRUE(ash::OverviewController::Get()->InOverviewSession());

  // 2. Click the `Start` button to start a focus session and stay in overview
  // mode.
  auto* toggle_button = GetToggleFocusButton(quick_settings);
  EXPECT_TRUE(toggle_button->GetVisible());

  auto* focus_mode_controller = ash::FocusModeController::Get();
  EXPECT_FALSE(focus_mode_controller->in_focus_session());
  test::Click(toggle_button);
  EXPECT_TRUE(focus_mode_controller->in_focus_session());
  EXPECT_TRUE(ash::OverviewController::Get()->InOverviewSession());
}

class FocusModeSpokenFeedbackTest : public LoggedInSpokenFeedbackTest {
 public:
  FocusModeSpokenFeedbackTest() = default;
  FocusModeSpokenFeedbackTest(const FocusModeSpokenFeedbackTest&) = delete;
  FocusModeSpokenFeedbackTest& operator=(const FocusModeSpokenFeedbackTest&) =
      delete;
  ~FocusModeSpokenFeedbackTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_{features::kFocusMode};
};

// Tests that when using `Search + Left/Right Arrow` key to navigate on the
// focus panel, the user update the timer texfield and start a focus session,
// which should also update the session duration for the controller.
IN_PROC_BROWSER_TEST_F(FocusModeSpokenFeedbackTest,
                       AfterA11yFocusRingOnTimerTextfield) {
  EnableChromeVox();

  // Set a session duration with 25 min and let the timer textfield gain the
  // focus.
  sm_.Call([] {
    auto* focus_mode_controller = FocusModeController::Get();
    focus_mode_controller->SetInactiveSessionDuration(base::Minutes(25));
    EXPECT_EQ(base::Minutes(25), focus_mode_controller->session_duration());

    // Open the focus panel.
    auto* quick_settings = OpenQuickSettings();
    ClickOnFocusTile(quick_settings);
    auto* timer_textfield = GetTimerTextfield(quick_settings);
    timer_textfield->RequestFocus();
  });
  sm_.ExpectSpeechPattern("Edit timer*");

  // Update the session duration from 25 min to 250 min by appending a `0` key
  // to the end of the text.
  sm_.Call([this] { SendKeyPress(ui::VKEY_0); });

  // Press `Search + Left Arrow` keys to the `Start Focus` button..
  sm_.Call([this] {
    SendKeyPressWithSearch(ui::VKEY_LEFT);
    SendKeyPressWithSearch(ui::VKEY_LEFT);
  });
  sm_.ExpectSpeechPattern("Start Focus*");

  // Press `Enter` key to start a focus session and Verify the session
  // duration..
  sm_.Call([this] {
    SendKeyPress(ui::VKEY_RETURN);
    EXPECT_EQ(base::Minutes(250),
              FocusModeController::Get()->session_duration());
  });
  sm_.Replay();
}

}  // namespace ash