| // 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/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'gesture_tester.dart'; |
| |
| // Anything longer than [kDoubleTapTimeout] will reset the serial tap count. |
| final Duration kSerialTapDelay = kDoubleTapTimeout ~/ 2; |
| |
| void main() { |
| setUp(ensureGestureBinding); |
| |
| late List<String> events; |
| late SerialTapGestureRecognizer serial; |
| |
| setUp(() { |
| events = <String>[]; |
| serial = SerialTapGestureRecognizer() |
| ..onSerialTapDown = (SerialTapDownDetails details) { |
| events.add('down#${details.count}'); |
| } |
| ..onSerialTapCancel = (SerialTapCancelDetails details) { |
| events.add('cancel#${details.count}'); |
| } |
| ..onSerialTapUp = (SerialTapUpDetails details) { |
| events.add('up#${details.count}'); |
| }; |
| }); |
| |
| // Down/up pair 1: normal tap sequence |
| const PointerDownEvent down1 = PointerDownEvent( |
| pointer: 1, |
| position: Offset(10.0, 10.0), |
| ); |
| |
| const PointerCancelEvent cancel1 = PointerCancelEvent( |
| pointer: 1, |
| ); |
| |
| const PointerUpEvent up1 = PointerUpEvent( |
| pointer: 1, |
| position: Offset(11.0, 9.0), |
| ); |
| |
| // Down/up pair 2: normal tap sequence close to pair 1 |
| const PointerDownEvent down2 = PointerDownEvent( |
| pointer: 2, |
| position: Offset(12.0, 12.0), |
| ); |
| |
| const PointerUpEvent up2 = PointerUpEvent( |
| pointer: 2, |
| position: Offset(13.0, 11.0), |
| ); |
| |
| // Down/up pair 3: normal tap sequence close to pair 1 |
| const PointerDownEvent down3 = PointerDownEvent( |
| pointer: 3, |
| position: Offset(12.0, 12.0), |
| ); |
| |
| const PointerUpEvent up3 = PointerUpEvent( |
| pointer: 3, |
| position: Offset(13.0, 11.0), |
| ); |
| |
| // Down/up pair 4: normal tap sequence far away from pair 1 |
| const PointerDownEvent down4 = PointerDownEvent( |
| pointer: 4, |
| position: Offset(130.0, 130.0), |
| ); |
| |
| const PointerUpEvent up4 = PointerUpEvent( |
| pointer: 4, |
| position: Offset(131.0, 129.0), |
| ); |
| |
| // Down/move/up sequence 5: intervening motion |
| const PointerDownEvent down5 = PointerDownEvent( |
| pointer: 5, |
| position: Offset(10.0, 10.0), |
| ); |
| |
| const PointerMoveEvent move5 = PointerMoveEvent( |
| pointer: 5, |
| position: Offset(25.0, 25.0), |
| ); |
| |
| const PointerUpEvent up5 = PointerUpEvent( |
| pointer: 5, |
| position: Offset(25.0, 25.0), |
| ); |
| |
| // Down/up pair 7: normal tap sequence close to pair 1 but on secondary button |
| const PointerDownEvent down6 = PointerDownEvent( |
| pointer: 6, |
| position: Offset(10.0, 10.0), |
| buttons: kSecondaryMouseButton, |
| ); |
| |
| const PointerUpEvent up6 = PointerUpEvent( |
| pointer: 6, |
| position: Offset(11.0, 9.0), |
| ); |
| |
| testGesture('Recognizes serial taps', (GestureTester tester) { |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| |
| events.clear(); |
| tester.async.elapse(kSerialTapDelay); |
| serial.addPointer(down2); |
| tester.closeArena(2); |
| tester.route(down2); |
| tester.route(up2); |
| GestureBinding.instance!.gestureArena.sweep(2); |
| expect(events, <String>['down#2', 'up#2']); |
| |
| events.clear(); |
| tester.async.elapse(kSerialTapDelay); |
| serial.addPointer(down3); |
| tester.closeArena(3); |
| tester.route(down3); |
| tester.route(up3); |
| GestureBinding.instance!.gestureArena.sweep(3); |
| expect(events, <String>['down#3', 'up#3']); |
| }); |
| |
| // Because tap gesture will hold off on declaring victory. |
| testGesture('Wins over tap gesture below it in the tree', (GestureTester tester) { |
| bool recognizedSingleTap = false; |
| bool canceledSingleTap = false; |
| final TapGestureRecognizer singleTap = TapGestureRecognizer() |
| ..onTap = () { |
| recognizedSingleTap = true; |
| } |
| ..onTapCancel = () { |
| canceledSingleTap = true; |
| }; |
| |
| singleTap.addPointer(down1); |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.async.elapse(kPressTimeout); // To register the possible single tap. |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| expect(recognizedSingleTap, isFalse); |
| expect(canceledSingleTap, isTrue); |
| }); |
| |
| testGesture('Wins over tap gesture above it in the tree', (GestureTester tester) { |
| bool recognizedSingleTap = false; |
| bool canceledSingleTap = false; |
| final TapGestureRecognizer singleTap = TapGestureRecognizer() |
| ..onTap = () { |
| recognizedSingleTap = true; |
| } |
| ..onTapCancel = () { |
| canceledSingleTap = true; |
| }; |
| |
| serial.addPointer(down1); |
| singleTap.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.async.elapse(kPressTimeout); // To register the possible single tap. |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| expect(recognizedSingleTap, isFalse); |
| expect(canceledSingleTap, isTrue); |
| }); |
| |
| testGesture('Loses to release gesture below it in the tree', (GestureTester tester) { |
| bool recognizedRelease = false; |
| final ReleaseGestureRecognizer release = ReleaseGestureRecognizer() |
| ..onRelease = () { |
| recognizedRelease = true; |
| }; |
| |
| release.addPointer(down1); |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'cancel#1']); |
| expect(recognizedRelease, isTrue); |
| }); |
| |
| testGesture('Wins over release gesture above it in the tree', (GestureTester tester) { |
| bool recognizedRelease = false; |
| final ReleaseGestureRecognizer release = ReleaseGestureRecognizer() |
| ..onRelease = () { |
| recognizedRelease = true; |
| }; |
| |
| serial.addPointer(down1); |
| release.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| expect(recognizedRelease, isFalse); |
| }); |
| |
| testGesture('Fires cancel if competing recognizer declares victory', (GestureTester tester) { |
| final WinningGestureRecognizer winner = WinningGestureRecognizer(); |
| winner.addPointer(down1); |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'cancel#1']); |
| }); |
| |
| testGesture('Wins over double-tap recognizer below it in the tree', (GestureTester tester) { |
| bool recognizedDoubleTap = false; |
| final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer() |
| ..onDoubleTap = () { |
| recognizedDoubleTap = true; |
| }; |
| |
| doubleTap.addPointer(down1); |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| expect(recognizedDoubleTap, isFalse); |
| |
| events.clear(); |
| tester.async.elapse(kSerialTapDelay); |
| doubleTap.addPointer(down2); |
| serial.addPointer(down2); |
| tester.closeArena(2); |
| tester.route(down2); |
| tester.route(up2); |
| GestureBinding.instance!.gestureArena.sweep(2); |
| expect(events, <String>['down#2', 'up#2']); |
| expect(recognizedDoubleTap, isFalse); |
| |
| events.clear(); |
| tester.async.elapse(kSerialTapDelay); |
| serial.addPointer(down3); |
| tester.closeArena(3); |
| tester.route(down3); |
| tester.route(up3); |
| GestureBinding.instance!.gestureArena.sweep(3); |
| expect(events, <String>['down#3', 'up#3']); |
| }); |
| |
| testGesture('Wins over double-tap recognizer above it in the tree', (GestureTester tester) { |
| bool recognizedDoubleTap = false; |
| final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer() |
| ..onDoubleTap = () { |
| recognizedDoubleTap = true; |
| }; |
| |
| serial.addPointer(down1); |
| doubleTap.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| expect(recognizedDoubleTap, isFalse); |
| |
| events.clear(); |
| tester.async.elapse(kSerialTapDelay); |
| serial.addPointer(down2); |
| doubleTap.addPointer(down2); |
| tester.closeArena(2); |
| tester.route(down2); |
| tester.route(up2); |
| GestureBinding.instance!.gestureArena.sweep(2); |
| expect(events, <String>['down#2', 'up#2']); |
| expect(recognizedDoubleTap, isFalse); |
| |
| events.clear(); |
| tester.async.elapse(kSerialTapDelay); |
| serial.addPointer(down3); |
| doubleTap.addPointer(down3); |
| tester.closeArena(3); |
| tester.route(down3); |
| tester.route(up3); |
| GestureBinding.instance!.gestureArena.sweep(3); |
| expect(events, <String>['down#3', 'up#3']); |
| }); |
| |
| testGesture('Fires cancel and resets for PointerCancelEvent', (GestureTester tester) { |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(cancel1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'cancel#1']); |
| |
| events.clear(); |
| tester.async.elapse(const Duration(milliseconds: 100)); |
| serial.addPointer(down2); |
| tester.closeArena(2); |
| tester.route(down2); |
| tester.route(up2); |
| GestureBinding.instance!.gestureArena.sweep(2); |
| expect(events, <String>['down#1', 'up#1']); |
| }); |
| |
| testGesture('Fires cancel and resets when pointer dragged past slop tolerance', (GestureTester tester) { |
| serial.addPointer(down5); |
| tester.closeArena(5); |
| tester.route(down5); |
| tester.route(move5); |
| tester.route(up5); |
| GestureBinding.instance!.gestureArena.sweep(5); |
| expect(events, <String>['down#1', 'cancel#1']); |
| |
| events.clear(); |
| tester.async.elapse(const Duration(milliseconds: 1000)); |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| }); |
| |
| testGesture('Resets if times out in between taps', (GestureTester tester) { |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| |
| events.clear(); |
| tester.async.elapse(const Duration(milliseconds: 1000)); |
| serial.addPointer(down2); |
| tester.closeArena(2); |
| tester.route(down2); |
| tester.route(up2); |
| GestureBinding.instance!.gestureArena.sweep(2); |
| expect(events, <String>['down#1', 'up#1']); |
| }); |
| |
| testGesture('Resets if taps are far apart', (GestureTester tester) { |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| |
| events.clear(); |
| tester.async.elapse(const Duration(milliseconds: 100)); |
| serial.addPointer(down4); |
| tester.closeArena(4); |
| tester.route(down4); |
| tester.route(up4); |
| GestureBinding.instance!.gestureArena.sweep(4); |
| expect(events, <String>['down#1', 'up#1']); |
| }); |
| |
| testGesture('Serial taps with different buttons will start a new tap sequence', (GestureTester tester) { |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>['down#1', 'up#1']); |
| |
| events.clear(); |
| tester.async.elapse(const Duration(milliseconds: 1000)); |
| serial.addPointer(down6); |
| tester.closeArena(6); |
| tester.route(down6); |
| tester.route(up6); |
| GestureBinding.instance!.gestureArena.sweep(6); |
| expect(events, <String>['down#1', 'up#1']); |
| }); |
| |
| testGesture('Interleaving taps cancel first sequence and start second sequence', (GestureTester tester) { |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| |
| serial.addPointer(down2); |
| tester.closeArena(2); |
| tester.route(down2); |
| |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| tester.route(up2); |
| GestureBinding.instance!.gestureArena.sweep(2); |
| expect(events, <String>['down#1', 'cancel#1', 'down#1', 'up#1']); |
| }); |
| |
| testGesture('Is no-op if no callbacks are specified', (GestureTester tester) { |
| serial = SerialTapGestureRecognizer(); |
| serial.addPointer(down1); |
| tester.closeArena(1); |
| tester.route(down1); |
| expect(serial.isTrackingPointer, isFalse); |
| tester.route(up1); |
| GestureBinding.instance!.gestureArena.sweep(1); |
| expect(events, <String>[]); |
| }); |
| |
| testGesture('Works for non-primary button', (GestureTester tester) { |
| serial.addPointer(down6); |
| tester.closeArena(6); |
| tester.route(down6); |
| tester.route(up6); |
| GestureBinding.instance!.gestureArena.sweep(6); |
| expect(events, <String>['down#1', 'up#1']); |
| }); |
| } |
| |
| class WinningGestureRecognizer extends PrimaryPointerGestureRecognizer { |
| @override |
| String get debugDescription => 'winner'; |
| |
| @override |
| void handlePrimaryPointer(PointerEvent event) { |
| resolve(GestureDisposition.accepted); |
| } |
| } |
| |
| class ReleaseGestureRecognizer extends PrimaryPointerGestureRecognizer { |
| VoidCallback? onRelease; |
| |
| @override |
| String get debugDescription => 'release'; |
| |
| @override |
| void handlePrimaryPointer(PointerEvent event) { |
| if (event is PointerUpEvent) { |
| resolve(GestureDisposition.accepted); |
| if (onRelease != null) { |
| onRelease!(); |
| } |
| } |
| } |
| } |