// 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));
}