| // 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), |
| ); |
| |
| void main() { |
| setUp(ensureGestureBinding); |
| |
| group('Long press', () { |
| LongPressGestureRecognizer longPress; |
| bool longPressDown; |
| 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 | kMiddleMouseButton, |
| 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: kMiddleMouseButton, |
| 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', () { |
| LongPressGestureRecognizer longPressDrag; |
| bool longPressStart; |
| 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>[]; |
| |
| 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>[]; |
| TapGestureRecognizer tapPrimary; |
| TapGestureRecognizer tapSecondary; |
| 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(); |
| }); |
| } |