chromium/ash/system/accessibility/autoclick_menu_bubble_controller_unittest.cc

// Copyright 2019 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/accessibility/autoclick_menu_bubble_controller.h"

#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/autoclick/autoclick_controller.h"
#include "ash/public/cpp/locale_update_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/system/accessibility/autoclick_scroll_bubble_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/rtl.h"

namespace ash {

namespace {

// A buffer for checking whether the menu view is near this region of the
// screen. This buffer allows for things like padding and the shelf size,
// but is still smaller than half the screen size, so that we can check the
// general corner in which the menu is displayed.
const int kMenuViewBoundsBuffer = 100;

// The buffers in dips around a scroll point where the scroll menu is shown.
// This should be slightly larger than kScrollPointBufferDips and
// kScrollRectBufferDips respectively from AutoclickScrollBubbleController to
// allow for some wiggle room.
const int kScrollViewBoundsBuffer = 26;
const int kScrollViewBoundsRectBuffer = 18;

ui::GestureEvent CreateTapEvent() {
  return ui::GestureEvent(0, 0, 0, base::TimeTicks(),
                          ui::GestureEventDetails(ui::EventType::kGestureTap));
}

}  // namespace

class AutoclickMenuBubbleControllerTest : public AshTestBase {
 public:
  AutoclickMenuBubbleControllerTest() = default;

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

  ~AutoclickMenuBubbleControllerTest() override = default;

  // testing::Test:
  void SetUp() override {
    AshTestBase::SetUp();
    Shell::Get()->accessibility_controller()->autoclick().SetEnabled(true);
  }

  AutoclickMenuBubbleController* GetBubbleController() {
    return Shell::Get()
        ->autoclick_controller()
        ->GetMenuBubbleControllerForTesting();
  }

  AutoclickMenuView* GetMenuView() {
    return GetBubbleController() ? GetBubbleController()->menu_view_.get()
                                 : nullptr;
  }

  views::View* GetMenuButton(AutoclickMenuView::ButtonId view_id) {
    AutoclickMenuView* menu_view = GetMenuView();
    if (!menu_view)
      return nullptr;
    return menu_view->GetViewByID(static_cast<int>(view_id));
  }

  gfx::Rect GetMenuViewBounds() {
    return GetBubbleController()
               ? GetBubbleController()->menu_view_->GetBoundsInScreen()
               : gfx::Rect(-kMenuViewBoundsBuffer, -kMenuViewBoundsBuffer);
  }

  AutoclickScrollView* GetScrollView() {
    return GetBubbleController()->scroll_bubble_controller_
               ? GetBubbleController()
                     ->scroll_bubble_controller_->scroll_view_.get()
               : nullptr;
  }

  views::View* GetScrollButton(AutoclickScrollView::ButtonId view_id) {
    AutoclickScrollView* scroll_view = GetScrollView();
    if (!scroll_view)
      return nullptr;
    return scroll_view->GetViewByID(static_cast<int>(view_id));
  }

  gfx::Rect GetScrollViewBounds() {
    return GetBubbleController()->scroll_bubble_controller_
               ? GetBubbleController()
                     ->scroll_bubble_controller_->scroll_view_
                     ->GetBoundsInScreen()
               : gfx::Rect(-kMenuViewBoundsBuffer, -kMenuViewBoundsBuffer);
  }
};

TEST_F(AutoclickMenuBubbleControllerTest, ExistsOnlyWhenAutoclickIsRunning) {
  // Cycle a few times to ensure there are no crashes when toggling the feature.
  for (int i = 0; i < 2; i++) {
    EXPECT_TRUE(GetBubbleController());
    EXPECT_TRUE(GetMenuView());
    Shell::Get()->autoclick_controller()->SetEnabled(
        false, false /* do not show dialog */);
    EXPECT_FALSE(GetBubbleController());
    Shell::Get()->autoclick_controller()->SetEnabled(
        true, false /* do not show dialog */);
  }
}

TEST_F(AutoclickMenuBubbleControllerTest, CanSelectAutoclickTypeFromBubble) {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  // Set to a different event type than the first event in kTestCases.
  controller->SetAutoclickEventType(AutoclickEventType::kRightClick);

  const struct {
    AutoclickMenuView::ButtonId view_id;
    AutoclickEventType expected_event_type;
  } kTestCases[] = {
      {AutoclickMenuView::ButtonId::kLeftClick, AutoclickEventType::kLeftClick},
      {AutoclickMenuView::ButtonId::kRightClick,
       AutoclickEventType::kRightClick},
      {AutoclickMenuView::ButtonId::kDoubleClick,
       AutoclickEventType::kDoubleClick},
      {AutoclickMenuView::ButtonId::kDragAndDrop,
       AutoclickEventType::kDragAndDrop},
      {AutoclickMenuView::ButtonId::kScroll, AutoclickEventType::kScroll},
      {AutoclickMenuView::ButtonId::kPause, AutoclickEventType::kNoAction},
  };

  for (const auto& test : kTestCases) {
    // Find the autoclick action button for this test case.
    views::View* button = GetMenuButton(test.view_id);
    ASSERT_TRUE(button) << "No view for id " << static_cast<int>(test.view_id);

    // Tap the action button.
    ui::GestureEvent event = CreateTapEvent();
    button->OnGestureEvent(&event);

    // Pref change happened.
    EXPECT_EQ(test.expected_event_type, controller->GetAutoclickEventType());
  }
}

TEST_F(AutoclickMenuBubbleControllerTest, UnpausesWhenPauseAlreadySelected) {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  views::View* pause_button =
      GetMenuButton(AutoclickMenuView::ButtonId::kPause);
  ui::GestureEvent event = CreateTapEvent();

  const struct {
    AutoclickEventType event_type;
  } kTestCases[]{
      {AutoclickEventType::kRightClick},  {AutoclickEventType::kLeftClick},
      {AutoclickEventType::kDoubleClick}, {AutoclickEventType::kDragAndDrop},
      {AutoclickEventType::kScroll},
  };

  for (const auto& test : kTestCases) {
    controller->SetAutoclickEventType(test.event_type);

    // First tap pauses.
    pause_button->OnGestureEvent(&event);
    EXPECT_EQ(AutoclickEventType::kNoAction,
              controller->GetAutoclickEventType());

    // Second tap unpauses and reverts to previous state.
    pause_button->OnGestureEvent(&event);
    EXPECT_EQ(test.event_type, controller->GetAutoclickEventType());
  }
}

TEST_F(AutoclickMenuBubbleControllerTest, CanChangePosition) {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();

  // Set to a known position for than the first event in kTestCases.
  controller->SetAutoclickMenuPosition(FloatingMenuPosition::kTopRight);

  // Get the full root window bounds to test the position.
  gfx::Rect window_bounds = Shell::GetPrimaryRootWindow()->bounds();

  // Test cases rotate clockwise.
  const struct {
    gfx::Point expected_location;
    FloatingMenuPosition expected_position;
  } kTestCases[] = {
      {gfx::Point(window_bounds.width(), window_bounds.height()),
       FloatingMenuPosition::kBottomRight},
      {gfx::Point(0, window_bounds.height()),
       FloatingMenuPosition::kBottomLeft},
      {gfx::Point(0, 0), FloatingMenuPosition::kTopLeft},
      {gfx::Point(window_bounds.width(), 0), FloatingMenuPosition::kTopRight},
  };

  // Find the autoclick menu position button.
  views::View* button = GetMenuButton(AutoclickMenuView::ButtonId::kPosition);
  ASSERT_TRUE(button) << "No position button found.";

  // Loop through all positions twice.
  for (int i = 0; i < 2; i++) {
    for (const auto& test : kTestCases) {
      // Tap the position button.
      ui::GestureEvent event = CreateTapEvent();
      button->OnGestureEvent(&event);

      // Pref change happened.
      EXPECT_EQ(test.expected_position, controller->GetAutoclickMenuPosition());

      // Menu is in generally the correct screen location.
      EXPECT_LT(
          GetMenuViewBounds().ManhattanDistanceToPoint(test.expected_location),
          kMenuViewBoundsBuffer);
    }
  }
}

TEST_F(AutoclickMenuBubbleControllerTest, DefaultChangesWithTextDirection) {
  gfx::Rect window_bounds = Shell::GetPrimaryRootWindow()->bounds();

  // RTL should position the menu on the bottom left.
  base::i18n::SetICUDefaultLocale("he");
  // Trigger the LocaleChangeObserver, which should cause a layout of the menu.
  ash::LocaleUpdateController::Get()->OnLocaleChanged();
  EXPECT_TRUE(base::i18n::IsRTL());
  EXPECT_LT(
      GetMenuViewBounds().ManhattanDistanceToPoint(window_bounds.bottom_left()),
      kMenuViewBoundsBuffer);

  // LTR should position the menu on the bottom right.
  base::i18n::SetICUDefaultLocale("en");
  ash::LocaleUpdateController::Get()->OnLocaleChanged();
  EXPECT_FALSE(base::i18n::IsRTL());
  EXPECT_LT(GetMenuViewBounds().ManhattanDistanceToPoint(
                window_bounds.bottom_right()),
            kMenuViewBoundsBuffer);
}

TEST_F(AutoclickMenuBubbleControllerTest, ScrollBubbleShowsAndCloses) {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  controller->SetAutoclickEventType(AutoclickEventType::kLeftClick);
  // No scroll view yet.
  EXPECT_FALSE(GetScrollView());

  // Scroll type should cause the scroll view to be shown.
  controller->SetAutoclickEventType(AutoclickEventType::kScroll);
  EXPECT_TRUE(GetScrollView());

  // Clicking the scroll close button resets to left click.
  views::View* close_button =
      GetScrollButton(AutoclickScrollView::ButtonId::kCloseScroll);
  ui::GestureEvent event = CreateTapEvent();
  close_button->OnGestureEvent(&event);
  EXPECT_FALSE(GetScrollView());
  EXPECT_EQ(AutoclickEventType::kLeftClick,
            controller->GetAutoclickEventType());
}

TEST_F(AutoclickMenuBubbleControllerTest, ScrollBubbleDefaultPositioning) {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  controller->SetAutoclickEventType(AutoclickEventType::kScroll);

  const struct { bool is_RTL; } kTestCases[] = {{true}, {false}};
  for (auto& test : kTestCases) {
    // These positions should be relative to the corners of the screen
    // whether we are in RTL or LTR.
    base::i18n::SetRTLForTesting(test.is_RTL);

    // When the menu is in the top right, the scroll view should be directly
    // under it and along the right side of the screen.
    controller->SetAutoclickMenuPosition(FloatingMenuPosition::kTopRight);
    EXPECT_LT(GetScrollViewBounds().ManhattanDistanceToPoint(
                  GetMenuViewBounds().bottom_center()),
              kMenuViewBoundsBuffer);
    EXPECT_EQ(GetScrollViewBounds().right(), GetMenuViewBounds().right());

    // When the menu is in the bottom right, the scroll view is directly above
    // it and along the right side of the screen.
    controller->SetAutoclickMenuPosition(FloatingMenuPosition::kBottomRight);
    EXPECT_LT(GetScrollViewBounds().ManhattanDistanceToPoint(
                  GetMenuViewBounds().top_center()),
              kMenuViewBoundsBuffer);
    EXPECT_EQ(GetScrollViewBounds().right(), GetMenuViewBounds().right());

    // When the menu is on the bottom left, the scroll view is directly above it
    // and along the left side of the screen.
    controller->SetAutoclickMenuPosition(FloatingMenuPosition::kBottomLeft);
    EXPECT_LT(GetScrollViewBounds().ManhattanDistanceToPoint(
                  GetMenuViewBounds().top_center()),
              kMenuViewBoundsBuffer);
    EXPECT_EQ(GetScrollViewBounds().x(), GetMenuViewBounds().x());

    // When the menu is on the top left, the scroll view is directly below it
    // and along the left side of the screen.
    controller->SetAutoclickMenuPosition(FloatingMenuPosition::kTopLeft);
    EXPECT_LT(GetScrollViewBounds().ManhattanDistanceToPoint(
                  GetMenuViewBounds().bottom_center()),
              kMenuViewBoundsBuffer);
    EXPECT_EQ(GetScrollViewBounds().x(), GetMenuViewBounds().x());
  }
}

TEST_F(AutoclickMenuBubbleControllerTest,
       ScrollBubbleManualPositioningLargeScrollBounds) {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  controller->SetAutoclickEventType(AutoclickEventType::kScroll);

  // With large scrollable bounds, the scroll bubble should just be positioned
  // near the scroll point. Try with high density bounds and LTR/RTL languages.
  const struct {
    bool is_RTL;
    gfx::Rect scroll_bounds;
    std::string display_spec;
  } kTestCases[] = {
      {true, gfx::Rect(0, 0, 1000, 800), "1000x800"},
      {false, gfx::Rect(0, 0, 1000, 800), "1000x800"},
      {false, gfx::Rect(100, 100, 800, 600), "1000x800"},
      {true, gfx::Rect(200, 0, 600, 600), "1000x800"},
      {true, gfx::Rect(0, 0, 1000, 800), "2000x1600*2"},
      {false, gfx::Rect(0, 0, 1000, 800), "2000x1600*2"},
  };
  for (auto& test : kTestCases) {
    UpdateDisplay(test.display_spec);
    base::i18n::SetRTLForTesting(test.is_RTL);
    gfx::Rect scroll_bounds = test.scroll_bounds;
    controller->SetAutoclickMenuPosition(FloatingMenuPosition::kTopRight);

    // Start with a point no where near the autoclick menu.
    gfx::Point point = gfx::Point(400, 400);
    GetBubbleController()->SetScrollPosition(scroll_bounds, point);

    // Just below the point in the Y axis.
    EXPECT_LT(abs(GetScrollViewBounds().y() - point.y()),
              kScrollViewBoundsBuffer);

    // Just off to the side in the X axis.
    if (test.is_RTL) {
      EXPECT_LT(abs(GetScrollViewBounds().right() - point.x()),
                kScrollViewBoundsBuffer);
    } else {
      EXPECT_LT(abs(GetScrollViewBounds().x() - point.x()),
                kScrollViewBoundsBuffer);
    }

    // Moving the autoclick bubble doesn't impact the scroll bubble once it
    // has been manually set.
    gfx::Rect bubble_bounds = GetScrollViewBounds();
    controller->SetAutoclickMenuPosition(FloatingMenuPosition::kBottomRight);
    EXPECT_EQ(bubble_bounds, GetScrollViewBounds());

    // If we position it by the edge of the screen, it should stay on-screen,
    // regardless of LTR vs RTL.
    point = gfx::Point(0, 0);
    GetBubbleController()->SetScrollPosition(scroll_bounds, point);
    EXPECT_LT(abs(GetScrollViewBounds().x() - point.x()),
              kScrollViewBoundsBuffer);
    EXPECT_LT(abs(GetScrollViewBounds().y() - point.y()),
              kScrollViewBoundsBuffer);

    point = gfx::Point(1000, 400);
    GetBubbleController()->SetScrollPosition(scroll_bounds, point);
    EXPECT_LT(abs(GetScrollViewBounds().right() - point.x()),
              kScrollViewBoundsBuffer);
    EXPECT_LT(abs(GetScrollViewBounds().y() - point.y()),
              kScrollViewBoundsBuffer);

    // If it's too close to the bottom, it will be shown above the point.
    point = gfx::Point(500, 700);
    GetBubbleController()->SetScrollPosition(scroll_bounds, point);
    EXPECT_LT(abs(GetScrollViewBounds().bottom() - point.y()),
              kScrollViewBoundsBuffer);
  }
}

TEST_F(AutoclickMenuBubbleControllerTest,
       ScrollBubbleManualPositioningSmallScrollBounds) {
  UpdateDisplay("1200x1000");
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  controller->SetAutoclickEventType(AutoclickEventType::kScroll);

  const struct {
    bool is_RTL;
    gfx::Rect scroll_bounds;
    gfx::Point scroll_point;
    bool expect_bounds_on_right;
    bool expect_bounds_on_left;
    bool expect_bounds_on_top;
    bool expect_bounds_on_bottom;
  } kTestCases[] = {
      // Small centered bounds, point closest to the right side.
      {true, gfx::Rect(400, 350, 300, 300), gfx::Point(555, 500),
       true /* on right */, false, false, false},
      {false, gfx::Rect(400, 350, 300, 300), gfx::Point(555, 500),
       true /* on right */, false, false, false},
      {false, gfx::Rect(400, 350, 300, 300), gfx::Point(650, 400),
       true /* on right */, false, false, false},

      // Small centered bounds, point closest to the left side.
      {true, gfx::Rect(400, 350, 300, 300), gfx::Point(545, 500), false,
       true /* on left */, false, false},
      {false, gfx::Rect(400, 350, 300, 300), gfx::Point(545, 500), false,
       true /* on left */, false, false},
      {false, gfx::Rect(400, 350, 300, 300), gfx::Point(410, 400), false,
       true /* on left */, false, false},

      // Point closest to the top of the bounds.
      {true, gfx::Rect(400, 350, 300, 300), gfx::Point(550, 400), false, false,
       true /* on top */, false},
      {false, gfx::Rect(400, 350, 300, 300), gfx::Point(550, 400), false, false,
       true /* on top */, false},
      {false, gfx::Rect(400, 350, 300, 300), gfx::Point(402, 351), false, false,
       true /* on top */, false},

      // Point closest to the bottom of the bounds.
      {true, gfx::Rect(400, 350, 300, 300), gfx::Point(550, 550), false, false,
       false, true /* on bottom */},
      {false, gfx::Rect(400, 350, 300, 300), gfx::Point(550, 550), false, false,
       false, true /* on bottom */},
      {false, gfx::Rect(400, 350, 300, 300), gfx::Point(450, 649), false, false,
       false, true /* on bottom */},

      // These bounds only have space on the right and bottom. Even points
      // close to the top left get mapped to the right or bottom.
      {false, gfx::Rect(100, 100, 300, 300), gfx::Point(130, 120),
       true /* on right */, false, false, false},
      {true, gfx::Rect(100, 100, 300, 300), gfx::Point(130, 120),
       true /* on right */, false, false, false},
      {false, gfx::Rect(100, 100, 300, 300), gfx::Point(120, 130), false, false,
       false, true /* on bottom */},

      // These bounds only have space on the top and left. Even points
      // close to the bottom right get mapped to the top or left.
      {false, gfx::Rect(900, 600, 300, 300), gfx::Point(1075, 800), false,
       true /* on left */, false, false},
      {false, gfx::Rect(900, 600, 300, 300), gfx::Point(1075, 800), false,
       true /* on left */, false, false},
      {false, gfx::Rect(900, 600, 300, 300), gfx::Point(1100, 775), false,
       false, true /* on top */, false},

      // These bounds have space above/below, but not left/right.
      {false, gfx::Rect(400, 100, 300, 800), gfx::Point(525, 110), false,
       true /* on left */, false, false},
      {false, gfx::Rect(400, 100, 300, 800), gfx::Point(525, 845), false,
       true /* on left */, false, false},
      {false, gfx::Rect(400, 100, 300, 800), gfx::Point(575, 845),
       true /* on right */, false, false, false},

      // These bounds have space left/right, but not above/below.
      {false, gfx::Rect(100, 350, 1000, 300), gfx::Point(200, 450), false,
       false, true /* on top */, false},
      {false, gfx::Rect(100, 350, 1000, 300), gfx::Point(200, 550), false,
       false, false, true /* on bottom */},
      {false, gfx::Rect(100, 350, 1000, 300), gfx::Point(1000, 550), false,
       false, false, true /* on bottom */},
  };
  for (auto& test : kTestCases) {
    base::i18n::SetRTLForTesting(test.is_RTL);
    gfx::Rect scroll_bounds = test.scroll_bounds;
    gfx::Point scroll_point = test.scroll_point;
    GetBubbleController()->SetScrollPosition(scroll_bounds, scroll_point);

    if (test.expect_bounds_on_right) {
      // The scroll view should be on the right of the bounds and centered
      // vertically on the scroll point.
      EXPECT_LT(abs(GetScrollViewBounds().y() +
                    GetScrollViewBounds().height() / 2 - scroll_point.y()),
                kScrollViewBoundsRectBuffer);
      EXPECT_LT(GetScrollViewBounds().x() - scroll_bounds.right(),
                kScrollViewBoundsRectBuffer);
      EXPECT_GT(GetScrollViewBounds().x() - scroll_bounds.right(), 0);
    } else if (test.expect_bounds_on_left) {
      // The scroll view should be on the left of the bounds and centered
      // vertically on the scroll point.
      EXPECT_LT(abs(GetScrollViewBounds().y() +
                    GetScrollViewBounds().height() / 2 - scroll_point.y()),
                kScrollViewBoundsRectBuffer);
      EXPECT_LT(scroll_bounds.x() - GetScrollViewBounds().right(),
                kScrollViewBoundsRectBuffer);
      EXPECT_GT(scroll_bounds.x() - GetScrollViewBounds().right(), 0);
    } else if (test.expect_bounds_on_top) {
      // The scroll view should be on the top of the bounds and centered
      // horizontally on the scroll point.
      EXPECT_LT(abs(GetScrollViewBounds().x() +
                    GetScrollViewBounds().width() / 2 - scroll_point.x()),
                kScrollViewBoundsRectBuffer);
      EXPECT_LT(scroll_bounds.y() - GetScrollViewBounds().bottom(),
                kScrollViewBoundsRectBuffer);
      EXPECT_GT(scroll_bounds.y() - GetScrollViewBounds().bottom(), 0);
    } else if (test.expect_bounds_on_bottom) {
      // The scroll view should be on the bottom of the bounds and centered
      // horizontally on the scroll point.
      EXPECT_LT(abs(GetScrollViewBounds().x() +
                    GetScrollViewBounds().width() / 2 - scroll_point.x()),
                kScrollViewBoundsRectBuffer);
      EXPECT_LT(GetScrollViewBounds().y() - scroll_bounds.bottom(),
                kScrollViewBoundsRectBuffer);
      EXPECT_GT(GetScrollViewBounds().y() - scroll_bounds.bottom(), -1);
    }
  }
}

}  // namespace ash