// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ash/accessibility/chromevox/touch_exploration_controller.h"
#include <math.h>
#include <stddef.h>
#include <memory>
#include <vector>
#include "ash/accessibility/chromevox/mock_touch_exploration_controller_delegate.h"
#include "base/memory/ptr_util.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/time/time.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/test/aura_test_base.h"
#include "ui/aura/test/test_cursor_client.h"
#include "ui/aura/window.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/gestures/gesture_provider_aura.h"
#include "ui/events/test/event_generator.h"
#include "ui/events/test/events_test_utils.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gl/gl_implementation.h"
#include "ui/gl/test/gl_surface_test_support.h"
using EventList = std::vector<std::unique_ptr<ui::Event>>;
namespace ash {
namespace {
// Records all mouse, touch, gesture, and key events.
class EventCapturer : public ui::EventHandler {
public:
EventCapturer() {}
EventCapturer(const EventCapturer&) = delete;
EventCapturer& operator=(const EventCapturer&) = delete;
~EventCapturer() override {}
void Reset() { events_.clear(); }
void OnEvent(ui::Event* event) override {
if (event->IsMouseEvent() || event->IsTouchEvent() ||
event->IsGestureEvent() || event->IsKeyEvent()) {
events_.push_back(event->Clone());
} else {
return;
}
// Stop event propagation so we don't click on random stuff that
// might break test assumptions.
event->StopPropagation();
// If there is a possibility that we're in an infinite loop, we should
// exit early with a sensible error rather than letting the test time out.
ASSERT_LT(events_.size(), 100u);
}
const EventList& captured_events() const { return events_; }
private:
EventList events_;
};
int Factorial(int n) {
if (n <= 0)
return 0;
if (n == 1)
return 1;
return n * Factorial(n - 1);
}
} // namespace
class TouchExplorationControllerTestApi {
public:
TouchExplorationControllerTestApi(
TouchExplorationController* touch_exploration_controller) {
touch_exploration_controller_.reset(touch_exploration_controller);
}
TouchExplorationControllerTestApi(const TouchExplorationControllerTestApi&) =
delete;
TouchExplorationControllerTestApi& operator=(
const TouchExplorationControllerTestApi&) = delete;
void CallTapTimerNowForTesting() {
DCHECK(touch_exploration_controller_->tap_timer_.IsRunning());
touch_exploration_controller_->tap_timer_.Stop();
touch_exploration_controller_->OnTapTimerFired();
}
void CallTapTimerNowIfRunningForTesting() {
if (touch_exploration_controller_->tap_timer_.IsRunning()) {
touch_exploration_controller_->tap_timer_.Stop();
touch_exploration_controller_->OnTapTimerFired();
}
}
void CallLongPressTimerNowIfRunningForTesting() {
if (touch_exploration_controller_->long_press_timer_.IsRunning()) {
touch_exploration_controller_->long_press_timer_.Stop();
touch_exploration_controller_->OnLiftActivationLongPressTimerFired();
}
}
bool IsInNoFingersDownStateForTesting() const {
return touch_exploration_controller_->state_ ==
touch_exploration_controller_->NO_FINGERS_DOWN;
}
bool IsInGestureInProgressStateForTesting() const {
return touch_exploration_controller_->state_ ==
touch_exploration_controller_->GESTURE_IN_PROGRESS;
}
bool IsInSlideGestureStateForTesting() const {
return touch_exploration_controller_->state_ ==
touch_exploration_controller_->SLIDE_GESTURE;
}
bool IsInTwoFingerTapStateForTesting() const {
return touch_exploration_controller_->state_ ==
touch_exploration_controller_->TWO_FINGER_TAP;
}
gfx::Rect BoundsOfRootWindowInDIPForTesting() const {
return touch_exploration_controller_->root_window_->GetBoundsInScreen();
}
// VLOGs should be suppressed in tests that generate a lot of logs,
// for example permutations of nine touch events.
void SuppressVLOGsForTesting(bool suppress) {
touch_exploration_controller_->VLOG_on_ = !suppress;
}
float GetMaxDistanceFromEdge() const {
return touch_exploration_controller_->kMaxDistanceFromEdge;
}
float GetSlopDistanceFromEdge() const {
return touch_exploration_controller_->kSlopDistanceFromEdge;
}
void SetTouchAccessibilityAnchorPoint(const gfx::Point& location) {
touch_exploration_controller_->SetTouchAccessibilityAnchorPoint(location);
}
void SetExcludeBounds(const gfx::Rect& bounds) {
touch_exploration_controller_->SetExcludeBounds(bounds);
}
void SetLiftActivationBounds(const gfx::Rect& bounds) {
touch_exploration_controller_->SetLiftActivationBounds(bounds);
}
private:
std::unique_ptr<TouchExplorationController> touch_exploration_controller_;
};
class TouchExplorationTest : public aura::test::AuraTestBase {
public:
TouchExplorationTest() {}
TouchExplorationTest(const TouchExplorationTest&) = delete;
TouchExplorationTest& operator=(const TouchExplorationTest&) = delete;
~TouchExplorationTest() override {}
void SetUp() override {
if (gl::GetGLImplementation() == gl::kGLImplementationNone)
gl::GLSurfaceTestSupport::InitializeOneOff();
aura::test::AuraTestBase::SetUp();
cursor_client_ =
std::make_unique<aura::test::TestCursorClient>(root_window());
root_window()->AddPreTargetHandler(&event_capturer_);
generator_ = std::make_unique<ui::test::EventGenerator>(root_window());
// Tests fail if time is ever 0.
simulated_clock_.Advance(base::Milliseconds(10));
// ui takes ownership of the tick clock.
ui::SetEventTickClockForTesting(&simulated_clock_);
cursor_client()->ShowCursor();
cursor_client()->DisableMouseEvents();
}
void TearDown() override {
ui::SetEventTickClockForTesting(nullptr);
root_window()->RemovePreTargetHandler(&event_capturer_);
SwitchTouchExplorationMode(false);
cursor_client_.reset();
aura::test::AuraTestBase::TearDown();
}
protected:
aura::client::CursorClient* cursor_client() { return cursor_client_.get(); }
const EventList& GetCapturedEvents() {
return event_capturer_.captured_events();
}
std::vector<ui::LocatedEvent*> GetCapturedLocatedEvents() {
const EventList& all_events = GetCapturedEvents();
std::vector<ui::LocatedEvent*> located_events;
for (size_t i = 0; i < all_events.size(); ++i) {
if (all_events[i]->IsMouseEvent() || all_events[i]->IsTouchEvent() ||
all_events[i]->IsGestureEvent()) {
located_events.push_back(
static_cast<ui::LocatedEvent*>(all_events[i].get()));
}
}
return located_events;
}
std::vector<ui::LocatedEvent*> GetCapturedLocatedEventsOfType(
ui::EventType type) {
std::vector<ui::LocatedEvent*> located_events = GetCapturedLocatedEvents();
std::vector<ui::LocatedEvent*> events;
for (size_t i = 0; i < located_events.size(); ++i) {
if (type == located_events[i]->type())
events.push_back(located_events[i]);
}
return events;
}
std::vector<gfx::Point>& GetTouchExplorePoints() {
return delegate_.GetTouchExplorePoints();
}
void ClearCapturedAndGestureEvents() {
event_capturer_.Reset();
GetTouchExplorePoints().clear();
}
void AdvanceSimulatedTimePastTapDelay() {
simulated_clock_.Advance(gesture_detector_config_.double_tap_timeout);
simulated_clock_.Advance(base::Milliseconds(1));
touch_exploration_controller_->CallTapTimerNowForTesting();
}
void AdvanceSimulatedTimePastPotentialTapDelay() {
simulated_clock_.Advance(base::Milliseconds(1000));
touch_exploration_controller_->CallTapTimerNowIfRunningForTesting();
}
void AdvanceSimulatedTimePastLongPressDelay() {
simulated_clock_.Advance(base::Milliseconds(5000));
touch_exploration_controller_->CallLongPressTimerNowIfRunningForTesting();
}
void SuppressVLOGs(bool suppress) {
touch_exploration_controller_->SuppressVLOGsForTesting(suppress);
}
void SwitchTouchExplorationMode(bool on) {
if (!on && touch_exploration_controller_.get()) {
touch_exploration_controller_.reset();
} else if (on && !touch_exploration_controller_.get()) {
touch_exploration_controller_ =
std::make_unique<TouchExplorationControllerTestApi>(
new TouchExplorationController(root_window(), &delegate_,
nullptr));
cursor_client()->ShowCursor();
cursor_client()->DisableMouseEvents();
}
}
void EnterTouchExplorationModeAtLocation(gfx::Point tap_location) {
ui::TouchEvent touch_press(
ui::EventType::kTouchPressed, tap_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&touch_press);
AdvanceSimulatedTimePastTapDelay();
EXPECT_TRUE(IsInTouchToMouseMode());
}
bool IsInTouchToMouseMode() {
aura::client::CursorClient* cursor_client =
aura::client::GetCursorClient(root_window());
return cursor_client && cursor_client->IsMouseEventsEnabled() &&
!cursor_client->IsCursorVisible();
}
bool IsInNoFingersDownState() {
return touch_exploration_controller_->IsInNoFingersDownStateForTesting();
}
bool IsInGestureInProgressState() {
return touch_exploration_controller_
->IsInGestureInProgressStateForTesting();
}
bool IsInSlideGestureState() {
return touch_exploration_controller_->IsInSlideGestureStateForTesting();
}
bool IsInTwoFingerTapState() {
return touch_exploration_controller_->IsInTwoFingerTapStateForTesting();
}
gfx::Rect BoundsOfRootWindowInDIP() {
return touch_exploration_controller_->BoundsOfRootWindowInDIPForTesting();
}
float GetMaxDistanceFromEdge() const {
return touch_exploration_controller_->GetMaxDistanceFromEdge();
}
float GetSlopDistanceFromEdge() const {
return touch_exploration_controller_->GetSlopDistanceFromEdge();
}
base::TimeTicks Now() { return ui::EventTimeForNow(); }
void SetTouchAccessibilityAnchorPoint(const gfx::Point& location) {
touch_exploration_controller_->SetTouchAccessibilityAnchorPoint(location);
}
void SetExcludeBounds(const gfx::Rect& bounds) {
touch_exploration_controller_->SetExcludeBounds(bounds);
}
void SetLiftActivationBounds(const gfx::Rect& bounds) {
touch_exploration_controller_->SetLiftActivationBounds(bounds);
}
// Taps at |tap_location|, waiting past tap delay to enter touch
// exploration. Pass true to |set_anchor_point| to ensure any subsequent
// gestures like a double tap go to |tap_location|. Usually, ChromeVox sets
// the anchor, which is the center of the focused node and can differ from
// |tap_location|.
void TapAndVerifyTouchExplore(gfx::Point tap_location,
bool set_anchor_point = false) {
generator_->set_current_screen_location(tap_location);
generator_->PressTouchId(1);
generator_->ReleaseTouchId(1);
AdvanceSimulatedTimePastTapDelay();
if (set_anchor_point)
SetTouchAccessibilityAnchorPoint(tap_location);
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kMouseMoved);
ASSERT_TRUE(events.empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
EXPECT_EQ(tap_location, GetTouchExplorePoints()[0]);
ClearCapturedAndGestureEvents();
}
std::unique_ptr<ui::test::EventGenerator> generator_;
ui::GestureDetector::Config gesture_detector_config_;
base::SimpleTestTickClock simulated_clock_;
MockTouchExplorationControllerDelegate delegate_;
private:
EventCapturer event_capturer_;
std::unique_ptr<TouchExplorationControllerTestApi>
touch_exploration_controller_;
std::unique_ptr<aura::test::TestCursorClient> cursor_client_;
};
// Executes a number of assertions to confirm that |e1| and |e2| are touch
// events and are equal to each other.
void ConfirmEventsAreTouchAndEqual(ui::Event* e1, ui::Event* e2) {
ASSERT_TRUE(e1->IsTouchEvent());
ASSERT_TRUE(e2->IsTouchEvent());
ui::TouchEvent* touch_event1 = e1->AsTouchEvent();
ui::TouchEvent* touch_event2 = e2->AsTouchEvent();
EXPECT_EQ(touch_event1->type(), touch_event2->type());
EXPECT_EQ(touch_event1->location(), touch_event2->location());
EXPECT_EQ(touch_event1->pointer_details().id,
touch_event2->pointer_details().id);
EXPECT_EQ(touch_event1->flags(), touch_event2->flags());
EXPECT_EQ(touch_event1->time_stamp(), touch_event2->time_stamp());
}
// Executes a number of assertions to confirm that |e1| and |e2| are mouse
// events and are equal to each other.
void ConfirmEventsAreMouseAndEqual(ui::Event* e1, ui::Event* e2) {
ASSERT_TRUE(e1->IsMouseEvent());
ASSERT_TRUE(e2->IsMouseEvent());
ui::MouseEvent* mouse_event1 = e1->AsMouseEvent();
ui::MouseEvent* mouse_event2 = e2->AsMouseEvent();
EXPECT_EQ(mouse_event1->type(), mouse_event2->type());
EXPECT_EQ(mouse_event1->location(), mouse_event2->location());
EXPECT_EQ(mouse_event1->root_location(), mouse_event2->root_location());
EXPECT_EQ(mouse_event1->flags(), mouse_event2->flags());
}
// Executes a number of assertions to confirm that |e1| and |e2| are key events
// and are equal to each other.
void ConfirmEventsAreKeyAndEqual(ui::Event* e1, ui::Event* e2) {
ASSERT_TRUE(e1->IsKeyEvent());
ASSERT_TRUE(e2->IsKeyEvent());
ui::KeyEvent* key_event1 = e1->AsKeyEvent();
ui::KeyEvent* key_event2 = e2->AsKeyEvent();
EXPECT_EQ(key_event1->type(), key_event2->type());
EXPECT_EQ(key_event1->key_code(), key_event2->key_code());
EXPECT_EQ(key_event1->code(), key_event2->code());
EXPECT_EQ(key_event1->flags(), key_event2->flags());
}
#define CONFIRM_EVENTS_ARE_TOUCH_AND_EQUAL(e1, e2) \
ASSERT_NO_FATAL_FAILURE(ConfirmEventsAreTouchAndEqual(e1, e2))
#define CONFIRM_EVENTS_ARE_MOUSE_AND_EQUAL(e1, e2) \
ASSERT_NO_FATAL_FAILURE(ConfirmEventsAreMouseAndEqual(e1, e2))
#define CONFIRM_EVENTS_ARE_KEY_AND_EQUAL(e1, e2) \
ASSERT_NO_FATAL_FAILURE(ConfirmEventsAreKeyAndEqual(e1, e2))
// TODO(mfomitchev): Need to investigate why we don't get mouse enter/exit
// events when running these tests as part of ui_base_unittests. We do get them
// when the tests are run as part of ash unit tests.
TEST_F(TouchExplorationTest, EntersTouchToMouseModeAfterPressAndDelay) {
SwitchTouchExplorationMode(true);
EXPECT_FALSE(IsInTouchToMouseMode());
generator_->PressTouch();
AdvanceSimulatedTimePastTapDelay();
EXPECT_TRUE(IsInTouchToMouseMode());
}
TEST_F(TouchExplorationTest, EntersTouchToMouseModeAfterMoveOutsideSlop) {
int slop = gesture_detector_config_.touch_slop;
int half_slop = slop / 2;
SwitchTouchExplorationMode(true);
EXPECT_FALSE(IsInTouchToMouseMode());
generator_->set_current_screen_location(gfx::Point(11, 12));
generator_->PressTouch();
generator_->MoveTouch(gfx::Point(11 + half_slop, 12));
EXPECT_FALSE(IsInTouchToMouseMode());
generator_->MoveTouch(gfx::Point(11, 12 + half_slop));
EXPECT_FALSE(IsInTouchToMouseMode());
AdvanceSimulatedTimePastTapDelay();
generator_->MoveTouch(gfx::Point(11 + slop + 1, 12));
EXPECT_TRUE(IsInTouchToMouseMode());
}
TEST_F(TouchExplorationTest, OneFingerTap) {
SwitchTouchExplorationMode(true);
gfx::Point location(11, 12);
generator_->set_current_screen_location(location);
generator_->PressTouch();
generator_->ReleaseTouch();
AdvanceSimulatedTimePastTapDelay();
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kMouseMoved);
ASSERT_EQ(0U, events.size());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
EXPECT_EQ(location, GetTouchExplorePoints()[0]);
EXPECT_TRUE(IsInNoFingersDownState());
}
TEST_F(TouchExplorationTest, ActualMouseMovesUnaffected) {
SwitchTouchExplorationMode(true);
gfx::Point location_start(11, 12);
gfx::Point location_end(13, 14);
generator_->set_current_screen_location(location_start);
generator_->PressTouch();
AdvanceSimulatedTimePastTapDelay();
generator_->MoveTouch(location_end);
gfx::Point location_real_mouse_move(15, 16);
ui::MouseEvent mouse_move(ui::EventType::kMouseMoved,
location_real_mouse_move, location_real_mouse_move,
ui::EventTimeForNow(), 0, 0);
generator_->Dispatch(&mouse_move);
generator_->ReleaseTouch();
AdvanceSimulatedTimePastTapDelay();
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kMouseMoved);
std::vector<gfx::Point> touch_explore_points = GetTouchExplorePoints();
ASSERT_EQ(1U, events.size());
ASSERT_EQ(3U, touch_explore_points.size());
EXPECT_EQ(location_start, touch_explore_points[0]);
EXPECT_EQ(location_end, touch_explore_points[1]);
EXPECT_EQ(location_end, touch_explore_points[2]);
// The real mouse move goes through.
EXPECT_EQ(location_real_mouse_move, events[0]->location());
CONFIRM_EVENTS_ARE_MOUSE_AND_EQUAL(events[0], &mouse_move);
EXPECT_FALSE(events[0]->flags() & ui::EF_IS_SYNTHESIZED);
EXPECT_FALSE(events[0]->flags() & ui::EF_TOUCH_ACCESSIBILITY);
EXPECT_TRUE(IsInNoFingersDownState());
}
// Turn the touch exploration mode on in the middle of the touch gesture.
// Confirm that events from the finger which was touching when the mode was
// turned on don't get rewritten.
TEST_F(TouchExplorationTest, TurnOnMidTouch) {
SwitchTouchExplorationMode(false);
generator_->PressTouchId(1);
EXPECT_TRUE(cursor_client()->IsCursorVisible());
ClearCapturedAndGestureEvents();
// Enable touch exploration mode while the first finger is touching the
// screen. Ensure that subsequent events from that first finger are not
// affected by the touch exploration mode, while the touch events from another
// finger get rewritten.
SwitchTouchExplorationMode(true);
ui::TouchEvent touch_move(
ui::EventType::kTouchMoved, gfx::Point(11, 12), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&touch_move);
EXPECT_TRUE(cursor_client()->IsCursorVisible());
EXPECT_FALSE(cursor_client()->IsMouseEventsEnabled());
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(1u, captured_events.size());
CONFIRM_EVENTS_ARE_TOUCH_AND_EQUAL(captured_events[0], &touch_move);
ClearCapturedAndGestureEvents();
// The press from the second finger should get rewritten.
generator_->PressTouchId(2);
AdvanceSimulatedTimePastTapDelay();
EXPECT_TRUE(IsInTouchToMouseMode());
ASSERT_FALSE(GetTouchExplorePoints().empty());
// The release of the first finger shouldn't be affected.
ui::TouchEvent touch_release(
ui::EventType::kTouchReleased, gfx::Point(11, 12), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&touch_release);
captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(1u, captured_events.size());
CONFIRM_EVENTS_ARE_TOUCH_AND_EQUAL(captured_events[0], &touch_release);
ClearCapturedAndGestureEvents();
// The move and release from the second finger should get rewritten.
generator_->MoveTouchId(gfx::Point(13, 14), 2);
generator_->ReleaseTouchId(2);
AdvanceSimulatedTimePastTapDelay();
captured_events = GetCapturedLocatedEvents();
ASSERT_TRUE(captured_events.empty());
ASSERT_EQ(2U, GetTouchExplorePoints().size());
EXPECT_TRUE(IsInNoFingersDownState());
}
// If an event is received after the double-tap timeout has elapsed, but
// before the timer has fired, a mouse move should still be generated.
TEST_F(TouchExplorationTest, TimerFiresLateDuringTouchExploration) {
SwitchTouchExplorationMode(true);
// Make sure the touch is not in a corner of the screen.
generator_->MoveTouch(gfx::Point(100, 200));
// Send a press, then add another finger after the double-tap timeout.
generator_->PressTouchId(1);
simulated_clock_.Advance(base::Milliseconds(1000));
generator_->PressTouchId(2);
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kMouseMoved);
ASSERT_TRUE(events.empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
generator_->ReleaseTouchId(2);
generator_->ReleaseTouchId(1);
AdvanceSimulatedTimePastTapDelay();
EXPECT_TRUE(IsInNoFingersDownState());
}
// If a new tap is received after the double-tap timeout has elapsed from
// a previous tap, but before the timer has fired, a mouse move should
// still be generated from the old tap.
TEST_F(TouchExplorationTest, TimerFiresLateAfterTap) {
SwitchTouchExplorationMode(true);
// Send a tap at location1.
gfx::Point location0(11, 12);
generator_->set_current_screen_location(location0);
generator_->PressTouch();
generator_->ReleaseTouch();
// Send a tap at location2, after the double-tap timeout, but before the
// timer fires.
gfx::Point location1(33, 34);
generator_->set_current_screen_location(location1);
simulated_clock_.Advance(base::Milliseconds(301));
generator_->PressTouch();
generator_->ReleaseTouch();
AdvanceSimulatedTimePastTapDelay();
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kMouseMoved);
ASSERT_TRUE(events.empty());
ASSERT_EQ(2U, GetTouchExplorePoints().size());
EXPECT_EQ(location0, GetTouchExplorePoints()[0]);
EXPECT_EQ(location1, GetTouchExplorePoints()[1]);
EXPECT_TRUE(IsInNoFingersDownState());
}
// Double-tapping should send a touch press and release through to the location
// of the last successful touch exploration.
TEST_F(TouchExplorationTest, DoubleTap) {
SwitchTouchExplorationMode(true);
gfx::Point tap_location(51, 52);
TapAndVerifyTouchExplore(tap_location);
// Now double-tap at a different location. This should result in
// no touches at all, but a click gesture to ChromeVox.
gfx::Point double_tap_location(33, 34);
generator_->set_current_screen_location(double_tap_location);
generator_->PressTouch();
generator_->ReleaseTouch();
generator_->PressTouch();
generator_->ReleaseTouch();
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_TRUE(captured_events.empty());
EXPECT_EQ(ax::mojom::Gesture::kClick, delegate_.GetLastGesture());
EXPECT_TRUE(IsInNoFingersDownState());
}
// The press of the second tap in a double-tap must come within the double-tap
// timeout, but the release of the second tap can come later.
TEST_F(TouchExplorationTest, DoubleTapTiming) {
SwitchTouchExplorationMode(true);
gfx::Point tap_location(51, 52);
TapAndVerifyTouchExplore(tap_location, true);
// The press of the second tap happens in time, but the release does not.
gfx::Point double_tap_location(33, 34);
generator_->set_current_screen_location(double_tap_location);
generator_->PressTouch();
generator_->ReleaseTouch();
simulated_clock_.Advance(gesture_detector_config_.double_tap_timeout -
base::Milliseconds(25));
generator_->PressTouch();
simulated_clock_.Advance(base::Milliseconds(50));
generator_->ReleaseTouch();
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(0U, captured_events.size());
EXPECT_EQ(ax::mojom::Gesture::kClick, delegate_.GetLastGesture());
}
// If an explicit anchor point is set during touch exploration, double-tapping
// should send a 'click' gesture rather than a simulated touch press and
// release.
TEST_F(TouchExplorationTest, DoubleTapWithExplicitAnchorPoint) {
SwitchTouchExplorationMode(true);
gfx::Point tap_location(51, 52);
TapAndVerifyTouchExplore(tap_location, true);
// Now double-tap at a different location. This should result in
// a click gesture.
gfx::Point double_tap_location(33, 34);
generator_->set_current_screen_location(double_tap_location);
generator_->PressTouch();
generator_->ReleaseTouch();
generator_->PressTouch();
generator_->ReleaseTouch();
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(0U, captured_events.size());
EXPECT_TRUE(IsInNoFingersDownState());
EXPECT_EQ(ax::mojom::Gesture::kClick, delegate_.GetLastGesture());
}
// Double-tapping where the user holds their finger down for the second time
// for a longer press should send a touch press and passthrough all further
// events from that finger. Other finger presses should be ignored.
TEST_F(TouchExplorationTest, DoubleTapPassthrough) {
SwitchTouchExplorationMode(true);
gfx::Point tap_location(11, 12);
TapAndVerifyTouchExplore(tap_location);
// Now double-tap and hold at a different location.
// This should result in a single touch press at the location of the tap,
// not at the location of the double-tap.
gfx::Point first_tap_location(13, 14);
generator_->set_current_screen_location(first_tap_location);
generator_->PressTouchId(1);
generator_->ReleaseTouchId(1);
ASSERT_EQ(0U, delegate_.NumPassthroughSounds());
gfx::Point second_tap_location(15, 16);
generator_->set_current_screen_location(second_tap_location);
generator_->PressTouchId(1);
// Advance to the finger passing through.
AdvanceSimulatedTimePastTapDelay();
ASSERT_EQ(1U, delegate_.NumPassthroughSounds());
gfx::Vector2d passthrough_offset = second_tap_location - tap_location;
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(1U, captured_events.size());
EXPECT_EQ(ui::EventType::kTouchPressed, captured_events[0]->type());
EXPECT_EQ(second_tap_location - passthrough_offset,
captured_events[0]->location());
EXPECT_TRUE(captured_events[0]->flags() & ui::EF_TOUCH_ACCESSIBILITY);
ClearCapturedAndGestureEvents();
// All events for the first finger should pass through now, displaced
// relative to the last touch exploration location.
gfx::Point first_move_location(17, 18);
generator_->MoveTouchId(first_move_location, 1);
gfx::Point second_move_location(12, 13);
generator_->MoveTouchId(second_move_location, 1);
captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(2U, captured_events.size());
EXPECT_EQ(ui::EventType::kTouchMoved, captured_events[0]->type());
EXPECT_EQ(first_move_location - passthrough_offset,
captured_events[0]->location());
EXPECT_EQ(ui::EventType::kTouchMoved, captured_events[1]->type());
EXPECT_EQ(second_move_location - passthrough_offset,
captured_events[1]->location());
EXPECT_TRUE(captured_events[1]->flags() & ui::EF_TOUCH_ACCESSIBILITY);
ClearCapturedAndGestureEvents();
// Events for other fingers should do nothing.
generator_->PressTouchId(2);
generator_->PressTouchId(3);
generator_->MoveTouchId(gfx::Point(34, 36), 2);
generator_->ReleaseTouchId(2);
captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(0U, captured_events.size());
// Even with finger 3 still down, events for the first finger should still
// pass through.
gfx::Point third_move_location(14, 15);
generator_->MoveTouchId(third_move_location, 1);
captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(1U, captured_events.size());
EXPECT_EQ(ui::EventType::kTouchMoved, captured_events[0]->type());
EXPECT_EQ(third_move_location - passthrough_offset,
captured_events[0]->location());
EXPECT_TRUE(captured_events[0]->flags() & ui::EF_TOUCH_ACCESSIBILITY);
// No fingers down state is only reached when every finger is lifted.
generator_->ReleaseTouchId(1);
EXPECT_FALSE(IsInNoFingersDownState());
generator_->ReleaseTouchId(3);
EXPECT_TRUE(IsInNoFingersDownState());
// There should have only ever been one pass through earcon played.
ASSERT_EQ(1U, delegate_.NumPassthroughSounds());
}
// Double-tapping, going into passthrough, and holding for the longpress
// time should send a touch press and released (right click)
// to the location of the last successful touch exploration.
TEST_F(TouchExplorationTest, DoubleTapLongPress) {
SwitchTouchExplorationMode(true);
gfx::Point tap_location(11, 12);
TapAndVerifyTouchExplore(tap_location);
// Now double-tap and hold at a different location.
// This should result in a single touch long press and release
// at the location of the tap, not at the location of the double-tap.
// There should be a time delay between the touch press and release.
gfx::Point first_tap_location(33, 34);
generator_->set_current_screen_location(first_tap_location);
generator_->PressTouch();
generator_->ReleaseTouch();
gfx::Point second_tap_location(23, 24);
generator_->set_current_screen_location(second_tap_location);
generator_->PressTouch();
// Advance to the finger passing through, and then to the longpress timeout.
AdvanceSimulatedTimePastTapDelay();
simulated_clock_.Advance(gesture_detector_config_.longpress_timeout);
generator_->ReleaseTouch();
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(2U, captured_events.size());
EXPECT_EQ(ui::EventType::kTouchPressed, captured_events[0]->type());
EXPECT_EQ(tap_location, captured_events[0]->location());
EXPECT_TRUE(captured_events[0]->flags() & ui::EF_TOUCH_ACCESSIBILITY);
base::TimeTicks pressed_time = captured_events[0]->time_stamp();
EXPECT_EQ(ui::EventType::kTouchReleased, captured_events[1]->type());
EXPECT_EQ(tap_location, captured_events[1]->location());
EXPECT_TRUE(captured_events[1]->flags() & ui::EF_TOUCH_ACCESSIBILITY);
base::TimeTicks released_time = captured_events[1]->time_stamp();
EXPECT_EQ(released_time - pressed_time,
gesture_detector_config_.longpress_timeout);
}
// Single-tapping should send a touch press and release through to the location
// of the last successful touch exploration if the grace period has not
// elapsed.
TEST_F(TouchExplorationTest, SingleTap) {
SwitchTouchExplorationMode(true);
// Tap once to simulate a touch explore point.
gfx::Point initial_location(11, 12);
generator_->set_current_screen_location(initial_location);
generator_->PressTouch();
AdvanceSimulatedTimePastTapDelay();
ASSERT_EQ(1U, GetTouchExplorePoints().size());
// Move to another location for single tap
gfx::Point tap_location(22, 23);
generator_->MoveTouch(tap_location);
generator_->ReleaseTouch();
// Allow time to pass within the grace period of releasing before
// tapping again.
gfx::Point final_location(33, 34);
generator_->set_current_screen_location(final_location);
simulated_clock_.Advance(base::Milliseconds(250));
generator_->PressTouch();
generator_->ReleaseTouch();
ASSERT_EQ(3U, GetTouchExplorePoints().size());
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_TRUE(captured_events.empty());
EXPECT_EQ(ax::mojom::Gesture::kClick, delegate_.GetLastGesture());
}
// Double-tapping without coming from touch exploration (no previous touch
// exploration event) should not generate any events.
TEST_F(TouchExplorationTest, DoubleTapNoTouchExplore) {
SwitchTouchExplorationMode(true);
// Double-tap without any previous touch.
// Touch exploration mode has not been entered, so there is no previous
// touch exploration event. The double-tap should be discarded, and no events
// should be generated at all.
gfx::Point double_tap_location(33, 34);
generator_->set_current_screen_location(double_tap_location);
generator_->PressTouch();
generator_->ReleaseTouch();
generator_->PressTouch();
// Since the state stays in single_tap_released, we need to make sure the
// tap timer doesn't fire and set the state to no fingers down (since there
// is still a finger down).
AdvanceSimulatedTimePastPotentialTapDelay();
EXPECT_FALSE(IsInNoFingersDownState());
generator_->ReleaseTouch();
EXPECT_TRUE(IsInNoFingersDownState());
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(0U, captured_events.size());
}
// Tapping and releasing with a second finger when in touch exploration mode
// should send a touch press and released to the location of the last
// successful touch exploration and return to touch explore.
TEST_F(TouchExplorationTest, SplitTap) {
SwitchTouchExplorationMode(true);
gfx::Point initial_touch_location(11, 12);
gfx::Point second_touch_location(33, 34);
// Tap and hold at one location, and get a mouse move event in touch explore.
EnterTouchExplorationModeAtLocation(initial_touch_location);
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kMouseMoved);
ASSERT_TRUE(events.empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
EXPECT_EQ(initial_touch_location, GetTouchExplorePoints()[0]);
ClearCapturedAndGestureEvents();
EXPECT_TRUE(IsInTouchToMouseMode());
// Now tap and release at a different location. This should result in a
// single touch and release at the location of the first (held) tap,
// not at the location of the second tap and release.
// After the release, there is still a finger in touch explore mode.
ui::TouchEvent split_tap_press(
ui::EventType::kTouchPressed, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&split_tap_press);
// To simulate the behavior of the real device, we manually disable
// mouse events. To not rely on manually setting the state, this is also
// tested in touch_exploration_controller_browsertest.
cursor_client()->DisableMouseEvents();
EXPECT_FALSE(cursor_client()->IsMouseEventsEnabled());
EXPECT_FALSE(cursor_client()->IsCursorVisible());
EXPECT_FALSE(IsInGestureInProgressState());
ui::TouchEvent split_tap_release(
ui::EventType::kTouchReleased, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&split_tap_release);
// Releasing the second finger should re-enable mouse events putting us
// back into the touch exploration mode.
EXPECT_TRUE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInNoFingersDownState());
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_TRUE(captured_events.empty());
EXPECT_EQ(ax::mojom::Gesture::kClick, delegate_.GetLastGesture());
ClearCapturedAndGestureEvents();
ui::TouchEvent touch_explore_release(
ui::EventType::kTouchReleased, initial_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&touch_explore_release);
AdvanceSimulatedTimePastTapDelay();
EXPECT_TRUE(IsInNoFingersDownState());
}
// If split tap is started but the touch explore finger is released first,
// there should still be a touch press and release sent to the location of
// the last successful touch exploration.
// Both fingers should be released after the click goes through.
TEST_F(TouchExplorationTest, SplitTapRelease) {
SwitchTouchExplorationMode(true);
gfx::Point initial_touch_location(11, 12);
gfx::Point second_touch_location(33, 34);
// Tap and hold at one location, and get a mouse move event in touch explore.
EnterTouchExplorationModeAtLocation(initial_touch_location);
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kMouseMoved);
ASSERT_TRUE(events.empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
// Now tap at a different location. Release at the first location,
// then release at the second. This should result in a
// click gesture to ChromeVox.
ui::TouchEvent split_tap_press(
ui::EventType::kTouchPressed, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&split_tap_press);
ui::TouchEvent touch_explore_release(
ui::EventType::kTouchReleased, initial_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&touch_explore_release);
ui::TouchEvent split_tap_release(
ui::EventType::kTouchReleased, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&split_tap_release);
EXPECT_TRUE(IsInNoFingersDownState());
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
EXPECT_TRUE(captured_events.empty());
EXPECT_EQ(ax::mojom::Gesture::kClick, delegate_.GetLastGesture());
}
TEST_F(TouchExplorationTest, SplitTapMultiFinger) {
SwitchTouchExplorationMode(true);
gfx::Point initial_touch_location(11, 12);
gfx::Point second_touch_location(33, 34);
gfx::Point third_touch_location(16, 17);
// Tap and hold at one location, and get a mouse move event in touch explore.
EnterTouchExplorationModeAtLocation(initial_touch_location);
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kMouseMoved);
ASSERT_TRUE(events.empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
EXPECT_EQ(initial_touch_location, GetTouchExplorePoints()[0]);
ClearCapturedAndGestureEvents();
// Now tap at a different location
ui::TouchEvent split_tap_press(
ui::EventType::kTouchPressed, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&split_tap_press);
simulated_clock_.Advance(gesture_detector_config_.longpress_timeout);
// Placing a third finger on the screen should cancel the split tap and
// enter the wait state.
ui::TouchEvent third_press(
ui::EventType::kTouchPressed, third_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 2));
generator_->Dispatch(&third_press);
// When all three fingers are released, no events should be captured.
// All fingers should then be up.
ui::TouchEvent touch_explore_release(
ui::EventType::kTouchReleased, initial_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&touch_explore_release);
ui::TouchEvent split_tap_release(
ui::EventType::kTouchReleased, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&split_tap_release);
ui::TouchEvent third_tap_release(
ui::EventType::kTouchReleased, third_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 2));
generator_->Dispatch(&third_tap_release);
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(0U, captured_events.size());
EXPECT_TRUE(IsInNoFingersDownState());
}
TEST_F(TouchExplorationTest, SplitTapLeaveSlop) {
SwitchTouchExplorationMode(true);
gfx::Point first_touch_location(11, 12);
gfx::Point second_touch_location(33, 34);
gfx::Point first_move_location(
first_touch_location.x() + gesture_detector_config_.touch_slop * 3 + 1,
first_touch_location.y());
gfx::Point second_move_location(
second_touch_location.x() + gesture_detector_config_.touch_slop * 3 + 1,
second_touch_location.y());
// Tap and hold at one location, and get a mouse move event in touch explore.
EnterTouchExplorationModeAtLocation(first_touch_location);
ClearCapturedAndGestureEvents();
// Now tap at a different location for split tap.
ui::TouchEvent split_tap_press(
ui::EventType::kTouchPressed, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&split_tap_press);
// Move the first finger out of slop and release both fingers. The split
// tap should have been cancelled.
ui::TouchEvent first_touch_move(
ui::EventType::kTouchMoved, first_move_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&first_touch_move);
ui::TouchEvent first_touch_release(
ui::EventType::kTouchReleased, first_move_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&first_touch_release);
ui::TouchEvent second_touch_release(
ui::EventType::kTouchReleased, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&second_touch_release);
std::vector<ui::LocatedEvent*> captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(0U, captured_events.size());
EXPECT_TRUE(IsInNoFingersDownState());
// Now do the same, but moving the split tap finger out of slop
EnterTouchExplorationModeAtLocation(first_touch_location);
ClearCapturedAndGestureEvents();
ui::TouchEvent split_tap_press2(
ui::EventType::kTouchPressed, second_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&split_tap_press2);
// Move the second finger out of slop and release both fingers. The split
// tap should have been cancelled.
ui::TouchEvent second_touch_move2(
ui::EventType::kTouchMoved, second_move_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&second_touch_move2);
ui::TouchEvent first_touch_release2(
ui::EventType::kTouchReleased, first_touch_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&first_touch_release2);
ui::TouchEvent second_touch_release2(
ui::EventType::kTouchReleased, second_move_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&second_touch_release2);
captured_events = GetCapturedLocatedEvents();
ASSERT_EQ(0U, captured_events.size());
EXPECT_TRUE(IsInNoFingersDownState());
}
// Finger must have moved more than slop, faster than the minimum swipe
// velocity, and before the tap timer fires in order to enter
// GestureInProgress state. Otherwise, if the tap timer fires before the a
// gesture is completed, enter touch exploration.
TEST_F(TouchExplorationTest, EnterGestureInProgressState) {
SwitchTouchExplorationMode(true);
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
float distance = gesture_detector_config_.touch_slop + 1;
ui::TouchEvent first_press(
ui::EventType::kTouchPressed, gfx::Point(0, 1), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
gfx::Point second_location(distance / 2, 1);
gfx::Point third_location(distance, 1);
gfx::Point touch_exploration_location(20, 21);
generator_->Dispatch(&first_press);
simulated_clock_.Advance(base::Milliseconds(10));
// Since we are not out of the touch slop yet, we should not be in gesture in
// progress.
generator_->MoveTouch(second_location);
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
simulated_clock_.Advance(base::Milliseconds(10));
// Once we are out of slop, we should be in GestureInProgress.
generator_->MoveTouch(third_location);
EXPECT_TRUE(IsInGestureInProgressState());
EXPECT_FALSE(IsInTouchToMouseMode());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
// Exit out of gesture mode once grace period is over and enter touch
// exploration. There should be a touch explore gesture when entering touch
// exploration and also for the touch move.
AdvanceSimulatedTimePastTapDelay();
generator_->MoveTouch(touch_exploration_location);
ASSERT_TRUE(captured_events.empty());
EXPECT_EQ(2U, GetTouchExplorePoints().size());
EXPECT_TRUE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
}
// A swipe+direction gesture should trigger a Shift+Search+Direction
// keyboard event.
TEST_F(TouchExplorationTest, GestureSwipe) {
SwitchTouchExplorationMode(true);
// Test all four swipe directions with 1 to 4 fingers.
struct GestureInfo {
int move_x;
int move_y;
int num_fingers;
ax::mojom::Gesture expected_gesture;
} gestures_to_test[] = {
{-1, 0, 1, ax::mojom::Gesture::kSwipeLeft1},
{0, -1, 1, ax::mojom::Gesture::kSwipeUp1},
{1, 0, 1, ax::mojom::Gesture::kSwipeRight1},
{0, 1, 1, ax::mojom::Gesture::kSwipeDown1},
{-1, 0, 2, ax::mojom::Gesture::kSwipeLeft2},
{0, -1, 2, ax::mojom::Gesture::kSwipeUp2},
{1, 0, 2, ax::mojom::Gesture::kSwipeRight2},
{0, 1, 2, ax::mojom::Gesture::kSwipeDown2},
{-1, 0, 3, ax::mojom::Gesture::kSwipeLeft3},
{0, -1, 3, ax::mojom::Gesture::kSwipeUp3},
{1, 0, 3, ax::mojom::Gesture::kSwipeRight3},
{0, 1, 3, ax::mojom::Gesture::kSwipeDown3},
{-1, 0, 4, ax::mojom::Gesture::kSwipeLeft4},
{0, -1, 4, ax::mojom::Gesture::kSwipeUp4},
{1, 0, 4, ax::mojom::Gesture::kSwipeRight4},
{0, 1, 4, ax::mojom::Gesture::kSwipeDown4},
};
// This value was taken from gesture_recognizer_unittest.cc in a swipe
// detector test, since it seems to be about the right amount to get a swipe.
const int kSteps = 15;
for (size_t i = 0; i < std::size(gestures_to_test); ++i) {
const float distance = 2 * gesture_detector_config_.touch_slop + 1;
int move_x = gestures_to_test[i].move_x * distance;
int move_y = gestures_to_test[i].move_y * distance;
int num_fingers = gestures_to_test[i].num_fingers;
ax::mojom::Gesture expected_gesture = gestures_to_test[i].expected_gesture;
std::vector<gfx::Point> start_points;
for (int j = 0; j < num_fingers; j++) {
start_points.push_back(gfx::Point(j * 10 + 100, j * 10 + 200));
}
gfx::Point* start_points_array = &start_points[0];
// A swipe is made when a fling starts
float delta_time =
distance / gesture_detector_config_.maximum_fling_velocity;
// delta_time is in seconds, so we convert to ms.
int delta_time_ms = floor(delta_time * 1000);
generator_->GestureMultiFingerScroll(num_fingers, start_points_array,
delta_time_ms, kSteps, move_x, move_y);
EXPECT_EQ(expected_gesture, delegate_.GetLastGesture());
EXPECT_TRUE(IsInNoFingersDownState());
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
ClearCapturedAndGestureEvents();
}
}
TEST_F(TouchExplorationTest, GestureSwipePortrit) {
// Rotate the window 90-degrees counter-clockwise.
root_window()->GetHost()->SetRootTransform(gfx::Transform::RowMajor(
0, 1, 0, 0, -1, 0, 0, root_window()->bounds().height(), 0, 0, 0, 0, 0, 0,
0, 0));
SwitchTouchExplorationMode(true);
// Test 2-4 finger gestures.
struct GestureInfo {
int move_x;
int move_y;
int num_fingers;
ax::mojom::Gesture expected_gesture;
} gestures_to_test[] = {
{-1, 0, 2, ax::mojom::Gesture::kSwipeDown2},
{0, -1, 2, ax::mojom::Gesture::kSwipeLeft2},
{1, 0, 2, ax::mojom::Gesture::kSwipeUp2},
{0, 1, 2, ax::mojom::Gesture::kSwipeRight2},
{-1, 0, 3, ax::mojom::Gesture::kSwipeDown3},
{0, -1, 3, ax::mojom::Gesture::kSwipeLeft3},
{1, 0, 3, ax::mojom::Gesture::kSwipeUp3},
{0, 1, 3, ax::mojom::Gesture::kSwipeRight3},
{-1, 0, 4, ax::mojom::Gesture::kSwipeDown4},
{0, -1, 4, ax::mojom::Gesture::kSwipeLeft4},
{1, 0, 4, ax::mojom::Gesture::kSwipeUp4},
{0, 1, 4, ax::mojom::Gesture::kSwipeRight4},
};
// This value was taken from gesture_recognizer_unittest.cc in a swipe
// detector test, since it seems to be about the right amount to get a swipe.
const int kSteps = 15;
for (size_t i = 0; i < std::size(gestures_to_test); ++i) {
const float distance = 2 * gesture_detector_config_.touch_slop + 1;
int move_x = gestures_to_test[i].move_x * distance;
int move_y = gestures_to_test[i].move_y * distance;
int num_fingers = gestures_to_test[i].num_fingers;
ax::mojom::Gesture expected_gesture = gestures_to_test[i].expected_gesture;
std::vector<gfx::Point> start_points;
for (int j = 0; j < num_fingers; j++) {
start_points.push_back(gfx::Point(j * 10 + 100, j * 10 + 200));
}
gfx::Point* start_points_array = &start_points[0];
// A swipe is made when a fling starts
float delta_time =
distance / gesture_detector_config_.maximum_fling_velocity;
// delta_time is in seconds, so we convert to ms.
int delta_time_ms = floor(delta_time * 1000);
generator_->GestureMultiFingerScroll(num_fingers, start_points_array,
delta_time_ms, kSteps, move_x, move_y);
EXPECT_EQ(expected_gesture, delegate_.GetLastGesture());
EXPECT_TRUE(IsInNoFingersDownState());
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
ClearCapturedAndGestureEvents();
}
}
TEST_F(TouchExplorationTest, AllFingerPermutations) {
SwitchTouchExplorationMode(true);
SuppressVLOGs(true);
// We will test all permutations of events from one finger
// to ensure that we return to NO_FINGERS_DOWN when fingers have been
// released.
std::vector<std::unique_ptr<ui::TouchEvent>> all_events;
// A copy of all events list which can be modified without destrying events.
std::vector<ui::TouchEvent*> queued_events;
for (int touch_id = 0; touch_id < 3; ++touch_id) {
all_events.clear();
int x = 10 * touch_id + 1;
int y = 10 * touch_id + 2;
all_events.push_back(std::make_unique<ui::TouchEvent>(
ui::EventType::kTouchPressed, gfx::Point(x++, y++), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, touch_id)));
all_events.push_back(std::make_unique<ui::TouchEvent>(
ui::EventType::kTouchMoved, gfx::Point(x++, y++), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, touch_id)));
all_events.push_back(std::make_unique<ui::TouchEvent>(
ui::EventType::kTouchReleased, gfx::Point(x, y), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, touch_id)));
// I'm going to explain this algorithm, and use an example in parentheses.
// The example will be all permutations of a b c d.
// There are four letters and 4! = 24 permutations.
const int num_events = all_events.size();
const int num_permutations = Factorial(num_events);
for (int p = 0; p < num_permutations; p++) {
std::vector<bool> fingers_pressed(3, false);
// Initialize queued_events.
for (size_t i = 0; i < all_events.size(); ++i) {
queued_events.push_back(all_events[i].get());
}
int current_num_permutations = num_permutations;
for (int events_left = num_events; events_left > 0; events_left--) {
// |p| indexes to each permutation when there are num_permutations
// permutations. (e.g. 0 is abcd, 1 is abdc, 2 is acbd, 3 is acdb...)
// But how do we find the index for the current number of permutations?
// To find the permutation within the part of the sequence we're
// currently looking at, we need a number between 0 and
// |current_num_permutations| - 1.
// (e.g. if we already chose the first letter, there are 3! = 6
// options left, so we do p % 6. So |current_permutation| would go
// from 0 to 5 and then reset to 0 again, for all combinations of
// whichever three letters are remaining, as we loop through the
// permutations)
int current_permutation = p % current_num_permutations;
// Since this is is the total number of permutations starting with
// this event and including future events, there could be multiple
// values of current_permutation that will generate the same event
// in this iteration.
// (e.g. If we chose 'a' but have b c d to choose from, we choose b when
// |current_permutation| = 0, 1 and c when |current_permutation| = 2, 3.
// Note that each letter gets two numbers, which is the next
// current_num_permutations, 2! for the two letters left.)
// Branching out from the first event, there are num_permutations
// permutations, and each value of |p| is associated with one of these
// permutations. However, once the first event is chosen, there
// are now |num_events| - 1 events left, so the number of permutations
// for the rest of the events changes, and will always be equal to
// the factorial of the events_left.
// (e.g. There are 3! = 6 permutations that start with 'a', so if we
// start with 'a' there will be 6 ways to then choose from b c d.)
// So we now set-up for the next iteration by setting
// current_num_permutations to the factorial of the next number of
// events left.
current_num_permutations /= events_left;
// To figure out what current event we want to choose, we integer
// divide the current permutation by the next current_num_permutations.
// (e.g. If there are 4 letters a b c d and 24 permutations, we divide
// by 24/4 = 6. Values 0 to 5 when divided by 6 equals 0, so the first
// 6 permutations start with 'a', and the last 6 will start with 'd'.
// Note that there are 6 that start with 'a' because there are 6
// permutations for the next three letters that follow 'a'.)
int index = current_permutation / current_num_permutations;
ui::TouchEvent* next_dispatch = queued_events[index];
ASSERT_TRUE(next_dispatch != NULL);
// |next_dispatch| has to be put in this container so that its time
// stamp can be changed to this point in the test, when it is being
// dispatched..
ui::EventTestApi test_dispatch(next_dispatch);
test_dispatch.set_time_stamp(Now());
generator_->Dispatch(next_dispatch);
queued_events.erase(queued_events.begin() + index);
// Keep track of what fingers have been pressed, to release
// only those fingers at the end, so the check for being in
// no fingers down can be accurate.
if (next_dispatch->type() == ui::EventType::kTouchPressed) {
fingers_pressed[next_dispatch->pointer_details().id] = true;
} else if (next_dispatch->type() == ui::EventType::kTouchReleased) {
fingers_pressed[next_dispatch->pointer_details().id] = false;
}
}
ASSERT_EQ(queued_events.size(), 0u);
// Release fingers recorded as pressed.
for (int j = 0; j < int(fingers_pressed.size()); j++) {
if (fingers_pressed[j] == true) {
generator_->ReleaseTouchId(j);
fingers_pressed[j] = false;
}
}
AdvanceSimulatedTimePastPotentialTapDelay();
EXPECT_TRUE(IsInNoFingersDownState());
ClearCapturedAndGestureEvents();
}
}
}
// With the simple swipe gestures, if additional fingers are added and the tap
// timer times out, then the state should change to the wait for one finger
// state.
TEST_F(TouchExplorationTest, GestureAddedFinger) {
SwitchTouchExplorationMode(true);
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
float distance = gesture_detector_config_.touch_slop + 1;
ui::TouchEvent first_press(
ui::EventType::kTouchPressed, gfx::Point(100, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&first_press);
simulated_clock_.Advance(base::Milliseconds(10));
gfx::Point second_location(100 + distance, 200);
generator_->MoveTouch(second_location);
EXPECT_TRUE(IsInGestureInProgressState());
EXPECT_FALSE(IsInTouchToMouseMode());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
// Generate a second press, but time out past the gesture period so that
// gestures are prevented from continuing to go through.
ui::TouchEvent second_press(
ui::EventType::kTouchPressed, gfx::Point(20, 21), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&second_press);
AdvanceSimulatedTimePastTapDelay();
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_FALSE(IsInTouchToMouseMode());
ASSERT_EQ(0U, captured_events.size());
}
TEST_F(TouchExplorationTest, EnterSlideGestureState) {
SwitchTouchExplorationMode(true);
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
int window_right = BoundsOfRootWindowInDIP().right();
float distance = gesture_detector_config_.touch_slop + 1;
ui::TouchEvent first_press(
ui::EventType::kTouchPressed, gfx::Point(window_right, 1), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
gfx::Point second_location(window_right, 1 + distance / 2);
gfx::Point third_location(window_right, 1 + distance);
gfx::Point fourth_location(window_right, 35);
generator_->Dispatch(&first_press);
simulated_clock_.Advance(base::Milliseconds(10));
// Since we haven't moved past slop yet, we should not be in slide gesture.
generator_->MoveTouch(second_location);
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_FALSE(IsInSlideGestureState());
simulated_clock_.Advance(base::Milliseconds(10));
// Once we are out of slop, we should be in slide gesture since we are along
// the edge of the screen.
generator_->MoveTouch(third_location);
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_TRUE(IsInSlideGestureState());
EXPECT_FALSE(IsInTouchToMouseMode());
// Now that we are in slide gesture, we can adjust the volume.
generator_->MoveTouch(fourth_location);
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
// Since we are at the right edge of the screen, but the sound timer has not
// elapsed, there should have been a sound that fired and a volume
// change.
size_t num_adjust_sounds = delegate_.NumAdjustSounds();
ASSERT_EQ(1U, num_adjust_sounds);
ASSERT_EQ(1U, delegate_.VolumeChanges().size());
// Exit out of slide gesture once touch is lifted, but not before even if the
// grace period is over.
AdvanceSimulatedTimePastPotentialTapDelay();
ASSERT_EQ(0U, captured_events.size());
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_TRUE(IsInSlideGestureState());
generator_->ReleaseTouch();
ASSERT_EQ(0U, captured_events.size());
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_FALSE(IsInSlideGestureState());
}
// If a press + move occurred outside the boundaries, but within the slop
// boundaries and then moved into the boundaries of an edge, there still should
// not be a slide gesture.
TEST_F(TouchExplorationTest, AvoidEnteringSlideGesture) {
SwitchTouchExplorationMode(true);
gfx::Rect window = BoundsOfRootWindowInDIP();
float distance = gesture_detector_config_.touch_slop + 1;
ui::TouchEvent first_press(
ui::EventType::kTouchPressed,
gfx::Point(window.right() - GetSlopDistanceFromEdge(), 1), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
gfx::Point out_of_slop(window.right() - GetSlopDistanceFromEdge() + distance,
1);
gfx::Point into_boundaries(window.right() - GetMaxDistanceFromEdge() / 2, 1);
generator_->Dispatch(&first_press);
simulated_clock_.Advance(base::Milliseconds(10));
generator_->MoveTouch(out_of_slop);
EXPECT_FALSE(IsInTouchToMouseMode());
EXPECT_TRUE(IsInGestureInProgressState());
EXPECT_FALSE(IsInSlideGestureState());
simulated_clock_.Advance(base::Milliseconds(10));
// Since we did not start moving while in the boundaries, we should not be in
// slide gestures.
generator_->MoveTouch(into_boundaries);
EXPECT_TRUE(IsInGestureInProgressState());
EXPECT_FALSE(IsInSlideGestureState());
EXPECT_FALSE(IsInTouchToMouseMode());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
generator_->ReleaseTouch();
}
// If the slide gesture begins within the boundaries and then moves
// SlopDistanceFromEdge there should still be a sound change. If the finger
// moves into the center screen, there should no longer be a sound change but it
// should still be in slide gesture. If the finger moves back into the edges
// without lifting, it should start changing sound again.
TEST_F(TouchExplorationTest, TestingBoundaries) {
SwitchTouchExplorationMode(true);
gfx::Rect window = BoundsOfRootWindowInDIP();
gfx::Point initial_press(window.right() - GetMaxDistanceFromEdge() / 2, 1);
gfx::Point center_screen(window.right() / 2, window.bottom() / 2);
ui::TouchEvent first_press(
ui::EventType::kTouchPressed, initial_press, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&first_press);
simulated_clock_.Advance(base::Milliseconds(10));
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_FALSE(IsInSlideGestureState());
EXPECT_FALSE(IsInTouchToMouseMode());
// Move past the touch slop to begin slide gestures.
// + slop + 1 to actually leave slop.
gfx::Point touch_move(
initial_press.x(),
initial_press.y() + gesture_detector_config_.touch_slop + 1);
generator_->MoveTouch(touch_move);
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_TRUE(IsInSlideGestureState());
EXPECT_FALSE(IsInTouchToMouseMode());
simulated_clock_.Advance(base::Milliseconds(10));
// Move the touch into slop boundaries. It should still be in slide gestures
// and adjust the volume.
gfx::Point into_slop_boundaries(
window.right() - GetSlopDistanceFromEdge() / 2, 1);
generator_->MoveTouch(into_slop_boundaries);
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_TRUE(IsInSlideGestureState());
EXPECT_FALSE(IsInTouchToMouseMode());
// The sound is rate limiting so it only activates every 150ms.
simulated_clock_.Advance(base::Milliseconds(200));
size_t num_adjust_sounds = delegate_.NumAdjustSounds();
ASSERT_EQ(1U, num_adjust_sounds);
ASSERT_EQ(1U, delegate_.VolumeChanges().size());
// Move the touch into the center of the window. It should still be in slide
// gestures, but there should not be anymore volume adjustments.
generator_->MoveTouch(center_screen);
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_TRUE(IsInSlideGestureState());
EXPECT_FALSE(IsInTouchToMouseMode());
simulated_clock_.Advance(base::Milliseconds(200));
num_adjust_sounds = delegate_.NumAdjustSounds();
ASSERT_EQ(1U, num_adjust_sounds);
ASSERT_EQ(1U, delegate_.VolumeChanges().size());
// Move the touch back into slop edge distance and volume should be changing
// again, one volume change for each new move.
generator_->MoveTouch(into_slop_boundaries);
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_TRUE(IsInSlideGestureState());
EXPECT_FALSE(IsInTouchToMouseMode());
generator_->MoveTouch(
gfx::Point(into_slop_boundaries.x() + gesture_detector_config_.touch_slop,
into_slop_boundaries.y()));
simulated_clock_.Advance(base::Milliseconds(200));
num_adjust_sounds = delegate_.NumAdjustSounds();
ASSERT_EQ(2U, num_adjust_sounds);
ASSERT_EQ(3U, delegate_.VolumeChanges().size());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
generator_->ReleaseTouch();
}
// Even if the gesture starts within bounds, if it has not moved past slop
// within the grace period, it should go to touch exploration.
TEST_F(TouchExplorationTest, InBoundariesTouchExploration) {
SwitchTouchExplorationMode(true);
gfx::Rect window = BoundsOfRootWindowInDIP();
gfx::Point initial_press(window.right() - GetMaxDistanceFromEdge() / 2, 1);
ui::TouchEvent first_press(
ui::EventType::kTouchPressed, initial_press, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&first_press);
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_FALSE(IsInSlideGestureState());
EXPECT_FALSE(IsInTouchToMouseMode());
AdvanceSimulatedTimePastTapDelay();
EXPECT_FALSE(IsInGestureInProgressState());
EXPECT_FALSE(IsInSlideGestureState());
EXPECT_TRUE(IsInTouchToMouseMode());
}
// If two fingers tap the screen at the same time and release before the tap
// timer runs out, a control key event should be sent to silence chromevox.
TEST_F(TouchExplorationTest, TwoFingerTap) {
SwitchTouchExplorationMode(true);
generator_->set_current_screen_location(gfx::Point(101, 102));
generator_->PressTouchId(1);
EXPECT_FALSE(IsInTwoFingerTapState());
generator_->PressTouchId(2);
EXPECT_TRUE(IsInTwoFingerTapState());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
generator_->ReleaseTouchId(1);
EXPECT_TRUE(IsInTwoFingerTapState());
generator_->ReleaseTouchId(2);
EXPECT_EQ(0U, captured_events.size());
ASSERT_EQ(ax::mojom::Gesture::kTap2, delegate_.GetLastGesture());
}
// If the fingers are not released before the tap timer runs out, a control
// keyevent is not sent and the state will no longer be in two finger tap.
TEST_F(TouchExplorationTest, TwoFingerTapAndHold) {
SwitchTouchExplorationMode(true);
generator_->PressTouchId(1);
EXPECT_FALSE(IsInTwoFingerTapState());
generator_->PressTouchId(2);
EXPECT_TRUE(IsInTwoFingerTapState());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
AdvanceSimulatedTimePastTapDelay();
// Since the tap delay has elapsed, it should no longer be in two finger tap.
EXPECT_FALSE(IsInTwoFingerTapState());
}
// The next two tests set up two finger swipes to happen. If one of the fingers
// moves out of slop before the tap timer fires, a two finger tap is not made.
// In this first test, the first finger placed will move out of slop.
TEST_F(TouchExplorationTest, TwoFingerTapAndMoveFirstFinger) {
SwitchTouchExplorationMode(true);
// Once one of the fingers leaves slop, it should no longer be in two finger
// tap.
ui::TouchEvent first_press_id_1(
ui::EventType::kTouchPressed, gfx::Point(100, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
ui::TouchEvent first_press_id_2(
ui::EventType::kTouchPressed, gfx::Point(110, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 2));
ui::TouchEvent slop_move_id_1(
ui::EventType::kTouchMoved,
gfx::Point(100 + gesture_detector_config_.touch_slop, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
ui::TouchEvent slop_move_id_2(
ui::EventType::kTouchMoved,
gfx::Point(110 + gesture_detector_config_.touch_slop, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 2));
ui::TouchEvent out_slop_id_1(
ui::EventType::kTouchMoved,
gfx::Point(100 + gesture_detector_config_.touch_slop + 1, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
// Dispatch the inital presses.
generator_->Dispatch(&first_press_id_1);
EXPECT_FALSE(IsInTwoFingerTapState());
generator_->Dispatch(&first_press_id_2);
EXPECT_TRUE(IsInTwoFingerTapState());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
// The presses have not moved out of slop yet so it should still be in
// TwoFingerTap.
generator_->Dispatch(&slop_move_id_1);
EXPECT_TRUE(IsInTwoFingerTapState());
generator_->Dispatch(&slop_move_id_2);
EXPECT_TRUE(IsInTwoFingerTapState());
// Once one of the fingers moves out of slop, we are no longer in
// TwoFingerTap.
generator_->Dispatch(&out_slop_id_1);
EXPECT_FALSE(IsInTwoFingerTapState());
}
// Similar test to the previous test except the second finger placed will be the
// one to move out of slop.
TEST_F(TouchExplorationTest, TwoFingerTapAndMoveSecondFinger) {
SwitchTouchExplorationMode(true);
// Once one of the fingers leaves slop, it should no longer be in two finger
// tap.
ui::TouchEvent first_press_id_1(
ui::EventType::kTouchPressed, gfx::Point(100, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
ui::TouchEvent first_press_id_2(
ui::EventType::kTouchPressed, gfx::Point(110, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 2));
ui::TouchEvent out_slop_id_2(
ui::EventType::kTouchMoved,
gfx::Point(100 + gesture_detector_config_.touch_slop + 1, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
generator_->Dispatch(&first_press_id_1);
EXPECT_FALSE(IsInTwoFingerTapState());
generator_->Dispatch(&first_press_id_2);
EXPECT_TRUE(IsInTwoFingerTapState());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(0U, captured_events.size());
generator_->Dispatch(&out_slop_id_2);
EXPECT_FALSE(IsInTwoFingerTapState());
}
TEST_F(TouchExplorationTest, ExclusionArea) {
SwitchTouchExplorationMode(true);
gfx::Rect window = BoundsOfRootWindowInDIP();
gfx::Rect exclude = window;
exclude.Inset(gfx::Insets::TLBR(0, 0, 30, 0));
SetExcludeBounds(exclude);
gfx::Point in_pt = exclude.CenterPoint();
gfx::Point in_mv_pt(in_pt.x(), (in_pt.y() + exclude.bottom()) / 2);
gfx::Point out_pt(in_pt.x(), exclude.bottom() + 20);
gfx::Point out_mv_pt(in_pt.x(), exclude.bottom() + 10);
// Motion starting in exclusion bounds is passed-through unchanged.
{
generator_->set_current_screen_location(in_pt);
generator_->PressTouchId(0);
AdvanceSimulatedTimePastPotentialTapDelay();
generator_->MoveTouchId(out_mv_pt, 0);
generator_->ReleaseTouchId(0);
EXPECT_TRUE(IsInNoFingersDownState());
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(3U, captured_events.size());
EXPECT_EQ(ui::EventType::kTouchPressed, captured_events[0]->type());
EXPECT_EQ(ui::EventType::kTouchMoved, captured_events[1]->type());
EXPECT_EQ(ui::EventType::kTouchReleased, captured_events[2]->type());
ClearCapturedAndGestureEvents();
}
// Complete motion outside exclusion is rewritten.
{
generator_->set_current_screen_location(out_pt);
generator_->PressTouchId(0);
AdvanceSimulatedTimePastTapDelay();
generator_->MoveTouchId(out_mv_pt, 0);
generator_->ReleaseTouchId(0);
AdvanceSimulatedTimePastTapDelay();
EXPECT_TRUE(IsInNoFingersDownState());
const EventList& captured_events = GetCapturedEvents();
ASSERT_TRUE(captured_events.empty());
ASSERT_EQ(3U, GetTouchExplorePoints().size());
ClearCapturedAndGestureEvents();
}
// For a motion starting outside: outside events are rewritten, inside
// events are discarded unless they end the motion.
{
// finger 0 down outside, moves inside.
generator_->set_current_screen_location(out_pt);
generator_->PressTouchId(0);
AdvanceSimulatedTimePastTapDelay();
generator_->MoveTouchId(out_mv_pt, 0);
generator_->MoveTouchId(in_mv_pt, 0);
ASSERT_TRUE(GetCapturedEvents().empty());
ASSERT_EQ(2U, GetTouchExplorePoints().size());
ClearCapturedAndGestureEvents();
// finger 1 down inside, moves outside
generator_->set_current_screen_location(in_pt);
generator_->PressTouchId(1);
generator_->MoveTouchId(out_mv_pt, 1);
generator_->ReleaseTouchId(1);
ASSERT_TRUE(GetCapturedEvents().empty());
ASSERT_TRUE(GetTouchExplorePoints().empty());
EXPECT_FALSE(IsInNoFingersDownState());
generator_->ReleaseTouchId(0);
AdvanceSimulatedTimePastTapDelay();
EXPECT_TRUE(IsInNoFingersDownState());
ASSERT_TRUE(GetCapturedEvents().empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
}
}
TEST_F(TouchExplorationTest, SingleTapInLiftActivationArea) {
SwitchTouchExplorationMode(true);
gfx::Rect lift_activation = BoundsOfRootWindowInDIP();
lift_activation.Inset(gfx::Insets::TLBR(0, 0, 30, 0));
SetLiftActivationBounds(lift_activation);
// Tap at one location, and get tap and mouse move events.
gfx::Point tap_location = lift_activation.CenterPoint();
// The user has to have previously selected something.
SetTouchAccessibilityAnchorPoint(tap_location);
generator_->set_current_screen_location(tap_location);
generator_->PressTouchId(1);
generator_->ReleaseTouchId(1);
AdvanceSimulatedTimePastTapDelay();
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(2U, captured_events.size());
EXPECT_EQ(ui::EventType::kTouchPressed, captured_events[0]->type());
EXPECT_EQ(ui::EventType::kTouchReleased, captured_events[1]->type());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
ClearCapturedAndGestureEvents();
gfx::Point out_tap_location(tap_location.x(), lift_activation.bottom() + 20);
SetTouchAccessibilityAnchorPoint(out_tap_location);
generator_->set_current_screen_location(out_tap_location);
generator_->PressTouchId(1);
generator_->ReleaseTouchId(1);
AdvanceSimulatedTimePastTapDelay();
const EventList& out_captured_events = GetCapturedEvents();
ASSERT_TRUE(out_captured_events.empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
}
TEST_F(TouchExplorationTest, TouchExploreLiftInLiftActivationArea) {
SwitchTouchExplorationMode(true);
gfx::Rect lift_activation = BoundsOfRootWindowInDIP();
lift_activation.Inset(gfx::Insets::TLBR(0, 0, 30, 0));
SetLiftActivationBounds(lift_activation);
// Explore at one location, and get tap and touch explore events.
gfx::Point tap_location = lift_activation.CenterPoint();
EnterTouchExplorationModeAtLocation(tap_location);
ClearCapturedAndGestureEvents();
ASSERT_EQ(0U, delegate_.NumTouchTypeSounds());
// A touch release should trigger a tap.
ui::TouchEvent touch_explore_release(
ui::EventType::kTouchReleased, tap_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&touch_explore_release);
AdvanceSimulatedTimePastTapDelay();
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(2U, captured_events.size());
EXPECT_EQ(ui::EventType::kTouchPressed, captured_events[0]->type());
EXPECT_EQ(ui::EventType::kTouchReleased, captured_events[1]->type());
ASSERT_EQ(1U, delegate_.NumTouchTypeSounds());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
ClearCapturedAndGestureEvents();
delegate_.ResetCountersToZero();
// Touch explore inside the activation bounds, but lift outside.
gfx::Point out_tap_location(tap_location.x(), lift_activation.bottom() + 20);
SetTouchAccessibilityAnchorPoint(out_tap_location);
EnterTouchExplorationModeAtLocation(tap_location);
ClearCapturedAndGestureEvents();
ui::TouchEvent out_touch_explore_release(
ui::EventType::kTouchReleased, out_tap_location, Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
generator_->Dispatch(&out_touch_explore_release);
AdvanceSimulatedTimePastTapDelay();
const EventList& out_captured_events = GetCapturedEvents();
ASSERT_TRUE(out_captured_events.empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
ASSERT_EQ(0U, delegate_.NumTouchTypeSounds());
}
// Ensure that any touch release events received after
// TouchExplorationController starts up are canceled, if we haven't
// seen any touch press events yet. http://crbug.com/751348
TEST_F(TouchExplorationTest, AlreadyHeldFingersGetCanceled) {
generator_->PressTouch();
SwitchTouchExplorationMode(true);
generator_->ReleaseTouch();
std::vector<ui::LocatedEvent*> events =
GetCapturedLocatedEventsOfType(ui::EventType::kTouchCancelled);
ASSERT_EQ(1U, events.size());
}
// Ensure 3 or 4 finger tap gets recognized correctly.
TEST_F(TouchExplorationTest, ThreeOrFourFingerTap) {
SwitchTouchExplorationMode(true);
ui::TouchEvent press_id_1(
ui::EventType::kTouchPressed, gfx::Point(100, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
ui::TouchEvent release_id_1(
ui::EventType::kTouchReleased, gfx::Point(100, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 1));
ui::TouchEvent press_id_2(
ui::EventType::kTouchPressed, gfx::Point(110, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 2));
ui::TouchEvent release_id_2(
ui::EventType::kTouchReleased, gfx::Point(110, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 2));
ui::TouchEvent press_id_3(
ui::EventType::kTouchPressed, gfx::Point(120, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 3));
ui::TouchEvent release_id_3(
ui::EventType::kTouchReleased, gfx::Point(120, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 3));
ui::TouchEvent press_id_4(
ui::EventType::kTouchPressed, gfx::Point(130, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 4));
ui::TouchEvent release_id_4(
ui::EventType::kTouchReleased, gfx::Point(120, 200), Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, 4));
// Three fingers down.
generator_->Dispatch(&press_id_1);
EXPECT_FALSE(IsInTwoFingerTapState());
generator_->Dispatch(&press_id_2);
EXPECT_TRUE(IsInTwoFingerTapState());
generator_->Dispatch(&press_id_3);
EXPECT_TRUE(IsInGestureInProgressState());
// Three fingers up.
generator_->Dispatch(&release_id_1);
EXPECT_TRUE(IsInGestureInProgressState());
generator_->Dispatch(&release_id_2);
EXPECT_TRUE(IsInGestureInProgressState());
ASSERT_EQ(ax::mojom::Gesture::kNone, delegate_.GetLastGesture());
generator_->Dispatch(&release_id_3);
EXPECT_TRUE(IsInNoFingersDownState());
ASSERT_EQ(ax::mojom::Gesture::kTap3, delegate_.GetLastGesture());
delegate_.ResetLastGesture();
// Four fingers down.
generator_->Dispatch(&press_id_1);
EXPECT_FALSE(IsInTwoFingerTapState());
generator_->Dispatch(&press_id_2);
EXPECT_TRUE(IsInTwoFingerTapState());
generator_->Dispatch(&press_id_3);
EXPECT_TRUE(IsInGestureInProgressState());
generator_->Dispatch(&press_id_4);
EXPECT_TRUE(IsInGestureInProgressState());
// Four fingers up.
generator_->Dispatch(&release_id_1);
EXPECT_TRUE(IsInGestureInProgressState());
generator_->Dispatch(&release_id_2);
EXPECT_TRUE(IsInGestureInProgressState());
generator_->Dispatch(&release_id_3);
EXPECT_TRUE(IsInGestureInProgressState());
ASSERT_EQ(ax::mojom::Gesture::kNone, delegate_.GetLastGesture());
generator_->Dispatch(&release_id_4);
EXPECT_TRUE(IsInNoFingersDownState());
ASSERT_EQ(ax::mojom::Gesture::kTap4, delegate_.GetLastGesture());
}
// Triggers right-click when anchor point remains in position after delay.
TEST_F(TouchExplorationTest, TriggersRightClickAfterDelay) {
SwitchTouchExplorationMode(true);
gfx::Rect lift_activation = BoundsOfRootWindowInDIP();
SetLiftActivationBounds(lift_activation);
// Explore at one location.
gfx::Point tap_location = lift_activation.CenterPoint();
EnterTouchExplorationModeAtLocation(tap_location);
ClearCapturedAndGestureEvents();
ASSERT_EQ(0U, delegate_.NumTouchTypeSounds());
// Stay in the same anchor point after delay.
AdvanceSimulatedTimePastLongPressDelay();
// Any event should be rewritten as a right-mouse click.
generator_->set_current_screen_location(tap_location);
generator_->MoveTouch(tap_location);
const EventList& captured_events = GetCapturedEvents();
ASSERT_EQ(2U, captured_events.size());
EXPECT_EQ(ui::EventType::kMousePressed, captured_events[0]->type());
EXPECT_EQ(ui::EventType::kMouseReleased, captured_events[1]->type());
// We immediately go back to touch exploration so there will be a touch
// explore event from the touch exploration.
ASSERT_EQ(1U, GetTouchExplorePoints().size());
EXPECT_EQ(1U, delegate_.NumLongPressRightClickSounds());
ClearCapturedAndGestureEvents();
delegate_.ResetCountersToZero();
}
// Does not trigger right-click when anchor point is not in lift activation
// bounds.
TEST_F(TouchExplorationTest,
DoesNotTriggerRightClickInNonLiftActivationBounds) {
SwitchTouchExplorationMode(true);
// Explore at one location.
gfx::Point tap_location = BoundsOfRootWindowInDIP().CenterPoint();
EnterTouchExplorationModeAtLocation(tap_location);
ClearCapturedAndGestureEvents();
ASSERT_EQ(0U, delegate_.NumTouchTypeSounds());
// Stay in the same anchor point after delay.
AdvanceSimulatedTimePastLongPressDelay();
generator_->set_current_screen_location(tap_location);
generator_->MoveTouch(tap_location);
const EventList& captured_events = GetCapturedEvents();
ASSERT_TRUE(captured_events.empty());
ASSERT_EQ(1U, GetTouchExplorePoints().size());
EXPECT_EQ(0U, delegate_.NumLongPressRightClickSounds());
ClearCapturedAndGestureEvents();
delegate_.ResetCountersToZero();
}
} // namespace ash