flutter/packages/flutter/test/gestures/long_press_test.dart
Ian Hickson bc49cd1bca
Allow long-press gestures to continue even if buttons change. (#127877)
Previously, if you changed buttons during a long-press gesture, if it was before the gesture was accepted we would discard it, and if it was after the gesture was accepted we would silently end it without firing any of the relevant events.

This silent cancelation behavior is terrible because it means there's no way for consumers to know what state they're in, so you end up with widgets that thing they're still being long-pressed even though nothing is happening.

We could change the behavior in three ways, as far as I can tell:

- we could send a cancel event when you change buttons. This would introduce a new kind of transition (start->cancel) which I don't think we currently require people to support. This would therefore not fix existing code and would make future code more complicated to handle a really obscure user action that it seems unlikely anyone cares about.

- we could send an end event when you change buttons. This would mean the action commits, even though the user is still holding the mouse button down. This seems slightly better than the previous option but still not ideal as it means nudging the mouse button commits you even though you're still holding the button down.

- we could ignore button changes after the long-press has been accepted.

I implemented the last one in this PR.
2023-07-06 00:42:57 +00:00

796 lines
27 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'gesture_tester.dart';
// Down/move/up pair 1: normal tap sequence
const PointerDownEvent down = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
);
const PointerUpEvent up = PointerUpEvent(
pointer: 5,
position: Offset(11, 9),
);
const PointerMoveEvent move = PointerMoveEvent(
pointer: 5,
position: Offset(100, 200),
);
// Down/up pair 2: normal tap sequence far away from pair 1
const PointerDownEvent down2 = PointerDownEvent(
pointer: 6,
position: Offset(10, 10),
);
const PointerUpEvent up2 = PointerUpEvent(
pointer: 6,
position: Offset(11, 9),
);
// Down/up pair 3: tap sequence with secondary button
const PointerDownEvent down3 = PointerDownEvent(
pointer: 7,
position: Offset(30, 30),
buttons: kSecondaryButton,
);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Long press', () {
late LongPressGestureRecognizer gesture;
late List<String> recognized;
void setUpHandlers() {
gesture
..onLongPressDown = (LongPressDownDetails details) {
recognized.add('down');
}
..onLongPressCancel = () {
recognized.add('cancel');
}
..onLongPress = () {
recognized.add('start');
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('move');
}
..onLongPressUp = () {
recognized.add('end');
};
}
setUp(() {
recognized = <String>[];
gesture = LongPressGestureRecognizer();
setUpHandlers();
});
testGesture('Should recognize long press', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(recognized, const <String>['down', 'start']);
gesture.dispose();
expect(recognized, const <String>['down', 'start']);
});
testGesture('Should recognize long press with altered duration', (GestureTester tester) {
gesture = LongPressGestureRecognizer(duration: const Duration(milliseconds: 100));
setUpHandlers();
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 50));
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 50));
expect(recognized, const <String>['down', 'start']);
gesture.dispose();
expect(recognized, const <String>['down', 'start']);
});
testGesture('Up cancels long press', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
tester.route(up);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(seconds: 1));
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Moving before accept cancels', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
tester.route(move);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(seconds: 1));
tester.route(up);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down', 'cancel']);
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Moving after accept is ok', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(seconds: 1));
expect(recognized, const <String>['down', 'start']);
tester.route(move);
expect(recognized, const <String>['down', 'start', 'move']);
tester.route(up);
expect(recognized, const <String>['down', 'start', 'move', 'end']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down', 'start', 'move', 'end']);
gesture.dispose();
expect(recognized, const <String>['down', 'start', 'move', 'end']);
});
testGesture('Should recognize both tap down and long press', (GestureTester tester) {
final TapGestureRecognizer tap = TapGestureRecognizer();
tap.onTapDown = (_) {
recognized.add('tap_down');
};
tap.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down', 'tap_down']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(recognized, const <String>['down', 'tap_down', 'start']);
tap.dispose();
gesture.dispose();
expect(recognized, const <String>['down', 'tap_down', 'start']);
});
testGesture('Drag start delayed by microtask', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
bool isDangerousStack = false;
drag.onStart = (DragStartDetails details) {
expect(isDangerousStack, isFalse);
recognized.add('drag_start');
};
drag.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
isDangerousStack = true;
gesture.dispose();
isDangerousStack = false;
expect(recognized, const <String>['down', 'cancel']);
tester.async.flushMicrotasks();
expect(recognized, const <String>['down', 'cancel', 'drag_start']);
drag.dispose();
});
testGesture('Should recognize long press up', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down); // kLongPressTimeout = 500;
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(recognized, const <String>['down', 'start']);
tester.route(up);
expect(recognized, const <String>['down', 'start', 'end']);
gesture.dispose();
expect(recognized, const <String>['down', 'start', 'end']);
});
testGesture('Should not recognize long press with more than one buttons', (GestureTester tester) {
gesture.addPointer(const PointerDownEvent(
pointer: 5,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton | kTertiaryButton,
position: Offset(10, 10),
));
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>[]);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, const <String>[]);
tester.route(up);
expect(recognized, const <String>[]);
gesture.dispose();
expect(recognized, const <String>[]);
});
testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
tester.route(const PointerMoveEvent(
pointer: 5,
kind: PointerDeviceKind.mouse,
buttons: kTertiaryButton,
position: Offset(10, 10),
));
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(recognized, const <String>['down', 'cancel']);
tester.route(up);
expect(recognized, const <String>['down', 'cancel']);
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('non-allowed pointer does not inadvertently reset the recognizer', (GestureTester tester) {
gesture = LongPressGestureRecognizer(
supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.touch },
);
setUpHandlers();
// Accept a long-press gesture
gesture.addPointer(down);
tester.closeArena(5);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 500));
expect(recognized, const <String>['down', 'start']);
// Add a non-allowed pointer (doesn't match the kind filter)
gesture.addPointer(const PointerDownEvent(
pointer: 101,
kind: PointerDeviceKind.mouse,
position: Offset(10, 10),
));
expect(recognized, const <String>['down', 'start']);
// Moving the primary pointer should result in a normal event
tester.route(const PointerMoveEvent(
pointer: 5,
position: Offset(15, 15),
));
expect(recognized, const <String>['down', 'start', 'move']);
gesture.dispose();
});
});
group('long press drag', () {
late LongPressGestureRecognizer gesture;
Offset? longPressDragUpdate;
late List<String> recognized;
void setUpHandlers() {
gesture
..onLongPressDown = (LongPressDownDetails details) {
recognized.add('down');
}
..onLongPressCancel = () {
recognized.add('cancel');
}
..onLongPress = () {
recognized.add('start');
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('move');
longPressDragUpdate = details.globalPosition;
}
..onLongPressUp = () {
recognized.add('end');
};
}
setUp(() {
gesture = LongPressGestureRecognizer();
setUpHandlers();
recognized = <String>[];
});
testGesture('Should recognize long press down', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(recognized, const <String>['down', 'start']);
gesture.dispose();
expect(recognized, const <String>['down', 'start']);
});
testGesture('Short up cancels long press', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
tester.route(up);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(seconds: 1));
expect(recognized, const <String>['down', 'cancel']);
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Moving before accept cancels', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down']);
tester.route(move);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(seconds: 1));
tester.route(up);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down', 'cancel']);
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Moving after accept does not cancel', (GestureTester tester) {
gesture.addPointer(down);
tester.closeArena(5);
expect(recognized, const <String>[]);
tester.route(down);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(seconds: 1));
expect(recognized, const <String>['down', 'start']);
tester.route(move);
expect(recognized, const <String>['down', 'start', 'move']);
expect(longPressDragUpdate, const Offset(100, 200));
tester.route(up);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, const <String>['down', 'start', 'move', 'end']);
gesture.dispose();
expect(recognized, const <String>['down', 'start', 'move', 'end']);
});
});
group('Enforce consistent-button restriction:', () {
// In sequence between `down` and `up` but with buttons changed
const PointerMoveEvent moveR = PointerMoveEvent(
pointer: 5,
buttons: kSecondaryButton,
position: Offset(10, 10),
);
late LongPressGestureRecognizer gesture;
final List<String> recognized = <String>[];
setUp(() {
gesture = LongPressGestureRecognizer()
..onLongPressDown = (LongPressDownDetails details) {
recognized.add('down');
}
..onLongPressCancel = () {
recognized.add('cancel');
}
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('start');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('end');
};
});
tearDown(() {
gesture.dispose();
recognized.clear();
});
testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) {
// First press
gesture.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 300));
tester.route(moveR);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(up);
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Buttons change before acceptance should not prevent the next long press', (GestureTester tester) {
// First press
gesture.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
expect(recognized, <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
tester.route(moveR);
expect(recognized, <String>['down', 'cancel']);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(up);
recognized.clear();
// Second press
gesture.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
expect(recognized, <String>['down']);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['down', 'start']);
recognized.clear();
tester.route(up2);
expect(recognized, <String>['end']);
});
testGesture('Should not cancel long press when buttons change after acceptance', (GestureTester tester) {
// First press
gesture.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['down', 'start']);
recognized.clear();
tester.route(moveR);
expect(recognized, <String>[]);
tester.route(up);
expect(recognized, <String>['end']);
});
testGesture('Buttons change after acceptance should not prevent the next long press', (GestureTester tester) {
// First press
gesture.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 1000));
tester.route(moveR);
tester.route(up);
recognized.clear();
// Second press
gesture.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['down', 'start']);
recognized.clear();
tester.route(up2);
expect(recognized, <String>['end']);
});
});
testGesture('Can filter long press based on device kind', (GestureTester tester) {
final LongPressGestureRecognizer mouseLongPress = LongPressGestureRecognizer(
supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.mouse },
);
bool mouseLongPressDown = false;
mouseLongPress.onLongPress = () {
mouseLongPressDown = true;
};
const PointerDownEvent mouseDown = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
kind: PointerDeviceKind.mouse,
);
const PointerDownEvent touchDown = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
);
// Touch events shouldn't be recognized.
mouseLongPress.addPointer(touchDown);
tester.closeArena(5);
expect(mouseLongPressDown, isFalse);
tester.route(touchDown);
expect(mouseLongPressDown, isFalse);
tester.async.elapse(const Duration(seconds: 2));
expect(mouseLongPressDown, isFalse);
// Mouse events are still recognized.
mouseLongPress.addPointer(mouseDown);
tester.closeArena(5);
expect(mouseLongPressDown, isFalse);
tester.route(mouseDown);
expect(mouseLongPressDown, isFalse);
tester.async.elapse(const Duration(seconds: 2));
expect(mouseLongPressDown, isTrue);
mouseLongPress.dispose();
});
group('Recognizers listening on different buttons do not form competition:', () {
// This test is assisted by tap recognizers. If a tap gesture has
// no competing recognizers, a pointer down event triggers its onTapDown
// immediately; if there are competitors, onTapDown is triggered after a
// timeout.
// The following tests make sure that long press recognizers do not form
// competition with a tap gesture recognizer listening on a different button.
final List<String> recognized = <String>[];
late TapGestureRecognizer tapPrimary;
late TapGestureRecognizer tapSecondary;
late LongPressGestureRecognizer longPress;
setUp(() {
tapPrimary = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('tapPrimary');
};
tapSecondary = TapGestureRecognizer()
..onSecondaryTapDown = (TapDownDetails details) {
recognized.add('tapSecondary');
};
longPress = LongPressGestureRecognizer()
..onLongPressStart = (_) {
recognized.add('longPress');
};
});
tearDown(() {
recognized.clear();
tapPrimary.dispose();
tapSecondary.dispose();
longPress.dispose();
});
testGesture('A primary long press recognizer does not form competition with a secondary tap recognizer', (GestureTester tester) {
longPress.addPointer(down3);
tapSecondary.addPointer(down3);
tester.closeArena(down3.pointer);
tester.route(down3);
expect(recognized, <String>['tapSecondary']);
});
testGesture('A primary long press recognizer forms competition with a primary tap recognizer', (GestureTester tester) {
longPress.addPointer(down);
tapPrimary.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
expect(recognized, <String>[]);
tester.route(up);
expect(recognized, <String>['tapPrimary']);
});
});
testGesture('A secondary long press should not trigger primary', (GestureTester tester) {
final List<String> recognized = <String>[];
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('primaryStart');
}
..onLongPress = () {
recognized.add('primary');
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('primaryUpdate');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('primaryEnd');
}
..onLongPressUp = () {
recognized.add('primaryUp');
};
const PointerDownEvent down2 = PointerDownEvent(
pointer: 2,
buttons: kSecondaryButton,
position: Offset(30.0, 30.0),
);
const PointerMoveEvent move2 = PointerMoveEvent(
pointer: 2,
buttons: kSecondaryButton,
position: Offset(100, 200),
);
const PointerUpEvent up2 = PointerUpEvent(
pointer: 2,
position: Offset(100, 201),
);
longPress.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(move2);
tester.route(up2);
expect(recognized, <String>[]);
longPress.dispose();
recognized.clear();
});
testGesture('A tertiary long press should not trigger primary or secondary', (GestureTester tester) {
final List<String> recognized = <String>[];
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('primaryStart');
}
..onLongPress = () {
recognized.add('primary');
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('primaryUpdate');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('primaryEnd');
}
..onLongPressUp = () {
recognized.add('primaryUp');
}
..onSecondaryLongPressStart = (LongPressStartDetails details) {
recognized.add('secondaryStart');
}
..onSecondaryLongPress = () {
recognized.add('secondary');
}
..onSecondaryLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('secondaryUpdate');
}
..onSecondaryLongPressEnd = (LongPressEndDetails details) {
recognized.add('secondaryEnd');
}
..onSecondaryLongPressUp = () {
recognized.add('secondaryUp');
};
const PointerDownEvent down2 = PointerDownEvent(
pointer: 2,
buttons: kTertiaryButton,
position: Offset(30.0, 30.0),
);
const PointerMoveEvent move2 = PointerMoveEvent(
pointer: 2,
buttons: kTertiaryButton,
position: Offset(100, 200),
);
const PointerUpEvent up2 = PointerUpEvent(
pointer: 2,
position: Offset(100, 201),
);
longPress.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(move2);
tester.route(up2);
expect(recognized, <String>[]);
longPress.dispose();
recognized.clear();
});
testGesture('Switching buttons mid-stream does not fail to send "end" event', (GestureTester tester) {
final List<String> recognized = <String>[];
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('primaryStart');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('primaryEnd');
};
const PointerDownEvent down4 = PointerDownEvent(
pointer: 8,
position: Offset(10, 10),
);
const PointerMoveEvent move4 = PointerMoveEvent(
pointer: 8,
position: Offset(100, 200),
buttons: kPrimaryButton | kSecondaryButton,
);
const PointerUpEvent up4 = PointerUpEvent(
pointer: 8,
position: Offset(100, 200),
buttons: kSecondaryButton,
);
longPress.addPointer(down4);
tester.closeArena(4);
tester.route(down4);
tester.async.elapse(const Duration(milliseconds: 1000));
recognized.add('two seconds later...');
tester.route(move4);
tester.async.elapse(const Duration(milliseconds: 1000));
recognized.add('two more seconds later...');
tester.route(up4);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['primaryStart', 'two seconds later...', 'two more seconds later...', 'primaryEnd']);
longPress.dispose();
});
testGesture('Switching buttons mid-stream does not fail to send "end" event (alternative sequence)', (GestureTester tester) {
// This reproduces sequences seen on macOS.
final List<String> recognized = <String>[];
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('primaryStart');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('primaryEnd');
};
const PointerDownEvent down5 = PointerDownEvent(
pointer: 9,
position: Offset(10, 10),
);
const PointerMoveEvent move5a = PointerMoveEvent(
pointer: 9,
position: Offset(100, 200),
buttons: 3, // add 2
);
const PointerMoveEvent move5b = PointerMoveEvent(
pointer: 9,
position: Offset(100, 200),
buttons: 2, // remove 1
);
const PointerUpEvent up5 = PointerUpEvent(
pointer: 9,
position: Offset(100, 200),
);
longPress.addPointer(down5);
tester.closeArena(4);
tester.route(down5);
tester.async.elapse(const Duration(milliseconds: 1000));
recognized.add('two seconds later...');
tester.route(move5a);
tester.async.elapse(const Duration(milliseconds: 1000));
recognized.add('two more seconds later...');
tester.route(move5b);
tester.async.elapse(const Duration(milliseconds: 1000));
recognized.add('two more seconds later still...');
tester.route(up5);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['primaryStart', 'two seconds later...', 'two more seconds later...', 'two more seconds later still...', 'primaryEnd']);
longPress.dispose();
});
}