// 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'; class TestGestureArenaMember extends GestureArenaMember { @override void acceptGesture(int key) { } @override void rejectGesture(int key) { } } void main() { setUp(ensureGestureBinding); // Down/up pair 1: normal tap sequence const PointerDownEvent down1 = PointerDownEvent( pointer: 1, position: Offset(10.0, 10.0), ); const PointerUpEvent up1 = PointerUpEvent( pointer: 1, position: Offset(11.0, 9.0), ); // Down/up pair 2: normal tap sequence far away from pair 1 const PointerDownEvent down2 = PointerDownEvent( pointer: 2, position: Offset(30.0, 30.0), ); const PointerUpEvent up2 = PointerUpEvent( pointer: 2, position: Offset(31.0, 29.0), ); // Down/move/up sequence 3: intervening motion, more than kTouchSlop. (~21px) const PointerDownEvent down3 = PointerDownEvent( pointer: 3, position: Offset(10.0, 10.0), ); const PointerMoveEvent move3 = PointerMoveEvent( pointer: 3, position: Offset(25.0, 25.0), ); const PointerUpEvent up3 = PointerUpEvent( pointer: 3, position: Offset(25.0, 25.0), ); // Down/move/up sequence 4: intervening motion, less than kTouchSlop. (~17px) const PointerDownEvent down4 = PointerDownEvent( pointer: 4, position: Offset(10.0, 10.0), ); const PointerMoveEvent move4 = PointerMoveEvent( pointer: 4, position: Offset(22.0, 22.0), ); const PointerUpEvent up4 = PointerUpEvent( pointer: 4, position: Offset(22.0, 22.0), ); // Down/up sequence 5: tap sequence with secondary button const PointerDownEvent down5 = PointerDownEvent( pointer: 5, position: Offset(20.0, 20.0), buttons: kSecondaryButton, ); const PointerUpEvent up5 = PointerUpEvent( pointer: 5, position: Offset(20.0, 20.0), ); // Down/up sequence 6: tap sequence with tertiary button const PointerDownEvent down6 = PointerDownEvent( pointer: 6, position: Offset(20.0, 20.0), buttons: kTertiaryButton, ); const PointerUpEvent up6 = PointerUpEvent( pointer: 6, position: Offset(20.0, 20.0), ); testGesture('Should recognize tap', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); bool tapRecognized = false; tap.onTap = () { tapRecognized = true; }; tap.addPointer(down1); tester.closeArena(1); expect(tapRecognized, isFalse); tester.route(down1); expect(tapRecognized, isFalse); tester.route(up1); expect(tapRecognized, isTrue); GestureBinding.instance!.gestureArena.sweep(1); expect(tapRecognized, isTrue); tap.dispose(); }); testGesture('Details contain the correct device kind', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); TapDownDetails? lastDownDetails; TapUpDetails? lastUpDetails; tap.onTapDown = (TapDownDetails details) { lastDownDetails = details; }; tap.onTapUp = (TapUpDetails details) { lastUpDetails = details; }; const PointerDownEvent mouseDown = PointerDownEvent(pointer: 1, kind: PointerDeviceKind.mouse); const PointerUpEvent mouseUp = PointerUpEvent(pointer: 1, kind: PointerDeviceKind.mouse); tap.addPointer(mouseDown); tester.closeArena(1); tester.route(mouseDown); expect(lastDownDetails?.kind, PointerDeviceKind.mouse); tester.route(mouseUp); expect(lastUpDetails?.kind, PointerDeviceKind.mouse); tap.dispose(); }); testGesture('No duplicate tap events', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); int tapsRecognized = 0; tap.onTap = () { tapsRecognized++; }; tap.addPointer(down1); tester.closeArena(1); expect(tapsRecognized, 0); tester.route(down1); expect(tapsRecognized, 0); tester.route(up1); expect(tapsRecognized, 1); GestureBinding.instance!.gestureArena.sweep(1); expect(tapsRecognized, 1); tap.addPointer(down1); tester.closeArena(1); expect(tapsRecognized, 1); tester.route(down1); expect(tapsRecognized, 1); tester.route(up1); expect(tapsRecognized, 2); GestureBinding.instance!.gestureArena.sweep(1); expect(tapsRecognized, 2); tap.dispose(); }); testGesture('Should not recognize two overlapping taps (FIFO)', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); int tapsRecognized = 0; tap.onTap = () { tapsRecognized++; }; tap.addPointer(down1); tester.closeArena(1); expect(tapsRecognized, 0); tester.route(down1); expect(tapsRecognized, 0); tap.addPointer(down2); tester.closeArena(2); expect(tapsRecognized, 0); tester.route(down1); expect(tapsRecognized, 0); tester.route(up1); expect(tapsRecognized, 1); GestureBinding.instance!.gestureArena.sweep(1); expect(tapsRecognized, 1); tester.route(up2); expect(tapsRecognized, 1); GestureBinding.instance!.gestureArena.sweep(2); expect(tapsRecognized, 1); tap.dispose(); }); testGesture('Should not recognize two overlapping taps (FILO)', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); int tapsRecognized = 0; tap.onTap = () { tapsRecognized++; }; tap.addPointer(down1); tester.closeArena(1); expect(tapsRecognized, 0); tester.route(down1); expect(tapsRecognized, 0); tap.addPointer(down2); tester.closeArena(2); expect(tapsRecognized, 0); tester.route(down1); expect(tapsRecognized, 0); tester.route(up2); expect(tapsRecognized, 0); GestureBinding.instance!.gestureArena.sweep(2); expect(tapsRecognized, 0); tester.route(up1); expect(tapsRecognized, 1); GestureBinding.instance!.gestureArena.sweep(1); expect(tapsRecognized, 1); tap.dispose(); }); testGesture('Distance cancels tap', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); bool tapRecognized = false; tap.onTap = () { tapRecognized = true; }; bool tapCanceled = false; tap.onTapCancel = () { tapCanceled = true; }; tap.addPointer(down3); tester.closeArena(3); expect(tapRecognized, isFalse); expect(tapCanceled, isFalse); tester.route(down3); expect(tapRecognized, isFalse); expect(tapCanceled, isFalse); tester.route(move3); expect(tapRecognized, isFalse); expect(tapCanceled, isTrue); tester.route(up3); expect(tapRecognized, isFalse); expect(tapCanceled, isTrue); GestureBinding.instance!.gestureArena.sweep(3); expect(tapRecognized, isFalse); expect(tapCanceled, isTrue); tap.dispose(); }); testGesture('Short distance does not cancel tap', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); bool tapRecognized = false; tap.onTap = () { tapRecognized = true; }; bool tapCanceled = false; tap.onTapCancel = () { tapCanceled = true; }; tap.addPointer(down4); tester.closeArena(4); expect(tapRecognized, isFalse); expect(tapCanceled, isFalse); tester.route(down4); expect(tapRecognized, isFalse); expect(tapCanceled, isFalse); tester.route(move4); expect(tapRecognized, isFalse); expect(tapCanceled, isFalse); tester.route(up4); expect(tapRecognized, isTrue); expect(tapCanceled, isFalse); GestureBinding.instance!.gestureArena.sweep(4); expect(tapRecognized, isTrue); expect(tapCanceled, isFalse); tap.dispose(); }); testGesture('Timeout does not cancel tap', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); bool tapRecognized = false; tap.onTap = () { tapRecognized = true; }; tap.addPointer(down1); tester.closeArena(1); expect(tapRecognized, isFalse); tester.route(down1); expect(tapRecognized, isFalse); tester.async.elapse(const Duration(milliseconds: 500)); expect(tapRecognized, isFalse); tester.route(up1); expect(tapRecognized, isTrue); GestureBinding.instance!.gestureArena.sweep(1); expect(tapRecognized, isTrue); tap.dispose(); }); testGesture('Should yield to other arena members', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); bool tapRecognized = false; tap.onTap = () { tapRecognized = true; }; tap.addPointer(down1); final TestGestureArenaMember member = TestGestureArenaMember(); final GestureArenaEntry entry = GestureBinding.instance!.gestureArena.add(1, member); GestureBinding.instance!.gestureArena.hold(1); tester.closeArena(1); expect(tapRecognized, isFalse); tester.route(down1); expect(tapRecognized, isFalse); tester.route(up1); expect(tapRecognized, isFalse); GestureBinding.instance!.gestureArena.sweep(1); expect(tapRecognized, isFalse); entry.resolve(GestureDisposition.accepted); expect(tapRecognized, isFalse); tap.dispose(); }); testGesture('Should trigger on release of held arena', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); bool tapRecognized = false; tap.onTap = () { tapRecognized = true; }; tap.addPointer(down1); final TestGestureArenaMember member = TestGestureArenaMember(); final GestureArenaEntry entry = GestureBinding.instance!.gestureArena.add(1, member); GestureBinding.instance!.gestureArena.hold(1); tester.closeArena(1); expect(tapRecognized, isFalse); tester.route(down1); expect(tapRecognized, isFalse); tester.route(up1); expect(tapRecognized, isFalse); GestureBinding.instance!.gestureArena.sweep(1); expect(tapRecognized, isFalse); entry.resolve(GestureDisposition.rejected); tester.async.flushMicrotasks(); expect(tapRecognized, isTrue); tap.dispose(); }); testGesture('Should log exceptions from callbacks', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); tap.onTap = () { throw Exception(test); }; final FlutterExceptionHandler? previousErrorHandler = FlutterError.onError; bool gotError = false; FlutterError.onError = (FlutterErrorDetails details) { gotError = true; }; tap.addPointer(down1); tester.closeArena(1); tester.route(down1); expect(gotError, isFalse); tester.route(up1); expect(gotError, isTrue); FlutterError.onError = previousErrorHandler; tap.dispose(); }); testGesture('onTapCancel should show reason in the proper format', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); tap.onTapCancel = () { throw Exception(test); }; final FlutterExceptionHandler? previousErrorHandler = FlutterError.onError; bool gotError = false; FlutterError.onError = (FlutterErrorDetails details) { expect(details.toString().contains('"spontaneous onTapCancel"') , isTrue); gotError = true; }; const int pointer = 1; tap.addPointer(const PointerDownEvent(pointer: pointer)); tester.closeArena(pointer); tester.async.elapse(const Duration(milliseconds: 500)); tester.route(const PointerCancelEvent(pointer: pointer)); expect(gotError, isTrue); FlutterError.onError = previousErrorHandler; tap.dispose(); }); testGesture('No duplicate tap events', (GestureTester tester) { final TapGestureRecognizer tapA = TapGestureRecognizer(); final TapGestureRecognizer tapB = TapGestureRecognizer(); final List<String> log = <String>[]; tapA.onTapDown = (TapDownDetails details) { log.add('tapA onTapDown'); }; tapA.onTapUp = (TapUpDetails details) { log.add('tapA onTapUp'); }; tapA.onTap = () { log.add('tapA onTap'); }; tapA.onTapCancel = () { log.add('tapA onTapCancel'); }; tapB.onTapDown = (TapDownDetails details) { log.add('tapB onTapDown'); }; tapB.onTapUp = (TapUpDetails details) { log.add('tapB onTapUp'); }; tapB.onTap = () { log.add('tapB onTap'); }; tapB.onTapCancel = () { log.add('tapB onTapCancel'); }; log.add('start'); tapA.addPointer(down1); log.add('added 1 to A'); tapB.addPointer(down1); log.add('added 1 to B'); tester.closeArena(1); log.add('closed 1'); tester.route(down1); log.add('routed 1 down'); tester.route(up1); log.add('routed 1 up'); GestureBinding.instance!.gestureArena.sweep(1); log.add('swept 1'); tapA.addPointer(down2); log.add('down 2 to A'); tapB.addPointer(down2); log.add('down 2 to B'); tester.closeArena(2); log.add('closed 2'); tester.route(down2); log.add('routed 2 down'); tester.route(up2); log.add('routed 2 up'); GestureBinding.instance!.gestureArena.sweep(2); log.add('swept 2'); tapA.dispose(); log.add('disposed A'); tapB.dispose(); log.add('disposed B'); expect(log, <String>[ 'start', 'added 1 to A', 'added 1 to B', 'closed 1', 'routed 1 down', 'routed 1 up', 'tapA onTapDown', 'tapA onTapUp', 'tapA onTap', 'swept 1', 'down 2 to A', 'down 2 to B', 'closed 2', 'routed 2 down', 'routed 2 up', 'tapA onTapDown', 'tapA onTapUp', 'tapA onTap', 'swept 2', 'disposed A', 'disposed B', ]); }); testGesture('PointerCancelEvent cancels tap', (GestureTester tester) { const PointerDownEvent down = PointerDownEvent( pointer: 5, position: Offset(10.0, 10.0), ); const PointerCancelEvent cancel = PointerCancelEvent( pointer: 5, position: Offset(10.0, 10.0), ); final TapGestureRecognizer tap = TapGestureRecognizer(); final List<String> recognized = <String>[]; tap.onTapDown = (_) { recognized.add('down'); }; tap.onTapUp = (_) { recognized.add('up'); }; tap.onTap = () { recognized.add('tap'); }; tap.onTapCancel = () { recognized.add('cancel'); }; tap.addPointer(down); tester.closeArena(5); tester.async.elapse(const Duration(milliseconds: 5000)); expect(recognized, <String>['down']); tester.route(cancel); expect(recognized, <String>['down', 'cancel']); tap.dispose(); }); testGesture('PointerCancelEvent after exceeding deadline cancels tap', (GestureTester tester) { const PointerDownEvent down = PointerDownEvent( pointer: 5, position: Offset(10.0, 10.0), ); const PointerCancelEvent cancel = PointerCancelEvent( pointer: 5, position: Offset(10.0, 10.0), ); final TapGestureRecognizer tap = TapGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..onStart = (_) {}; // Need a callback to compete final List<String> recognized = <String>[]; tap.onTapDown = (_) { recognized.add('down'); }; tap.onTapUp = (_) { recognized.add('up'); }; tap.onTap = () { recognized.add('tap'); }; tap.onTapCancel = () { recognized.add('cancel'); }; tap.addPointer(down); drag.addPointer(down); tester.closeArena(5); tester.route(down); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 1000)); expect(recognized, <String>['down']); tester.route(cancel); expect(recognized, <String>['down', 'cancel']); tap.dispose(); drag.dispose(); }); testGesture('losing tap gesture recognizer does not send onTapCancel', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); final List<String> recognized = <String>[]; tap.onTapDown = (_) { recognized.add('down'); }; tap.onTapUp = (_) { recognized.add('up'); }; tap.onTap = () { recognized.add('tap'); }; tap.onTapCancel = () { recognized.add('cancel'); }; tap.addPointer(down3); drag.addPointer(down3); tester.closeArena(3); tester.route(move3); GestureBinding.instance!.gestureArena.sweep(3); expect(recognized, isEmpty); tap.dispose(); drag.dispose(); }); testGesture('non-primary pointers does not trigger timeout', (GestureTester tester) { // Regression test for https://github.com/flutter/flutter/issues/43310 // Pointer1 down, pointer2 down, then pointer 1 up, all within the timeout. // In this way, `BaseTapGestureRecognizer.didExceedDeadline` can be triggered // after its `_reset`. final TapGestureRecognizer tap = TapGestureRecognizer(); final List<String> recognized = <String>[]; tap.onTapDown = (_) { recognized.add('down'); }; tap.onTapUp = (_) { recognized.add('up'); }; tap.onTap = () { recognized.add('tap'); }; tap.onTapCancel = () { recognized.add('cancel'); }; tap.addPointer(down1); tester.closeArena(down1.pointer); tap.addPointer(down2); tester.closeArena(down2.pointer); expect(recognized, isEmpty); tester.route(up1); GestureBinding.instance!.gestureArena.sweep(down1.pointer); expect(recognized, <String>['down', 'up', 'tap']); recognized.clear(); // If regression happens, the following step will throw error tester.async.elapse(const Duration(milliseconds: 200)); expect(recognized, isEmpty); tester.route(up2); GestureBinding.instance!.gestureArena.sweep(down2.pointer); expect(recognized, isEmpty); tap.dispose(); }); group('Enforce consistent-button restriction:', () { // Change buttons during down-up sequence 1 const PointerMoveEvent move1lr = PointerMoveEvent( pointer: 1, position: Offset(10.0, 10.0), buttons: kPrimaryMouseButton | kSecondaryMouseButton, ); const PointerMoveEvent move1r = PointerMoveEvent( pointer: 1, position: Offset(10.0, 10.0), buttons: kSecondaryMouseButton, ); final List<String> recognized = <String>[]; late TapGestureRecognizer tap; setUp(() { tap = TapGestureRecognizer() ..onTapDown = (TapDownDetails details) { recognized.add('down'); } ..onTapUp = (TapUpDetails details) { recognized.add('up'); } ..onTapCancel = () { recognized.add('cancel'); }; }); tearDown(() { tap.dispose(); recognized.clear(); }); testGesture('changing buttons before TapDown should cancel gesture without sending cancel', (GestureTester tester) { tap.addPointer(down1); tester.closeArena(1); expect(recognized, <String>[]); tester.route(move1lr); expect(recognized, <String>[]); tester.route(move1r); expect(recognized, <String>[]); tester.route(up1); expect(recognized, <String>[]); tap.dispose(); }); testGesture('changing buttons before TapDown should not prevent the next tap', (GestureTester tester) { tap.addPointer(down1); tester.closeArena(1); tester.route(move1lr); tester.route(move1r); tester.route(up1); expect(recognized, <String>[]); tap.addPointer(down2); tester.closeArena(2); tester.async.elapse(const Duration(milliseconds: 1000)); tester.route(up2); expect(recognized, <String>['down', 'up']); tap.dispose(); }); testGesture('changing buttons after TapDown should cancel gesture and send cancel', (GestureTester tester) { tap.addPointer(down1); tester.closeArena(1); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 1000)); expect(recognized, <String>['down']); tester.route(move1lr); expect(recognized, <String>['down', 'cancel']); tester.route(move1r); expect(recognized, <String>['down', 'cancel']); tester.route(up1); expect(recognized, <String>['down', 'cancel']); tap.dispose(); }); testGesture('changing buttons after TapDown should not prevent the next tap', (GestureTester tester) { tap.addPointer(down1); tester.closeArena(1); tester.async.elapse(const Duration(milliseconds: 1000)); tester.route(move1lr); tester.route(move1r); tester.route(up1); GestureBinding.instance!.gestureArena.sweep(1); expect(recognized, <String>['down', 'cancel']); tap.addPointer(down2); tester.closeArena(2); tester.async.elapse(const Duration(milliseconds: 1000)); tester.route(up2); GestureBinding.instance!.gestureArena.sweep(2); expect(recognized, <String>['down', 'cancel', 'down', 'up']); tap.dispose(); }); }); group('Recognizers listening on different buttons do not form competition:', () { // If a tap gesture has no competitors, a pointer down event triggers // onTapDown immediately; if there are competitors, onTapDown is triggered // after a timeout. The following tests make sure that tap recognizers // listening on different buttons do not form competition. final List<String> recognized = <String>[]; late TapGestureRecognizer primary; late TapGestureRecognizer primary2; late TapGestureRecognizer secondary; late TapGestureRecognizer tertiary; setUp(() { primary = TapGestureRecognizer() ..onTapDown = (TapDownDetails details) { recognized.add('primaryDown'); } ..onTapUp = (TapUpDetails details) { recognized.add('primaryUp'); } ..onTapCancel = () { recognized.add('primaryCancel'); }; primary2 = TapGestureRecognizer() ..onTapDown = (TapDownDetails details) { recognized.add('primary2Down'); } ..onTapUp = (TapUpDetails details) { recognized.add('primary2Up'); } ..onTapCancel = () { recognized.add('primary2Cancel'); }; secondary = TapGestureRecognizer() ..onSecondaryTapDown = (TapDownDetails details) { recognized.add('secondaryDown'); } ..onSecondaryTapUp = (TapUpDetails details) { recognized.add('secondaryUp'); } ..onSecondaryTapCancel = () { recognized.add('secondaryCancel'); }; tertiary = TapGestureRecognizer() ..onTertiaryTapDown = (TapDownDetails details) { recognized.add('tertiaryDown'); } ..onTertiaryTapUp = (TapUpDetails details) { recognized.add('tertiaryUp'); } ..onTertiaryTapCancel = () { recognized.add('tertiaryCancel'); }; }); tearDown(() { recognized.clear(); primary.dispose(); primary2.dispose(); secondary.dispose(); tertiary.dispose(); }); testGesture('A primary tap recognizer does not form competition with a secondary tap recognizer', (GestureTester tester) { primary.addPointer(down1); secondary.addPointer(down1); tester.closeArena(1); tester.route(down1); expect(recognized, <String>['primaryDown']); recognized.clear(); tester.route(up1); expect(recognized, <String>['primaryUp']); }); testGesture('A primary tap recognizer does not form competition with a tertiary tap recognizer', (GestureTester tester) { primary.addPointer(down1); tertiary.addPointer(down1); tester.closeArena(1); tester.route(down1); expect(recognized, <String>['primaryDown']); recognized.clear(); tester.route(up1); expect(recognized, <String>['primaryUp']); }); testGesture('A primary tap recognizer forms competition with another primary tap recognizer', (GestureTester tester) { primary.addPointer(down1); primary2.addPointer(down1); tester.closeArena(1); tester.route(down1); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 500)); expect(recognized, <String>['primaryDown', 'primary2Down']); }); }); group('Gestures of different buttons trigger correct callbacks:', () { final List<String> recognized = <String>[]; late TapGestureRecognizer tap; const PointerCancelEvent cancel1 = PointerCancelEvent( pointer: 1, ); const PointerCancelEvent cancel5 = PointerCancelEvent( pointer: 5, ); const PointerCancelEvent cancel6 = PointerCancelEvent( pointer: 6, ); setUp(() { tap = TapGestureRecognizer() ..onTapDown = (TapDownDetails details) { recognized.add('primaryDown'); } ..onTap = () { recognized.add('primary'); } ..onTapUp = (TapUpDetails details) { recognized.add('primaryUp'); } ..onTapCancel = () { recognized.add('primaryCancel'); } ..onSecondaryTapDown = (TapDownDetails details) { recognized.add('secondaryDown'); } ..onSecondaryTapUp = (TapUpDetails details) { recognized.add('secondaryUp'); } ..onSecondaryTapCancel = () { recognized.add('secondaryCancel'); } ..onTertiaryTapDown = (TapDownDetails details) { recognized.add('tertiaryDown'); } ..onTertiaryTapUp = (TapUpDetails details) { recognized.add('tertiaryUp'); } ..onTertiaryTapCancel = () { recognized.add('tertiaryCancel'); }; }); tearDown(() { recognized.clear(); tap.dispose(); }); testGesture('A primary tap should trigger primary callbacks', (GestureTester tester) { tap.addPointer(down1); tester.closeArena(down1.pointer); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 500)); expect(recognized, <String>['primaryDown']); recognized.clear(); tester.route(up1); expect(recognized, <String>['primaryUp', 'primary']); GestureBinding.instance!.gestureArena.sweep(down1.pointer); }); testGesture('A primary tap cancel trigger primary callbacks', (GestureTester tester) { tap.addPointer(down1); tester.closeArena(down1.pointer); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 500)); expect(recognized, <String>['primaryDown']); recognized.clear(); tester.route(cancel1); expect(recognized, <String>['primaryCancel']); GestureBinding.instance!.gestureArena.sweep(down1.pointer); }); testGesture('A secondary tap should trigger secondary callbacks', (GestureTester tester) { tap.addPointer(down5); tester.closeArena(down5.pointer); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 500)); expect(recognized, <String>['secondaryDown']); recognized.clear(); tester.route(up5); GestureBinding.instance!.gestureArena.sweep(down5.pointer); expect(recognized, <String>['secondaryUp']); }); testGesture('A tertiary tap should trigger tertiary callbacks', (GestureTester tester) { tap.addPointer(down6); tester.closeArena(down6.pointer); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 500)); expect(recognized, <String>['tertiaryDown']); recognized.clear(); tester.route(up6); GestureBinding.instance!.gestureArena.sweep(down6.pointer); expect(recognized, <String>['tertiaryUp']); }); testGesture('A secondary tap cancel should trigger secondary callbacks', (GestureTester tester) { tap.addPointer(down5); tester.closeArena(down5.pointer); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 500)); expect(recognized, <String>['secondaryDown']); recognized.clear(); tester.route(cancel5); GestureBinding.instance!.gestureArena.sweep(down5.pointer); expect(recognized, <String>['secondaryCancel']); }); testGesture('A tertiary tap cancel should trigger tertiary callbacks', (GestureTester tester) { tap.addPointer(down6); tester.closeArena(down6.pointer); expect(recognized, <String>[]); tester.async.elapse(const Duration(milliseconds: 500)); expect(recognized, <String>['tertiaryDown']); recognized.clear(); tester.route(cancel6); GestureBinding.instance!.gestureArena.sweep(down6.pointer); expect(recognized, <String>['tertiaryCancel']); }); }); testGesture('A second tap after rejection is ignored', (GestureTester tester) { bool didTap = false; final TapGestureRecognizer tap = TapGestureRecognizer() ..onTap = () { didTap = true; }; // Add drag recognizer for competition final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..onStart = (_) {}; final TestPointer pointer1 = TestPointer(1); final PointerDownEvent down = pointer1.down(Offset.zero); drag.addPointer(down); tap.addPointer(down); tester.closeArena(1); // One-finger moves, canceling the tap tester.route(down); tester.route(pointer1.move(const Offset(50.0, 0))); // Add another finger final TestPointer pointer2 = TestPointer(2); final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0)); drag.addPointer(down2); tap.addPointer(down2); tester.closeArena(2); tester.route(down2); expect(didTap, isFalse); }); }