blob: fa4942490797079b17ed988723d3b48267238d5d [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/ui/a11y/lib/magnifier/magnifier.h"
#include <fuchsia/ui/input/accessibility/cpp/fidl.h>
#include <lib/zx/time.h>
#include <limits>
#include <optional>
#include <gtest/gtest.h>
#include "src/lib/testing/loop_fixture/test_loop_fixture.h"
#include "src/ui/a11y/lib/gesture_manager/arena/gesture_arena.h"
#include "src/ui/a11y/lib/gesture_manager/arena/tests/mocks/mock_contest_member.h"
#include "src/ui/a11y/lib/magnifier/tests/mocks/mock_magnification_handler.h"
#include "src/ui/a11y/lib/testing/formatting.h"
#include "src/ui/a11y/lib/testing/input.h"
#include "src/ui/lib/glm_workaround/glm_workaround.h"
#include <glm/gtc/epsilon.hpp>
// These tests cover magnifier behavior, mostly around magnifier gestures. Care needs to be taken
// wrt. the constants in magnifier.h. In particular, mind the default, min, and max zoom, and the
// drag detection threshold.
namespace accessibility_test {
namespace {
using fuchsia::ui::input::accessibility::EventHandling;
// Transition period plus one frame to account for rounding error.
constexpr zx::duration kTestTransitionPeriod = a11y::Magnifier::kTransitionPeriod + kFramePeriod;
constexpr zx::duration kFrameEpsilon = zx::msec(1);
static_assert(kFramePeriod > kFrameEpsilon);
class MagnifierTest : public gtest::TestLoopFixture {
public:
MagnifierTest() { arena_.Add(&magnifier_); }
a11y::Magnifier* magnifier() { return &magnifier_; }
void SendPointerEvents(const std::vector<PointerParams>& events) {
for (const auto& params : events) {
SendPointerEvent(params);
}
}
private:
void SendPointerEvent(const PointerParams& params) {
arena_.OnEvent(ToPointerEvent(params, input_event_time_++));
// Run the loop to simulate a trivial passage of time. (This is realistic for everything but ADD
// + DOWN and UP + REMOVE.)
//
// This covers a bug discovered during manual testing where the temporary zoom threshold timeout
// was posted without a delay and triggered any time the third tap took nonzero time.
RunLoopUntilIdle();
}
a11y::GestureArena arena_;
a11y::Magnifier magnifier_;
// We don't actually use these times. If we did, we'd want to more closely correlate them with
// fake time.
uint64_t input_event_time_ = 0;
};
// Ensure that a trigger + (temporary) pan gesture without a registered handler doesn't crash
// anything.
TEST_F(MagnifierTest, WithoutHandler) {
SendPointerEvents(2 * TapEvents(1, {0, 0}) + DragEvents(1, {0, 0}, {.5f, 0}));
RunLoopFor(kTestTransitionPeriod);
}
// Ensure that a trigger + (temporary) pan gesture with a closed handler doesn't crash
// anything.
TEST_F(MagnifierTest, WithClosedHandler) {
{
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
RunLoopFor(kFramePeriod);
}
SendPointerEvents(2 * TapEvents(1, {0, 0}) + DragEvents(1, {0, 0}, {.5f, 0}));
RunLoopFor(kTestTransitionPeriod);
}
// Ensures that unactivated interaction does not touch a handler.
TEST_F(MagnifierTest, NoTrigger) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(DownEvents(1, {0, 0}) + MoveEvents(1, {0, 0}, {.25f, 0}, 5));
RunLoopFor(kTestTransitionPeriod);
// mid-interaction check
EXPECT_EQ(handler.transform(), ClipSpaceTransform::identity());
SendPointerEvents(MoveEvents(1, {.25f, 0}, {.5f, 0}, 5) + UpEvents(1, {.5f, 0}));
RunLoopFor(kTestTransitionPeriod);
// post-interaction check
EXPECT_EQ(handler.transform(), ClipSpaceTransform::identity());
}
// Ensure that a 3x1 tap triggers magnification.
TEST_F(MagnifierTest, Trigger3x1) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform().scale, a11y::Magnifier::kDefaultScale);
}
// Ensure that a 2x3 tap triggers magnification.
TEST_F(MagnifierTest, Trigger2x3) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(2 * Zip({TapEvents(1, {}), TapEvents(2, {}), TapEvents(3, {})}));
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform().scale, a11y::Magnifier::kDefaultScale);
}
// Ensure that a 4x1 stays magnified.
TEST_F(MagnifierTest, Trigger4x1) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(4 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform().scale, a11y::Magnifier::kDefaultScale);
}
// Ensures that when a new handler is registered, it receives the up-to-date transform.
TEST_F(MagnifierTest, LateHandler) {
SendPointerEvents(3 * TapEvents(1, {}));
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
// If there was no handler, we shouldn't have waited for the animation.
RunLoopUntilIdle();
EXPECT_EQ(handler.transform(), (ClipSpaceTransform{.scale = a11y::Magnifier::kDefaultScale}));
}
// This covers a bug discovered during code review where if, in between handlers, the transform is
// changed while magnified (e.g. a pan gesture is issued), the new handler would end up unmagnified.
TEST_F(MagnifierTest, InteractionBeforeLateHandler) {
{
MockMagnificationHandler h1;
magnifier()->RegisterHandler(h1.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
// Due to other bugs, this edge case only manifested if the magnification finishes
// transitioning.
}
static_assert(.2f > a11y::Magnifier::kDragThreshold,
"Need to increase jitter to exceed drag threshold.");
// Starts with a two-finger tap, with one finger moving a little and back to where it started.
const auto jitterDrag = Zip({DownEvents(1, {}), TapEvents(2, {})}) +
MoveEvents(1, {}, {.2f, .2f}) + MoveEvents(1, {.2f, .2f}, {}) +
UpEvents(1, {});
// First interaction surfaces channel closure.
SendPointerEvents(jitterDrag);
RunLoopUntilIdle();
// Next interaction manifests bug (zeroes out transition progress).
SendPointerEvents(jitterDrag);
RunLoopUntilIdle();
MockMagnificationHandler h2;
magnifier()->RegisterHandler(h2.NewBinding());
RunLoopUntilIdle();
EXPECT_EQ(h2.transform(), (ClipSpaceTransform{.scale = a11y::Magnifier::kDefaultScale}));
}
// Ensures that switching a handler causes transition updates to be delivered only to the new
// handler, still throttled at the framerate but relative to when the switch took place.
TEST_F(MagnifierTest, SwitchHandlerDuringTransition) {
MockMagnificationHandler h1, h2;
magnifier()->RegisterHandler(h1.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kFramePeriod * 3 / 2);
magnifier()->RegisterHandler(h2.NewBinding());
RunLoopUntilIdle();
EXPECT_EQ(h1.update_count(), 2u);
EXPECT_EQ(h2.update_count(), 1u);
RunLoopFor(kFramePeriod - kFrameEpsilon);
EXPECT_EQ(h2.update_count(), 1u);
RunLoopFor(kFrameEpsilon);
EXPECT_EQ(h1.update_count(), 2u);
EXPECT_EQ(h2.update_count(), 2u);
}
// Ensure that a 3x1 trigger focuses on the tap coordinate.
TEST_F(MagnifierTest, TriggerFocus) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
static constexpr glm::vec2 tap_coordinate{.5f, -.25f};
SendPointerEvents(3 * TapEvents(1, tap_coordinate));
RunLoopFor(kTestTransitionPeriod);
// After the final transformation, the coordinate that was tapped should still be where it was
// before.
EXPECT_EQ(handler.transform().Apply(tap_coordinate), tap_coordinate) << handler.transform();
}
// Ensure that a 3x1 trigger animates smoothly at the framerate.
TEST_F(MagnifierTest, TriggerTransition) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
// Drain the initial SetClipSpaceTransform and wait until the next frame can be presented so that
// we can begin testing the animation right away.
RunLoopFor(kFramePeriod);
auto last_update_count = handler.update_count();
float last_scale = handler.transform().scale;
static constexpr glm::vec2 tap_coordinate{1, -1};
SendPointerEvents(3 * TapEvents(1, tap_coordinate));
// Since there shouldn't be a pending Present at this time, simply advancing the loop should
// propagate the first frame of our transition. Subsequent updates will occur after every frame
// period.
RunLoopUntilIdle();
for (zx::duration elapsed; elapsed < a11y::Magnifier::kTransitionPeriod;
elapsed += kFramePeriod) {
EXPECT_EQ(handler.update_count(), last_update_count + 1)
<< "Expect animation to be throttled at framerate.";
EXPECT_GT(handler.transform().scale, last_scale) << elapsed;
// The animation should still be focused on the tap coordinate.
static constexpr float epsilon =
std::numeric_limits<float>::epsilon() * a11y::Magnifier::kDefaultScale;
EXPECT_TRUE(glm::all(
glm::epsilonEqual(handler.transform().Apply(tap_coordinate), tap_coordinate, epsilon)))
<< handler.transform();
last_scale = handler.transform().scale;
last_update_count = handler.update_count();
RunLoopFor(kFramePeriod);
}
// After the transition period, we expect the animation to stop.
last_update_count = handler.update_count();
RunLoopFor(kFramePeriod * 5);
EXPECT_EQ(handler.update_count(), last_update_count);
}
// Ensure that panning during a transition integrates smoothly.
TEST_F(MagnifierTest, TransitionWithPan) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
SendPointerEvents(DownEvents(1, {}) + TapEvents(2, {}));
// Let one frame animate so that the scale is allowed to transition past 1, which allows pan.
// Otherwise we would expect the first translation assertion below to fail since even if a pan
// gesture is being processed, the scale still being locked at 1 would allow no freedom to pan.
RunLoopFor(kFramePeriod);
ClipSpaceTransform last_transform = handler.transform();
static_assert(a11y::Magnifier::kDragThreshold < 1.f / kDefaultMoves,
"Need to increase drag step size to catch all moves.");
for (const PointerParams& move_event : MoveEvents(1, {}, {-1, 1})) {
SendPointerEvents({move_event});
RunLoopFor(kFramePeriod);
EXPECT_LT(handler.transform().x, last_transform.x);
EXPECT_GT(handler.transform().y, last_transform.y);
EXPECT_GT(handler.transform().scale, last_transform.scale);
last_transform = handler.transform();
}
}
// Ensure that a temporary pan during a transtion integrates smoothly and continues to focus the
// pointer.
TEST_F(MagnifierTest, TransitionWithTemporaryPan) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(2 * TapEvents(1, {}) + DownEvents(1, {}));
// Let one frame animate so that the scale is allowed to transition past 1, which allows pan.
// Otherwise since the tracking is relative and the first pan would still be locked, this would
// throw off our focus and make the assertions below a lot more complicated.
static_assert((a11y::Magnifier::kDefaultScale - 1) * a11y::Magnifier::kTransitionRate >=
1.f / kDefaultMoves,
"Need to run transition further to allow drag freedom, or reduce drag step size.");
RunLoopFor(kFramePeriod);
float last_scale = handler.transform().scale;
static_assert(a11y::Magnifier::kDragThreshold < 1.f / kDefaultMoves,
"Need to increase drag step size to catch all moves.");
for (const PointerParams& move_event : MoveEvents(1, {}, {-1, 1})) {
SendPointerEvents({move_event});
RunLoopFor(kFramePeriod);
EXPECT_GT(handler.transform().scale, last_scale);
last_scale = handler.transform().scale;
// The animation should still be focused on the tap coordinate.
const glm::vec2 mapped_coordinate = handler.transform().Apply(move_event.coordinate);
static constexpr float epsilon =
std::numeric_limits<float>::epsilon() * a11y::Magnifier::kDefaultScale;
EXPECT_TRUE(glm::all(glm::epsilonEqual(mapped_coordinate, move_event.coordinate, epsilon)))
<< handler.transform() << ": " << mapped_coordinate << " vs. " << move_event.coordinate;
}
}
// Ensure that panning magnification clamps to display edges, i.e. that the display area remains
// covered by content.
TEST_F(MagnifierTest, ClampPan) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
// Focus on the upper right.
SendPointerEvents(3 * TapEvents(1, {1, -1}));
RunLoopFor(kTestTransitionPeriod);
const auto transform = handler.transform();
// Now attempt to pan with a swipe towards the lower left.
SendPointerEvents(Zip({TapEvents(1, {1, -1}), DragEvents(2, {1, -1}, {-1, 1})}));
RunLoopFor(kFramePeriod);
EXPECT_EQ(handler.transform(), transform) << "Clamped pan should not have moved.";
}
TEST_F(MagnifierTest, Pan) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
// Focus on the upper right.
SendPointerEvents(3 * TapEvents(1, {1, -1}));
RunLoopFor(kTestTransitionPeriod);
ClipSpaceTransform transform = handler.transform();
// Now attempt to pan with a swipe towards the upper right.
SendPointerEvents(Zip({TapEvents(1, {-1, 1}), DragEvents(2, {-1, 1}, {1, -1})}));
RunLoopFor(kFramePeriod);
transform.x += 2;
transform.y -= 2;
EXPECT_EQ(handler.transform().scale, transform.scale);
static constexpr float epsilon = std::numeric_limits<float>::epsilon() * kDefaultMoves;
EXPECT_TRUE(glm::all(
glm::epsilonEqual(handler.transform().translation(), transform.translation(), epsilon)))
<< "Expected to pan towards the lower left by -(2, -2) to " << transform.translation()
<< " (actual: " << handler.transform().translation() << ").";
}
TEST_F(MagnifierTest, PanTemporary) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
// Segue from an activation 3x1 in the upper right to a drag to the lower left.
SendPointerEvents(2 * TapEvents(1, {1, -1}) + DownEvents(1, {1, -1}));
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform().scale, a11y::Magnifier::kDefaultScale);
EXPECT_EQ(handler.transform().Apply({1, -1}), glm::vec2(1, -1));
// Unlike the non-temporary pan, temporary pan should continue to focus the pointer.
SendPointerEvents(MoveEvents(1, {1, -1}, {-1, 1}));
RunLoopFor(kFramePeriod);
EXPECT_EQ(handler.transform().scale, a11y::Magnifier::kDefaultScale);
EXPECT_EQ(handler.transform().Apply({-1, 1}), glm::vec2(-1, 1));
}
TEST_F(MagnifierTest, PinchZoom) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
static_assert(2 * a11y::Magnifier::kDefaultScale < a11y::Magnifier::kMaxScale,
"Need to adjust test zoom level to be less than max scale.");
SendPointerEvents(Zip({DragEvents(1, {-.1f, 0}, {-.2f, 0}), DragEvents(2, {.1f, 0}, {.2f, 0})}));
RunLoopFor(kFramePeriod);
static constexpr float epsilon =
std::numeric_limits<float>::epsilon() * 2 * a11y::Magnifier::kDefaultScale;
EXPECT_NEAR(handler.transform().scale, 2 * a11y::Magnifier::kDefaultScale, epsilon);
}
// Ensures that after pinching zoom and toggling magnification, the magnification level is restored
// to the adjusted level.
TEST_F(MagnifierTest, RememberZoom) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
SendPointerEvents(Zip({DragEvents(1, {-.1f, 0}, {-.2f, 0}), DragEvents(2, {.1f, 0}, {.2f, 0})}));
RunLoopFor(kFramePeriod);
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
static constexpr float epsilon =
std::numeric_limits<float>::epsilon() * 2 * a11y::Magnifier::kDefaultScale;
EXPECT_NEAR(handler.transform().scale, 2 * a11y::Magnifier::kDefaultScale, epsilon);
}
TEST_F(MagnifierTest, MinZoom) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
static_assert(.1f * a11y::Magnifier::kDefaultScale < a11y::Magnifier::kMinScale,
"Need to adjust test gesture to reach min scale.");
SendPointerEvents(Zip({DragEvents(1, {-1, 0}, {-.1f, 0}), DragEvents(2, {1, 0}, {.1f, 0})}));
RunLoopFor(kFramePeriod);
EXPECT_EQ(handler.transform().scale, a11y::Magnifier::kMinScale);
}
TEST_F(MagnifierTest, MaxZoom) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
static_assert(a11y::Magnifier::kDefaultScale > .1f * a11y::Magnifier::kMaxScale,
"Need to adjust test gesture to reach max scale.");
SendPointerEvents(Zip({DragEvents(1, {-.1f, 0}, {-1, 0}), DragEvents(2, {.1f, 0}, {1, 0})}));
RunLoopFor(kFramePeriod);
EXPECT_EQ(handler.transform().scale, a11y::Magnifier::kMaxScale);
}
// Ensures that zooming at the edge of the screen does not violate clamping; pan should adjust to
// compensate.
TEST_F(MagnifierTest, ClampZoom) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {1, 0}));
RunLoopFor(kTestTransitionPeriod);
static_assert(a11y::Magnifier::kDefaultScale > 1.5f * a11y::Magnifier::kMinScale,
"Need to adjust test zoom level to be greater than min scale.");
SendPointerEvents(Zip({DragEvents(1, {0, -.3f}, {0, -.2f}), DragEvents(2, {0, .3f}, {0, .2f})}));
RunLoopFor(kFramePeriod);
static constexpr float epsilon =
std::numeric_limits<float>::epsilon() * a11y::Magnifier::kDefaultScale / 1.5f;
EXPECT_NEAR(handler.transform().scale, a11y::Magnifier::kDefaultScale / 1.5f, epsilon);
// Check the anchor point to verify clamping. x should be clamped at 1. y can deviate pretty
// wildly since it's governed by the zoom centroid, which is subject to incremental approximation.
// While it's possible to calculate the tolerance exactly, it's not worth it.
const glm::vec2 pt = handler.transform().Apply({1, 0});
EXPECT_EQ(pt.x, 1);
EXPECT_NEAR(pt.y, 0, .01f);
}
// Ensures that transitioning out of a non-default magnification animates smoothly.
TEST_F(MagnifierTest, TransitionOut) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
// zoom it
SendPointerEvents(Zip({DragEvents(1, {-.1f, 0}, {-.2f, 0}), DragEvents(2, {.1f, 0}, {.2f, 0})}));
// pan it
SendPointerEvents(Zip({TapEvents(1, {1, -1}), DragEvents(2, {1, -1}, {-1, 1})}));
// Zoom will issue Present immediately, so we need to wait an extra frame for the pan to be issued
// and then for the next Present to be available.
RunLoopFor(kFramePeriod * 2);
ClipSpaceTransform last_transform = handler.transform();
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopUntilIdle();
// We expect this to restore from the pan above, which means panning +x and -y.
for (zx::duration elapsed; elapsed < a11y::Magnifier::kTransitionPeriod;
elapsed += kFramePeriod) {
EXPECT_GT(handler.transform().x, last_transform.x) << elapsed;
EXPECT_LT(handler.transform().y, last_transform.y) << elapsed;
EXPECT_LT(handler.transform().scale, last_transform.scale) << elapsed;
last_transform = handler.transform();
RunLoopFor(kFramePeriod);
}
// After the transition period, we expect the animation to stop.
EXPECT_EQ(handler.transform(), ClipSpaceTransform::identity());
const auto update_count = handler.update_count();
RunLoopFor(kFramePeriod * 5);
EXPECT_EQ(handler.update_count(), update_count);
}
// Also include coverage for 2x3 zoom-out.
TEST_F(MagnifierTest, ZoomOut2x3) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
SendPointerEvents(2 * Zip({TapEvents(1, {}), TapEvents(2, {}), TapEvents(3, {})}));
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform(), ClipSpaceTransform::identity());
}
// Magnification should cease after a temporary magnification gesture is released.
TEST_F(MagnifierTest, TemporaryRelease) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(2 * TapEvents(1, {}) + DownEvents(1, {}));
RunLoopFor(a11y::Magnifier::kTemporaryZoomHold);
float last_scale = handler.transform().scale;
SendPointerEvents(UpEvents(1, {}));
RunLoopUntilIdle();
// Go ahead and double check that we're animating the transition back out.
for (zx::duration elapsed; elapsed < a11y::Magnifier::kTransitionPeriod;
elapsed += kFramePeriod) {
EXPECT_LT(handler.transform().scale, last_scale) << elapsed;
last_scale = handler.transform().scale;
RunLoopFor(kFramePeriod);
}
EXPECT_EQ(handler.transform(), ClipSpaceTransform::identity());
}
// Segueing a trigger gesture into a pan should behave as a temporary magnification.
TEST_F(MagnifierTest, TemporaryPanRelease) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
SendPointerEvents(2 * TapEvents(1, {}) + DownEvents(1, {}) + MoveEvents(1, {}, {.5f, .5f}));
RunLoopFor(kTestTransitionPeriod);
SendPointerEvents(UpEvents(1, {.5f, .5f}));
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform(), ClipSpaceTransform::identity());
}
// Ensure that rapid input does not trigger updates faster than the framerate.
TEST_F(MagnifierTest, InputFrameThrottling) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
// Go ahead and send the initial SetClipSpaceTransform so that we can ensure that the initial
// input handling below doesn't somehow schedule another Present immediately.
RunLoopUntilIdle();
SendPointerEvents(2 * TapEvents(1, {}) + DownEvents(1, {}) + MoveEvents(1, {}, {-1, -1}));
RunLoopUntilIdle();
EXPECT_EQ(handler.update_count(), 1u);
RunLoopFor(kFramePeriod);
EXPECT_EQ(handler.update_count(), 2u);
RunLoopFor(kFramePeriod);
EXPECT_EQ(handler.update_count(), 3u);
}
class MagnifierRecognizerTest : public gtest::TestLoopFixture {
public:
MockContestMember* member() { return &member_; }
a11y::Magnifier* magnifier() { return &magnifier_; }
void SendPointerEvents(const std::vector<PointerParams>& events) {
for (const auto& params : events) {
SendPointerEvent(params);
}
}
private:
void SendPointerEvent(const PointerParams& params) {
if (member_.is_held()) {
magnifier_.HandleEvent(ToPointerEvent(params, input_event_time_));
}
// Run the loop to simulate a trivial passage of time. (This is realistic for everything but ADD
// + DOWN and UP + REMOVE.)
//
// This covers a bug discovered during manual testing where the temporary zoom threshold timeout
// was posted without a delay and triggered any time the third tap took nonzero time.
RunLoopUntilIdle();
++input_event_time_;
}
MockContestMember member_;
a11y::Magnifier magnifier_;
// We don't actually use these times. If we did, we'd want to more closely correlate them with
// fake time.
uint64_t input_event_time_ = 0;
};
constexpr zx::duration kTriggerEpsilon = zx::msec(1);
static_assert(a11y::Magnifier::kTriggerMaxDelay > kTriggerEpsilon);
TEST_F(MagnifierRecognizerTest, Reject1x4Immediately) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}) + DownEvents(2, {}) + DownEvents(3, {}) + DownEvents(4, {}));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
// 3x1 should be accepted as soon as the last tap begins and released at the end.
TEST_F(MagnifierRecognizerTest, Accept3x1) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(2 * TapEvents(1, {}) + DownEvents(1, {}));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kAccepted);
ASSERT_TRUE(member()->is_held());
SendPointerEvents(UpEvents(1, {}));
EXPECT_FALSE(member()->is_held());
}
// 2x3 should be accepted as soon as the last pointer of the last tap comes down and released at the
// end.
TEST_F(MagnifierRecognizerTest, Accept2x3) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(Zip({TapEvents(1, {}), TapEvents(2, {}), TapEvents(3, {})}) +
DownEvents(1, {}) + DownEvents(2, {}));
SendPointerEvents(DownEvents(3, {}));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kAccepted);
SendPointerEvents(UpEvents(3, {}) + UpEvents(2, {}));
ASSERT_TRUE(member()->is_held());
SendPointerEvents(UpEvents(1, {}));
EXPECT_FALSE(member()->is_held());
}
TEST_F(MagnifierRecognizerTest, Reject2x1AfterTimeout) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(2 * TapEvents(1, {}));
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay - kTriggerEpsilon);
EXPECT_TRUE(member()->is_held()) << "Boundary condition: held before timeout.";
RunLoopFor(kTriggerEpsilon);
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
// Ensures that a 3x1 with a long wait between taps (but shorter than the timeout) is accepted.
TEST_F(MagnifierRecognizerTest, Accept3x1UnderTimeout) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(TapEvents(1, {}));
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay - kTriggerEpsilon);
SendPointerEvents(TapEvents(1, {}));
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay - kTriggerEpsilon);
SendPointerEvents(TapEvents(1, {}));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kAccepted);
}
// Ensures that a long press after a 3x1 trigger is rejected after the tap timeout.
TEST_F(MagnifierRecognizerTest, Reject4x1LongPressAfterTimeout) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
// At this point as verified by |Accept3x1|, we have accepted and released.
magnifier()->OnWin();
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kUndecided);
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay);
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
// Ensures that a fourth tap after a 3x1 trigger is rejected after the tap timeout.
TEST_F(MagnifierRecognizerTest, Reject4x1AfterTimeout) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
// At this point as verified by |Accept3x1|, we have accepted and released.
magnifier()->OnWin();
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(TapEvents(1, {}));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kUndecided);
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay);
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
// Covers an edge regression where the second 3-tap in a zoom-out might be allowed to take forever.
TEST_F(MagnifierRecognizerTest, Reject2x3ZoomOutAfterTimeout) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
// At this point as verified by |Accept3x1|, we have accepted and released.
magnifier()->OnWin();
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(Zip({TapEvents(1, {}), TapEvents(2, {}), TapEvents(3, {})}));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kUndecided);
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay);
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
TEST_F(MagnifierRecognizerTest, RejectUnmagnified1Drag) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}) + MoveEvents(1, {}, {.25f, 0}, 1));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
TEST_F(MagnifierRecognizerTest, RejectMagnified1Drag) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}) + MoveEvents(1, {}, {.25f, 0}, 1));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
TEST_F(MagnifierRecognizerTest, RejectUnmagnified2Drag) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}) + TapEvents(2, {}) + MoveEvents(1, {}, {.25f, 0}, 1));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
TEST_F(MagnifierRecognizerTest, AcceptMagnified2Drag) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
magnifier()->OnWin();
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}) + TapEvents(2, {}) + MoveEvents(1, {}, {.25f, 0}, 1));
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kAccepted);
ASSERT_TRUE(member()->is_held());
SendPointerEvents(UpEvents(1, {}));
EXPECT_FALSE(member()->is_held());
}
TEST_F(MagnifierRecognizerTest, RejectUnmagnified1LongPress) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}));
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay);
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
TEST_F(MagnifierRecognizerTest, RejectMagnified1LongPress) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
magnifier()->OnWin();
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}));
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay);
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
TEST_F(MagnifierRecognizerTest, RejectUnmagnified2LongPress) {
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(DownEvents(1, {}) + DownEvents(2, {}));
RunLoopFor(a11y::Magnifier::kTriggerMaxDelay);
EXPECT_EQ(member()->status(), a11y::ContestMember::Status::kRejected);
}
// Ensures that transitions don't happen until we've won.
TEST_F(MagnifierRecognizerTest, TriggerWaitForWin) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform(), ClipSpaceTransform::identity());
magnifier()->OnWin();
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform().scale, a11y::Magnifier::kDefaultScale);
}
// Ensures that if another recognizer wins after we accept, magnifier does not enable.
TEST_F(MagnifierRecognizerTest, AbortOnLoss) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
magnifier()->OnDefeat();
RunLoopFor(kTestTransitionPeriod);
EXPECT_EQ(handler.transform(), ClipSpaceTransform::identity());
}
// Ensures that drags don't start until we've won.
TEST_F(MagnifierRecognizerTest, PanWaitForWin) {
MockMagnificationHandler handler;
magnifier()->RegisterHandler(handler.NewBinding());
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(3 * TapEvents(1, {}));
magnifier()->OnWin();
RunLoopFor(kTestTransitionPeriod);
ClipSpaceTransform transform = handler.transform();
magnifier()->OnContestStarted(member()->TakeInterface());
SendPointerEvents(Zip({DownEvents(1, {}), TapEvents(2, {})}) + MoveEvents(1, {}, {.5f, .5f}));
RunLoopFor(kFramePeriod);
EXPECT_EQ(handler.transform(), transform);
magnifier()->OnWin();
// There are at least two or three reasonable interpretations here:
// * buffer the pan until we win and then snap to the most up-to-date position
// * delay accumulation until we win
// * buffer the pan until we win and transition smoothly to the most up-to-date position
// For simplicity and consistency with trigger gestures, we pick the first for now. In practice
// the win should be awarded almost immediately for the magnifier if it is competing against
// screen reader.
RunLoopFor(kFramePeriod);
transform.x = .5f;
transform.y = .5f;
EXPECT_EQ(handler.transform(), transform);
SendPointerEvents(MoveEvents(1, {.5f, .5f}, {1, 1}));
RunLoopFor(kFramePeriod);
transform.x = 1;
transform.y = 1;
EXPECT_EQ(handler.transform(), transform);
}
} // namespace
} // namespace accessibility_test