chromium/chrome/browser/ui/views/tabs/tab_scrubber_chromeos_browsertest.cc

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

#include "chrome/browser/ui/views/tabs/tab_scrubber_chromeos.h"

#include <memory>
#include <utility>

#include "ash/constants/ash_switches.h"
#include "ash/display/event_transformation_handler.h"
#include "ash/shell.h"
#include "base/command_line.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chromeos/crosapi/cpp/crosapi_constants.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/test/shell_surface_builder.h"
#include "components/exo/wm_helper.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/aura/window.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/wm/core/window_util.h"

namespace {

constexpr int kScrubbingGestureFingerCount = 3;

// Waits until the immersive mode reveal ends, and therefore the top view of
// the browser is no longer visible.
class ImmersiveRevealEndedWaiter : public ImmersiveModeController::Observer {
 public:
  explicit ImmersiveRevealEndedWaiter(
      ImmersiveModeController* immersive_controller)
      : immersive_controller_(immersive_controller) {
    immersive_controller_->AddObserver(this);
  }

  ImmersiveRevealEndedWaiter(const ImmersiveRevealEndedWaiter&) = delete;
  ImmersiveRevealEndedWaiter& operator=(const ImmersiveRevealEndedWaiter&) =
      delete;

  ~ImmersiveRevealEndedWaiter() override {
    if (immersive_controller_)
      immersive_controller_->RemoveObserver(this);
  }

  void Wait() {
    if (!immersive_controller_ || !immersive_controller_->IsRevealed())
      return;

    base::RunLoop run_loop;
    quit_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }

 private:
  void MaybeQuitRunLoop() {
    if (!quit_closure_.is_null())
      std::move(quit_closure_).Run();
  }

  // ImmersiveModeController::Observer:
  void OnImmersiveRevealEnded() override { MaybeQuitRunLoop(); }

  void OnImmersiveModeControllerDestroyed() override {
    MaybeQuitRunLoop();
    immersive_controller_->RemoveObserver(this);
    immersive_controller_ = nullptr;
  }

  raw_ptr<ImmersiveModeController> immersive_controller_;
  base::OnceClosure quit_closure_;
};

}  // namespace

class TabScrubberChromeOSTest : public InProcessBrowserTest,
                                public TabStripModelObserver {
 public:
  TabScrubberChromeOSTest() = default;

  TabScrubberChromeOSTest(const TabScrubberChromeOSTest&) = delete;
  TabScrubberChromeOSTest& operator=(const TabScrubberChromeOSTest&) = delete;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(ash::switches::kNaturalScrollDefault);
  }

  void SetUpOnMainThread() override {
    TabScrubberChromeOS::GetInstance()->use_default_activation_delay_ = false;
    // Disable external monitor scaling of coordinates.
    ash::Shell* shell = ash::Shell::Get();
    shell->event_transformation_handler()->set_transformation_mode(
        ash::EventTransformationHandler::TRANSFORM_NONE);

    wm_helper_ = std::make_unique<exo::WMHelper>();
  }

  void TearDownOnMainThread() override {
    wm_helper_.reset();

    browser()->tab_strip_model()->RemoveObserver(this);
  }

  TabStrip* GetTabStrip(Browser* browser) {
    aura::Window* window = browser->window()->GetNativeWindow();
    // This test depends on TabStrip impl.
    TabStrip* tab_strip =
        BrowserView::GetBrowserViewForNativeWindow(window)->tabstrip();
    DCHECK(tab_strip);
    return tab_strip;
  }

  int GetStartX(Browser* browser,
                int index,
                TabScrubberChromeOS::Direction direction) {
    return TabScrubberChromeOS::GetStartPoint(GetTabStrip(browser), index,
                                              direction)
        .x();
  }

  int GetTabCenter(Browser* browser, int index) {
    return GetTabStrip(browser)
        ->tab_at(index)
        ->GetMirroredBounds()
        .CenterPoint()
        .x();
  }

  // The simulated scroll event's offsets are calculated in the tests rather
  // than generated by the real event system. For the offsets calculation to be
  // correct in an RTL layout, we must invert the direction since it is
  // calculated here in the tests based on the indices and they're inverted in
  // RTL layouts:
  // Tab indices in an English layout : 0 - 1 - 2 - 3 - 4.
  // Tab indices in an Arabic layout  : 4 - 3 - 2 - 1 - 0.
  TabScrubberChromeOS::Direction InvertDirectionIfNeeded(
      TabScrubberChromeOS::Direction direction) {
    if (base::i18n::IsRTL()) {
      return direction == TabScrubberChromeOS::LEFT ? TabScrubberChromeOS::RIGHT
                                                    : TabScrubberChromeOS::LEFT;
    }

    return direction;
  }

  // Sends one scroll event synchronously without initial or final
  // fling events.
  void SendScrubEvent(Browser* browser, int index) {
    auto event_generator = CreateEventGenerator(browser);
    int active_index = browser->tab_strip_model()->active_index();
    TabScrubberChromeOS::Direction direction = index < active_index
                                                   ? TabScrubberChromeOS::LEFT
                                                   : TabScrubberChromeOS::RIGHT;

    direction = InvertDirectionIfNeeded(direction);

    int offset = GetTabCenter(browser, index) -
                 GetStartX(browser, active_index, direction);
    ui::ScrollEvent scroll_event(ui::EventType::kScroll, gfx::Point(0, 0),
                                 ui::EventTimeForNow(), 0, offset, 0, offset, 0,
                                 kScrubbingGestureFingerCount);
    event_generator->Dispatch(&scroll_event);
  }

  enum ScrubType {
    EACH_TAB,
    SKIP_TABS,
    REPEAT_TABS,
  };

  // Sends asynchronous events and waits for tab at |index| to become
  // active.
  void Scrub(Browser* browser, int index, ScrubType scrub_type) {
    auto event_generator = CreateEventGenerator(browser);
    activation_order_.clear();
    int active_index = browser->tab_strip_model()->active_index();
    ASSERT_NE(index, active_index);
    ASSERT_TRUE(scrub_type != SKIP_TABS || ((index - active_index) % 2) == 0);
    TabScrubberChromeOS::Direction direction;
    int increment;
    if (index < active_index) {
      direction = TabScrubberChromeOS::LEFT;
      increment = -1;
    } else {
      direction = TabScrubberChromeOS::RIGHT;
      increment = 1;
    }

    direction = InvertDirectionIfNeeded(direction);

    if (scrub_type == SKIP_TABS)
      increment *= 2;
    browser->tab_strip_model()->AddObserver(this);
    ScrollGenerator scroll_generator(event_generator.get());
    int last = GetStartX(browser, active_index, direction);
    for (int i = active_index + increment; i != (index + increment);
         i += increment) {
      int tab_center = GetTabCenter(browser, i);
      scroll_generator.GenerateScroll(tab_center - last);
      last = GetStartX(browser, i, direction);
      if (scrub_type == REPEAT_TABS) {
        scroll_generator.GenerateScroll(increment);
        last += increment;
      }
    }
    browser->tab_strip_model()->RemoveObserver(this);
  }

  // Sends events and waits for tab at |index| to become active
  // if it's different from the currently active tab.
  // If the active tab is expected to stay the same, send events
  // synchronously (as we don't have anything to wait for).
  void SendScrubSequence(Browser* browser, float x_offset, int index) {
    auto event_generator = CreateEventGenerator(browser);
    browser->tab_strip_model()->AddObserver(this);
    ScrollGenerator scroll_generator(event_generator.get());
    scroll_generator.GenerateScroll(x_offset);
    browser->tab_strip_model()->RemoveObserver(this);
  }

  // Sends alt-tab key press event to start the window cycle list.
  void StartCyclingWindows(Browser* browser) {
    auto event_generator = CreateEventGenerator(browser);
    // Views use VKEY_MENU for both left and right Alt keys.
    event_generator->PressKey(ui::VKEY_MENU, ui::EF_NONE);
    event_generator->PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_ALT_DOWN);
    event_generator->ReleaseKey(ui::KeyboardCode::VKEY_TAB, ui::EF_ALT_DOWN);
  }

  // Sends alt-tab key release event to start the window cycle list.
  void StopCyclingWindows(Browser* browser) {
    auto event_generator = CreateEventGenerator(browser);
    event_generator->ReleaseKey(ui::VKEY_MENU, ui::EF_NONE);
  }

  bool IsTabScrubberChromeOSEnabled() {
    return TabScrubberChromeOS::GetInstance()->GetEnabledForTesting();
  }

  void AddTabs(Browser* browser, int num_tabs) {
    TabStrip* tab_strip = GetTabStrip(browser);
    for (int i = 0; i < num_tabs; ++i)
      AddBlankTabAndShow(browser);
    ASSERT_EQ(num_tabs + 1, browser->tab_strip_model()->count());
    ASSERT_EQ(num_tabs, browser->tab_strip_model()->active_index());
    tab_strip->StopAnimating(true);
    ASSERT_FALSE(tab_strip->IsAnimating());
    // Perform any scheduled layouts so the tabstrip is in a steady state.
    BrowserView::GetBrowserViewForBrowser(browser)
        ->GetWidget()
        ->LayoutRootViewIfNecessary();
  }

  // TabStripModelObserver overrides.
  void OnTabStripModelChanged(
      TabStripModel* tab_strip_model,
      const TabStripModelChange& change,
      const TabStripSelectionChange& selection) override {
    if (tab_strip_model->empty() || !selection.active_tab_changed())
      return;

    ASSERT_TRUE(selection.new_model.active().has_value());
    activation_order_.push_back(selection.new_model.active().value());
  }

  std::unique_ptr<ui::test::EventGenerator> CreateEventGenerator(
      Browser* browser) {
    aura::Window* window = browser->window()->GetNativeWindow();
    aura::Window* root = window->GetRootWindow();
    return std::make_unique<ui::test::EventGenerator>(root, window);
  }
  // History of tab activation. Scrub() resets it.
  std::vector<size_t> activation_order_;

 protected:
  bool IsDelegatedToLacros(ui::ScrollEvent event) {
    return TabScrubberChromeOS::MaybeDelegateHandlingToLacros(&event);
  }

 private:
  // Used to generate a sequence of scrolls. Starts with a cancel, is followed
  // by any number of scrolls and finally a fling-start. After every event this
  // forces the TabScrubberChromeOS to complete any pending activation.
  class ScrollGenerator {
   public:
    explicit ScrollGenerator(ui::test::EventGenerator* event_generator)
        : event_generator_(event_generator) {
      ui::ScrollEvent fling_cancel(ui::EventType::kScrollFlingCancel,
                                   gfx::Point(), time_for_next_event_, 0, 0, 0,
                                   0, 0, kScrubbingGestureFingerCount);
      event_generator->Dispatch(&fling_cancel);
      if (TabScrubberChromeOS::GetInstance()->IsActivationPending())
        TabScrubberChromeOS::GetInstance()->FinishScrub(true);
    }

    ScrollGenerator(const ScrollGenerator&) = delete;
    ScrollGenerator& operator=(const ScrollGenerator&) = delete;

    ~ScrollGenerator() {
      ui::ScrollEvent fling_start(ui::EventType::kScrollFlingStart,
                                  gfx::Point(), time_for_next_event_, 0,
                                  last_x_offset_, 0, last_x_offset_, 0,
                                  kScrubbingGestureFingerCount);
      event_generator_->Dispatch(&fling_start);
      if (TabScrubberChromeOS::GetInstance()->IsActivationPending())
        TabScrubberChromeOS::GetInstance()->FinishScrub(true);
    }

    void GenerateScroll(int x_offset) {
      time_for_next_event_ += base::Milliseconds(100);
      ui::ScrollEvent scroll(ui::EventType::kScroll, gfx::Point(),
                             time_for_next_event_, 0, x_offset, 0, x_offset, 0,
                             kScrubbingGestureFingerCount);
      last_x_offset_ = x_offset;
      event_generator_->Dispatch(&scroll);
      if (TabScrubberChromeOS::GetInstance()->IsActivationPending())
        TabScrubberChromeOS::GetInstance()->FinishScrub(true);
    }

    raw_ptr<ui::test::EventGenerator> event_generator_;
    base::TimeTicks time_for_next_event_ = ui::EventTimeForNow();
    int last_x_offset_ = 0;
  };

  std::unique_ptr<exo::WMHelper> wm_helper_;
};

// Swipe a single tab in each direction.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, Single) {
  AddTabs(browser(), 1);

  Scrub(browser(), 0, EACH_TAB);
  EXPECT_THAT(activation_order_, testing::ElementsAre(0));
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());

  Scrub(browser(), 1, EACH_TAB);
  EXPECT_THAT(activation_order_, testing::ElementsAre(1));
  EXPECT_EQ(1, browser()->tab_strip_model()->active_index());
}

// Swipe 4 tabs in each direction. Each of the tabs should become active.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, Multi) {
  AddTabs(browser(), 4);

  Scrub(browser(), 0, EACH_TAB);
  EXPECT_THAT(activation_order_, testing::ElementsAre(3, 2, 1, 0));
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());

  Scrub(browser(), 4, EACH_TAB);
  EXPECT_THAT(activation_order_, testing::ElementsAre(1, 2, 3, 4));
  EXPECT_EQ(4, browser()->tab_strip_model()->active_index());
}

IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, MultiBrowser) {
  AddTabs(browser(), 1);
  Scrub(browser(), 0, EACH_TAB);
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());

  Browser* browser2 = CreateBrowser(browser()->profile());
  browser2->window()->Activate();
  ASSERT_TRUE(browser2->window()->IsActive());
  ASSERT_FALSE(browser()->window()->IsActive());
  AddTabs(browser2, 1);

  Scrub(browser2, 0, EACH_TAB);
  EXPECT_EQ(0, browser2->tab_strip_model()->active_index());
}

// Tests that tab scrubbing works correctly for a full-screen browser.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, FullScreenBrowser) {
  // Initializes the position of mouse. Makes the mouse away from the tabstrip
  // to prevent any interference on this test.
  ASSERT_TRUE(ui_test_utils::SendMouseMoveSync(
      gfx::Point(0, browser()->window()->GetBounds().height())));
  AddTabs(browser(), 6);
  browser()->tab_strip_model()->ActivateTabAt(4);

  chrome::ToggleFullscreenMode(browser());
  BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow(
      browser()->window()->GetNativeWindow());
  ImmersiveModeController* immersive_controller =
      browser_view->immersive_mode_controller();
  EXPECT_TRUE(immersive_controller->IsEnabled());

  ImmersiveRevealEndedWaiter waiter(immersive_controller);
  waiter.Wait();

  EXPECT_FALSE(immersive_controller->IsRevealed());

  EXPECT_EQ(4, browser()->tab_strip_model()->active_index());
  Scrub(browser(), 0, EACH_TAB);
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());
  EXPECT_THAT(activation_order_, testing::ElementsAre(3, 2, 1, 0));
}

// Swipe 4 tabs in each direction with an extra swipe within each. The same
// 4 tabs should become active.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, Repeated) {
  AddTabs(browser(), 4);

  Scrub(browser(), 0, REPEAT_TABS);
  EXPECT_THAT(activation_order_, testing::ElementsAre(3, 2, 1, 0));
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());

  Scrub(browser(), 4, REPEAT_TABS);
  EXPECT_THAT(activation_order_, testing::ElementsAre(1, 2, 3, 4));
  EXPECT_EQ(4, browser()->tab_strip_model()->active_index());
}

// Confirm that we get the last tab made active when we skip tabs.
// These tests have 5 total tabs. We will only received scroll events
// on tabs 0, 2 and 4.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, Skipped) {
  AddTabs(browser(), 4);

  Scrub(browser(), 0, SKIP_TABS);
  EXPECT_THAT(activation_order_, testing::ElementsAre(2, 0));
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());

  Scrub(browser(), 4, SKIP_TABS);
  EXPECT_THAT(activation_order_, testing::ElementsAre(2, 4));
  EXPECT_EQ(4, browser()->tab_strip_model()->active_index());
}

// Confirm that nothing happens when the swipe is small.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, NoChange) {
  AddTabs(browser(), 1);

  SendScrubSequence(browser(), -1, 1);
  EXPECT_EQ(1, browser()->tab_strip_model()->active_index());

  SendScrubSequence(browser(), 1, 1);
  EXPECT_EQ(1, browser()->tab_strip_model()->active_index());
}

// Confirm that very large swipes go to the beginning and and of the tabstrip.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, Bounds) {
  AddTabs(browser(), 1);

  SendScrubSequence(browser(), -10000, 0);
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());

  SendScrubSequence(browser(), 10000, 1);
  EXPECT_EQ(1, browser()->tab_strip_model()->active_index());
}

IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, DeleteHighlighted) {
  AddTabs(browser(), 1);

  SendScrubEvent(browser(), 0);
  EXPECT_TRUE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
  browser()->tab_strip_model()->CloseWebContentsAt(0,
                                                   TabCloseTypes::CLOSE_NONE);
  EXPECT_FALSE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
}

// Delete the currently highlighted tab. Make sure the TabScrubberChromeOS is
// aware.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, DeleteBeforeHighlighted) {
  AddTabs(browser(), 2);

  SendScrubEvent(browser(), 1);
  EXPECT_TRUE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
  browser()->tab_strip_model()->CloseWebContentsAt(0,
                                                   TabCloseTypes::CLOSE_NONE);
  EXPECT_EQ(0, TabScrubberChromeOS::GetInstance()->highlighted_tab());
}

// Move the currently highlighted tab and confirm it gets tracked.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, MoveHighlighted) {
  AddTabs(browser(), 1);

  SendScrubEvent(browser(), 0);
  EXPECT_TRUE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
  browser()->tab_strip_model()->ToggleSelectionAt(0);
  browser()->tab_strip_model()->ToggleSelectionAt(1);
  browser()->tab_strip_model()->MoveSelectedTabsTo(1, std::nullopt);
  EXPECT_EQ(1, TabScrubberChromeOS::GetInstance()->highlighted_tab());
}

// Move a tab to before the highlighted one. Make sure that the highlighted tab
// index is updated correctly.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, MoveBefore) {
  AddTabs(browser(), 2);

  SendScrubEvent(browser(), 1);
  EXPECT_TRUE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
  browser()->tab_strip_model()->ToggleSelectionAt(0);
  browser()->tab_strip_model()->ToggleSelectionAt(2);
  browser()->tab_strip_model()->MoveSelectedTabsTo(2, std::nullopt);
  EXPECT_EQ(0, TabScrubberChromeOS::GetInstance()->highlighted_tab());
}

// Move a tab to after the highlighted one. Make sure that the highlighted tab
// index is updated correctly.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, MoveAfter) {
  AddTabs(browser(), 2);

  SendScrubEvent(browser(), 1);
  EXPECT_TRUE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
  browser()->tab_strip_model()->MoveSelectedTabsTo(0, std::nullopt);
  EXPECT_EQ(2, TabScrubberChromeOS::GetInstance()->highlighted_tab());
}

// Close the browser while an activation is pending.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, CloseBrowser) {
  AddTabs(browser(), 1);

  SendScrubEvent(browser(), 0);
  EXPECT_TRUE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
  browser()->window()->Close();
  EXPECT_FALSE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
}

// In an RTL layout, swipe 4 tabs in each direction. Each of the tabs should
// become active.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, RTLMulti) {
  base::i18n::SetICUDefaultLocale("ar");
  ASSERT_TRUE(base::i18n::IsRTL());

  AddTabs(browser(), 4);

  Scrub(browser(), 0, EACH_TAB);
  EXPECT_THAT(activation_order_, testing::ElementsAre(3, 2, 1, 0));
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());

  Scrub(browser(), 4, EACH_TAB);
  EXPECT_THAT(activation_order_, testing::ElementsAre(1, 2, 3, 4));
  EXPECT_EQ(4, browser()->tab_strip_model()->active_index());
}

// In an RTL layout, confirm that we get the last tab made active when we skip
// tabs. These tests have 5 total tabs. We will only received scroll events
// on tabs 0, 2 and 4.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, RTLSkipped) {
  base::i18n::SetICUDefaultLocale("ar");
  ASSERT_TRUE(base::i18n::IsRTL());

  AddTabs(browser(), 4);

  Scrub(browser(), 0, SKIP_TABS);
  EXPECT_THAT(activation_order_, testing::ElementsAre(2, 0));
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());

  Scrub(browser(), 4, SKIP_TABS);
  EXPECT_THAT(activation_order_, testing::ElementsAre(2, 4));
  EXPECT_EQ(4, browser()->tab_strip_model()->active_index());
}

// In an RTL layout, move a tab to before the highlighted one. Make sure that
// the highlighted tab index is updated correctly.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, RTLMoveBefore) {
  base::i18n::SetICUDefaultLocale("ar");
  ASSERT_TRUE(base::i18n::IsRTL());

  AddTabs(browser(), 2);

  SendScrubEvent(browser(), 1);
  EXPECT_TRUE(TabScrubberChromeOS::GetInstance()->IsActivationPending());
  browser()->tab_strip_model()->ToggleSelectionAt(0);
  browser()->tab_strip_model()->ToggleSelectionAt(2);
  browser()->tab_strip_model()->MoveSelectedTabsTo(2, std::nullopt);
  EXPECT_EQ(0, TabScrubberChromeOS::GetInstance()->highlighted_tab());
}

// If the window cycle list is open, the tab scrubber should be disabled.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, DisabledIfWindowCycleListOpen) {
  AddTabs(browser(), 4);

  // Create a second browser, but don't make it active.
  Browser* browser2 = CreateBrowser(browser()->profile());
  browser()->window()->Activate();
  ASSERT_FALSE(browser2->window()->IsActive());
  ASSERT_TRUE(browser()->window()->IsActive());

  // Open window cycle list. It should be open now so tab scrubber should be
  // disabled.
  StartCyclingWindows(browser());
  EXPECT_FALSE(IsTabScrubberChromeOSEnabled());
  Scrub(browser(), 0, EACH_TAB);
  EXPECT_EQ(0u, activation_order_.size());
  EXPECT_EQ(4, browser()->tab_strip_model()->active_index());

  // Stop cycling. Scrub should work again.
  StopCyclingWindows(browser());
  EXPECT_TRUE(IsTabScrubberChromeOSEnabled());
  Scrub(browser(), 0, EACH_TAB);
  EXPECT_THAT(activation_order_, testing::ElementsAre(3, 2, 1, 0));
  EXPECT_EQ(0, browser()->tab_strip_model()->active_index());
}

IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, VerticalAndHorizontalScroll) {
  auto event_generator = CreateEventGenerator(browser());
  constexpr int kOffset = 100;

  {
    // If y offset is larger than x offset, the event should be recognized as a
    // vertical scroll and should not begin scrubbing.
    ui::ScrollEvent vertical_scroll_event(
        ui::EventType::kScroll, gfx::Point(0, 0), ui::EventTimeForNow(), 0,
        /*x_offset=*/0, /*y_offset=*/kOffset,
        /*x_offset_ordinal_=*/0, /*y_offset=*/kOffset,
        kScrubbingGestureFingerCount);
    event_generator->Dispatch(&vertical_scroll_event);
    EXPECT_FALSE(vertical_scroll_event.handled());
  }

  {
    // If x offset is larger than y offset, the event should be recognized as a
    // horizontal scroll and should begin scrubbing.
    ui::ScrollEvent horizontal_scroll_event(
        ui::EventType::kScroll, gfx::Point(0, 0), ui::EventTimeForNow(), 0,
        /*x_offset=*/kOffset, /*y_offset=*/0,
        /*x_offset_ordinal_=*/kOffset, /*y_offset=*/0,
        kScrubbingGestureFingerCount);
    event_generator->Dispatch(&horizontal_scroll_event);
    EXPECT_TRUE(horizontal_scroll_event.handled());
  }

  {
    // Finish scrubbing by dispatching fling scroll event. For finishing the
    // event, it is not required to be horizontal scroll. This happens for
    // example when the user start scrubbing with a horizontal scroll and the
    // fingers go up at the end of the scroll.
    ui::ScrollEvent fling_scroll_event(
        ui::EventType::kScrollFlingStart, gfx::Point(0, 0),
        ui::EventTimeForNow(), 0,
        /*x_offset=*/0, /*y_offset=*/kOffset,
        /*x_offset_ordinal_=*/0, /*y_offset=*/kOffset, 0);
    event_generator->Dispatch(&fling_scroll_event);
    EXPECT_TRUE(fling_scroll_event.handled());
  }
}

// Check scroll events other than 3-fingers scroll are not handled by
// TabScrubber.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, EventHandling) {
  auto event_generator = CreateEventGenerator(browser());
  constexpr int kOffset = 100;

  {
    // Begin scrubbing and mark the event as handled for 3-fingers scroll event.
    ui::ScrollEvent scroll_event_with_3_fingers(
        ui::EventType::kScroll, gfx::Point(0, 0), ui::EventTimeForNow(), 0,
        kOffset, 0, kOffset, 0, kScrubbingGestureFingerCount);
    event_generator->Dispatch(&scroll_event_with_3_fingers);
    EXPECT_TRUE(scroll_event_with_3_fingers.handled());
  }

  {
    // Fling scroll event which is called during the scrubbing should be
    // consumed here.
    ui::ScrollEvent fling_scroll_event(ui::EventType::kScrollFlingStart,
                                       gfx::Point(0, 0), ui::EventTimeForNow(),
                                       0, kOffset, 0, kOffset, 0, 0);
    event_generator->Dispatch(&fling_scroll_event);
    EXPECT_TRUE(fling_scroll_event.handled());
  }

  {
    // Fling scroll event should NOT be consumed here if the scrubbing is not
    // ongoing. Do NOT handle the event for this scenario.
    ui::ScrollEvent fling_scroll_event(ui::EventType::kScrollFlingStart,
                                       gfx::Point(0, 0), ui::EventTimeForNow(),
                                       0, kOffset, 0, kOffset, 0, 0);
    event_generator->Dispatch(&fling_scroll_event);
    EXPECT_FALSE(fling_scroll_event.handled());
  }

  {
    // Other scroll events should be not handled by TabScrubber.
    ui::ScrollEvent scroll_event_with_2_fingers(
        ui::EventType::kScroll, gfx::Point(0, 0), ui::EventTimeForNow(), 0,
        kOffset, 0, kOffset, 0,
        /*finger_count=*/2);
    event_generator->Dispatch(&scroll_event_with_2_fingers);
    EXPECT_FALSE(scroll_event_with_2_fingers.handled());
  }
}

// Check scroll events other than 3-fingers scroll are not handled by
// TabScrubber with the active Lacros window.
IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, EventHandlingWithLacrosWindow) {
  // Create Lacros window and activate.
  auto shell_surface = exo::test::ShellSurfaceBuilder({100, 100})
                           .BuildClientControlledShellSurface();
  exo::SetShellApplicationId(shell_surface->GetWidget()->GetNativeWindow(),
                             crosapi::kLacrosAppIdPrefix);
  wm::ActivateWindow(shell_surface->GetWidget()->GetNativeWindow());
  ASSERT_TRUE(
      wm::IsActiveWindow(shell_surface->GetWidget()->GetNativeWindow()));

  auto event_generator = CreateEventGenerator(browser());
  constexpr int kOffset = 100;

  // Handle 3-fingers scroll event.
  ui::ScrollEvent scroll_event_with_3_fingers(
      ui::EventType::kScroll, gfx::Point(0, 0), ui::EventTimeForNow(), 0,
      kOffset, 0, kOffset, 0, kScrubbingGestureFingerCount);
  event_generator->Dispatch(&scroll_event_with_3_fingers);
  EXPECT_TRUE(scroll_event_with_3_fingers.handled());

  // Fling scroll event should be passed to Lacros via HandleTabScrubbing, but
  // should not be marked as handled since it may be consumed elsewhere as well.
  ui::ScrollEvent fling_scroll_event(ui::EventType::kScrollFlingStart,
                                     gfx::Point(0, 0), ui::EventTimeForNow(), 0,
                                     kOffset, 0, kOffset, 0, 0);
  event_generator->Dispatch(&fling_scroll_event);
  EXPECT_FALSE(fling_scroll_event.handled());

  // Other scroll events should be not handled by TabScrubber.
  ui::ScrollEvent scroll_event_with_2_fingers(
      ui::EventType::kScroll, gfx::Point(0, 0), ui::EventTimeForNow(), 0,
      kOffset, 0, kOffset, 0,
      /*finger_count=*/2);
  event_generator->Dispatch(&scroll_event_with_2_fingers);
  EXPECT_FALSE(scroll_event_with_2_fingers.handled());
}

IN_PROC_BROWSER_TEST_F(TabScrubberChromeOSTest, MaybeDelegateHandlingToLacros) {
  constexpr int kOffset = 100;

  ui::ScrollEvent fling_scroll_event(ui::EventType::kScrollFlingStart,
                                     gfx::Point(0, 0), ui::EventTimeForNow(), 0,
                                     kOffset, 0, kOffset, 0, 0);

  ui::ScrollEvent scroll_event_with_3_fingers(
      ui::EventType::kScroll, gfx::Point(0, 0), ui::EventTimeForNow(), 0,
      kOffset, 0, kOffset, 0, kScrubbingGestureFingerCount);

  ui::ScrollEvent scroll_event_with_2_fingers(
      ui::EventType::kScroll, gfx::Point(0, 0), ui::EventTimeForNow(), 0,
      kOffset, 0, kOffset, 0,
      /*finger_count=*/2);

  // When there is no activated Lacros window, all scroll events should not be
  // delegated to Lacros.
  EXPECT_FALSE(IsDelegatedToLacros(fling_scroll_event));
  EXPECT_FALSE(IsDelegatedToLacros(scroll_event_with_3_fingers));
  EXPECT_FALSE(IsDelegatedToLacros(scroll_event_with_2_fingers));

  // Create Lacros window and activate.
  auto shell_surface = exo::test::ShellSurfaceBuilder({100, 100})
                           .BuildClientControlledShellSurface();
  exo::SetShellApplicationId(shell_surface->GetWidget()->GetNativeWindow(),
                             crosapi::kLacrosAppIdPrefix);
  wm::ActivateWindow(shell_surface->GetWidget()->GetNativeWindow());
  ASSERT_TRUE(
      wm::IsActiveWindow(shell_surface->GetWidget()->GetNativeWindow()));

  // If Lacros window is activated, delegate scroll events related to tab
  // scrubbing to Lacros while do not delegate other scroll events such as
  // 2-fingers scroll event.
  EXPECT_TRUE(IsDelegatedToLacros(fling_scroll_event));
  EXPECT_TRUE(IsDelegatedToLacros(scroll_event_with_3_fingers));
  EXPECT_FALSE(IsDelegatedToLacros(scroll_event_with_2_fingers));
}