// 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 '../flutter_test_alternative.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, ); const PointerUpEvent up3 = PointerUpEvent( pointer: 7, position: Offset(31, 29), ); // Down/up pair 4: tap sequence with tertiary button const PointerDownEvent down4 = PointerDownEvent( pointer: 8, position: Offset(42, 24), buttons: kTertiaryButton, ); const PointerUpEvent up4 = PointerUpEvent( pointer: 8, position: Offset(43, 23), ); void main() { setUp(ensureGestureBinding); group('Long press', () { late LongPressGestureRecognizer longPress; late bool longPressDown; late bool longPressUp; setUp(() { longPress = LongPressGestureRecognizer(); longPressDown = false; longPress.onLongPress = () { longPressDown = true; }; longPressUp = false; longPress.onLongPressUp = () { longPressUp = true; }; }); testGesture('Should recognize long press', (GestureTester tester) { longPress.addPointer(down); tester.closeArena(5); expect(longPressDown, isFalse); tester.route(down); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 700)); expect(longPressDown, isTrue); longPress.dispose(); }); testGesture('Should recognize long press with altered duration', (GestureTester tester) { longPress = LongPressGestureRecognizer(duration: const Duration(milliseconds: 100)); longPressDown = false; longPress.onLongPress = () { longPressDown = true; }; longPressUp = false; longPress.onLongPressUp = () { longPressUp = true; }; longPress.addPointer(down); tester.closeArena(5); expect(longPressDown, isFalse); tester.route(down); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 50)); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 50)); expect(longPressDown, isTrue); longPress.dispose(); }); testGesture('Up cancels long press', (GestureTester tester) { longPress.addPointer(down); tester.closeArena(5); expect(longPressDown, isFalse); tester.route(down); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressDown, isFalse); tester.route(up); expect(longPressDown, isFalse); tester.async.elapse(const Duration(seconds: 1)); expect(longPressDown, isFalse); longPress.dispose(); }); testGesture('Moving before accept cancels', (GestureTester tester) { longPress.addPointer(down); tester.closeArena(5); expect(longPressDown, isFalse); tester.route(down); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressDown, isFalse); tester.route(move); expect(longPressDown, isFalse); tester.async.elapse(const Duration(seconds: 1)); tester.route(up); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressDown, isFalse); expect(longPressUp, isFalse); longPress.dispose(); }); testGesture('Moving after accept is ok', (GestureTester tester) { longPress.addPointer(down); tester.closeArena(5); expect(longPressDown, isFalse); tester.route(down); expect(longPressDown, isFalse); tester.async.elapse(const Duration(seconds: 1)); expect(longPressDown, isTrue); tester.route(move); tester.route(up); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressDown, isTrue); expect(longPressUp, isTrue); longPress.dispose(); }); testGesture('Should recognize both tap down and long press', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); bool tapDownRecognized = false; tap.onTapDown = (_) { tapDownRecognized = true; }; tap.addPointer(down); longPress.addPointer(down); tester.closeArena(5); expect(tapDownRecognized, isFalse); expect(longPressDown, isFalse); tester.route(down); expect(tapDownRecognized, isFalse); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(tapDownRecognized, isTrue); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 700)); expect(tapDownRecognized, isTrue); expect(longPressDown, isTrue); tap.dispose(); longPress.dispose(); }); testGesture('Drag start delayed by microtask', (GestureTester tester) { final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); bool isDangerousStack = false; bool dragStartRecognized = false; drag.onStart = (DragStartDetails details) { expect(isDangerousStack, isFalse); dragStartRecognized = true; }; drag.addPointer(down); longPress.addPointer(down); tester.closeArena(5); expect(dragStartRecognized, isFalse); expect(longPressDown, isFalse); tester.route(down); expect(dragStartRecognized, isFalse); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(dragStartRecognized, isFalse); expect(longPressDown, isFalse); isDangerousStack = true; longPress.dispose(); isDangerousStack = false; expect(dragStartRecognized, isFalse); expect(longPressDown, isFalse); tester.async.flushMicrotasks(); expect(dragStartRecognized, isTrue); expect(longPressDown, isFalse); drag.dispose(); }); testGesture('Should recognize long press up', (GestureTester tester) { bool longPressUpRecognized = false; longPress.onLongPressUp = () { longPressUpRecognized = true; }; longPress.addPointer(down); tester.closeArena(5); expect(longPressUpRecognized, isFalse); tester.route(down); // kLongPressTimeout = 500; expect(longPressUpRecognized, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressUpRecognized, isFalse); tester.async.elapse(const Duration(milliseconds: 700)); tester.route(up); expect(longPressUpRecognized, isTrue); longPress.dispose(); }); testGesture('Should not recognize long press with more than one buttons', (GestureTester tester) { longPress.addPointer(const PointerDownEvent( pointer: 5, kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton | kTertiaryButton, position: Offset(10, 10), )); tester.closeArena(5); expect(longPressDown, isFalse); tester.route(down); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 1000)); expect(longPressDown, isFalse); tester.route(up); expect(longPressUp, isFalse); longPress.dispose(); }); testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) { longPress.addPointer(down); tester.closeArena(5); expect(longPressDown, isFalse); tester.route(down); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressDown, isFalse); tester.route(const PointerMoveEvent( pointer: 5, kind: PointerDeviceKind.mouse, buttons: kTertiaryButton, position: Offset(10, 10), )); expect(longPressDown, isFalse); tester.async.elapse(const Duration(milliseconds: 700)); expect(longPressDown, isFalse); tester.route(up); expect(longPressUp, isFalse); longPress.dispose(); }); }); group('long press drag', () { late LongPressGestureRecognizer longPressDrag; late bool longPressStart; late bool longPressUp; Offset? longPressDragUpdate; setUp(() { longPressDrag = LongPressGestureRecognizer(); longPressStart = false; longPressDrag.onLongPressStart = (LongPressStartDetails details) { longPressStart = true; }; longPressUp = false; longPressDrag.onLongPressEnd = (LongPressEndDetails details) { longPressUp = true; }; longPressDragUpdate = null; longPressDrag.onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) { longPressDragUpdate = details.globalPosition; }; }); testGesture('Should recognize long press down', (GestureTester tester) { longPressDrag.addPointer(down); tester.closeArena(5); expect(longPressStart, isFalse); tester.route(down); expect(longPressStart, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressStart, isFalse); tester.async.elapse(const Duration(milliseconds: 700)); expect(longPressStart, isTrue); longPressDrag.dispose(); }); testGesture('Short up cancels long press', (GestureTester tester) { longPressDrag.addPointer(down); tester.closeArena(5); expect(longPressStart, isFalse); tester.route(down); expect(longPressStart, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressStart, isFalse); tester.route(up); expect(longPressStart, isFalse); tester.async.elapse(const Duration(seconds: 1)); expect(longPressStart, isFalse); longPressDrag.dispose(); }); testGesture('Moving before accept cancels', (GestureTester tester) { longPressDrag.addPointer(down); tester.closeArena(5); expect(longPressStart, isFalse); tester.route(down); expect(longPressStart, isFalse); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressStart, isFalse); tester.route(move); expect(longPressStart, isFalse); tester.async.elapse(const Duration(seconds: 1)); tester.route(up); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressStart, isFalse); expect(longPressUp, isFalse); longPressDrag.dispose(); }); testGesture('Moving after accept does not cancel', (GestureTester tester) { longPressDrag.addPointer(down); tester.closeArena(5); expect(longPressStart, isFalse); tester.route(down); expect(longPressStart, isFalse); tester.async.elapse(const Duration(seconds: 1)); expect(longPressStart, isTrue); tester.route(move); expect(longPressDragUpdate, const Offset(100, 200)); tester.route(up); tester.async.elapse(const Duration(milliseconds: 300)); expect(longPressStart, isTrue); expect(longPressUp, isTrue); longPressDrag.dispose(); }); }); 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), ); final List<String> recognized = <String>[]; late LongPressGestureRecognizer longPress; setUp(() { longPress = LongPressGestureRecognizer() ..onLongPressStart = (LongPressStartDetails details) { recognized.add('start'); } ..onLongPressEnd = (LongPressEndDetails details) { recognized.add('end'); }; }); tearDown(() { longPress.dispose(); recognized.clear(); }); testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) { // First press longPress.addPointer(down); tester.closeArena(down.pointer); tester.route(down); tester.async.elapse(const Duration(milliseconds: 300)); tester.route(moveR); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 700)); tester.route(up); expect(recognized, <String>[]); }); testGesture('Buttons change before acceptance should not prevent the next long press', (GestureTester tester) { // First press longPress.addPointer(down); tester.closeArena(down.pointer); tester.route(down); tester.async.elapse(const Duration(milliseconds: 300)); tester.route(moveR); tester.async.elapse(const Duration(milliseconds: 700)); tester.route(up); recognized.clear(); // Second press longPress.addPointer(down2); tester.closeArena(down2.pointer); tester.route(down2); tester.async.elapse(const Duration(milliseconds: 1000)); expect(recognized, <String>['start']); recognized.clear(); tester.route(up2); expect(recognized, <String>['end']); }); testGesture('Should cancel long press when buttons change after acceptance', (GestureTester tester) { // First press longPress.addPointer(down); tester.closeArena(down.pointer); tester.route(down); tester.async.elapse(const Duration(milliseconds: 1000)); expect(recognized, <String>['start']); recognized.clear(); tester.route(moveR); expect(recognized, <String>[]); tester.route(up); expect(recognized, <String>[]); }); testGesture('Buttons change after acceptance should not prevent the next long press', (GestureTester tester) { // First press longPress.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 longPress.addPointer(down2); tester.closeArena(down2.pointer); tester.route(down2); tester.async.elapse(const Duration(milliseconds: 1000)); expect(recognized, <String>['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(kind: 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), kind: PointerDeviceKind.touch, ); // 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(); }); }