// 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 '../gestures/gesture_tester.dart'; // Anything longer than [kDoubleTapTimeout] will reset the consecutive tap count. final Duration kConsecutiveTapDelay = kDoubleTapTimeout ~/ 2; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late List<String> events; late BaseTapAndDragGestureRecognizer tapAndDrag; void setUpTapAndPanGestureRecognizer() { tapAndDrag = TapAndPanGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down ..maxConsecutiveTap = 3 ..onTapDown = (TapDragDownDetails details) { events.add('down#${details.consecutiveTapCount}'); } ..onTapUp = (TapDragUpDetails details) { events.add('up#${details.consecutiveTapCount}'); } ..onDragStart = (TapDragStartDetails details) { events.add('panstart#${details.consecutiveTapCount}'); } ..onDragUpdate = (TapDragUpdateDetails details) { events.add('panupdate#${details.consecutiveTapCount}'); } ..onDragEnd = (TapDragEndDetails details) { events.add('panend#${details.consecutiveTapCount}'); } ..onCancel = () { events.add('cancel'); }; } void setUpTapAndHorizontalDragGestureRecognizer() { tapAndDrag = TapAndHorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down ..maxConsecutiveTap = 3 ..onTapDown = (TapDragDownDetails details) { events.add('down#${details.consecutiveTapCount}'); } ..onTapUp = (TapDragUpDetails details) { events.add('up#${details.consecutiveTapCount}'); } ..onDragStart = (TapDragStartDetails details) { events.add('horizontaldragstart#${details.consecutiveTapCount}'); } ..onDragUpdate = (TapDragUpdateDetails details) { events.add('horizontaldragupdate#${details.consecutiveTapCount}'); } ..onDragEnd = (TapDragEndDetails details) { events.add('horizontaldragend#${details.consecutiveTapCount}'); } ..onCancel = () { events.add('cancel'); }; } setUp(() { events = <String>[]; }); // 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), ); const PointerCancelEvent cancel1 = PointerCancelEvent( pointer: 1, ); // 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), ); // Mouse Down/move/up sequence 6: intervening motion - kPrecisePointerPanSlop const PointerDownEvent down6 = PointerDownEvent( kind: PointerDeviceKind.mouse, pointer: 6, position: Offset(10.0, 10.0), ); const PointerMoveEvent move6 = PointerMoveEvent( kind: PointerDeviceKind.mouse, pointer: 6, position: Offset(15.0, 15.0), delta: Offset(5.0, 5.0), ); const PointerUpEvent up6 = PointerUpEvent( kind: PointerDeviceKind.mouse, pointer: 6, position: Offset(15.0, 15.0), ); testGesture('Recognizes consecutive taps', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.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(kConsecutiveTapDelay); tapAndDrag.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(kConsecutiveTapDelay); tapAndDrag.addPointer(down3); tester.closeArena(3); tester.route(down3); tester.route(up3); GestureBinding.instance.gestureArena.sweep(3); expect(events, <String>['down#3', 'up#3']); }); testGesture('Resets if times out in between taps', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.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)); tapAndDrag.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) { setUpTapAndPanGestureRecognizer(); tapAndDrag.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)); tapAndDrag.addPointer(down4); tester.closeArena(4); tester.route(down4); tester.route(up4); GestureBinding.instance.gestureArena.sweep(4); expect(events, <String>['down#1', 'up#1']); }); testGesture('Resets if consecutiveTapCount reaches maxConsecutiveTap', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); // First tap. tapAndDrag.addPointer(down1); tester.closeArena(1); tester.route(down1); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['down#1', 'up#1']); // Second tap. events.clear(); tapAndDrag.addPointer(down2); tester.closeArena(2); tester.route(down2); tester.route(up2); GestureBinding.instance.gestureArena.sweep(2); expect(events, <String>['down#2', 'up#2']); // Third tap. events.clear(); tapAndDrag.addPointer(down3); tester.closeArena(3); tester.route(down3); tester.route(up3); GestureBinding.instance.gestureArena.sweep(3); expect(events, <String>['down#3', 'up#3']); // Fourth tap. Here we arrived at the `maxConsecutiveTap` for `consecutiveTapCount` // so our count should reset and our new count should be `1`. events.clear(); tapAndDrag.addPointer(down3); tester.closeArena(3); tester.route(down3); tester.route(up3); GestureBinding.instance.gestureArena.sweep(3); expect(events, <String>['down#1', 'up#1']); }); testGesture('Should recognize drag', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final TestPointer pointer = TestPointer(5); final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(down); tester.closeArena(5); tester.route(down); tester.route(pointer.move(const Offset(40.0, 45.0))); tester.route(pointer.up()); GestureBinding.instance.gestureArena.sweep(5); expect(events, <String>['down#1', 'panstart#1', 'panupdate#1', 'panend#1']); }); testGesture('Recognizes consecutive taps + drag', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final TestPointer pointer = TestPointer(5); final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downA); tester.closeArena(5); tester.route(downA); tester.route(pointer.up()); GestureBinding.instance.gestureArena.sweep(5); tester.async.elapse(kConsecutiveTapDelay); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downB); tester.closeArena(5); tester.route(downB); tester.route(pointer.up()); GestureBinding.instance.gestureArena.sweep(5); tester.async.elapse(kConsecutiveTapDelay); final PointerDownEvent downC = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downC); tester.closeArena(5); tester.route(downC); tester.route(pointer.move(const Offset(40.0, 45.0))); tester.route(pointer.up()); expect(events, <String>[ 'down#1', 'up#1', 'down#2', 'up#2', 'down#3', 'panstart#3', 'panupdate#3', 'panend#3']); }); testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - before acceptance', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down2); tester.closeArena(1); tester.route(down1); 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', 'up#1']); }); testGesture('Calls tap up when the recognizer accepts before handleEvent is called', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.addPointer(down1); tester.closeArena(1); GestureBinding.instance.gestureArena.sweep(1); tester.route(down1); tester.route(up1); expect(events, <String>['down#1', 'up#1']); }); testGesture('Recognizer rejects pointer that is not the primary one (FILO) - before acceptance', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down2); tester.closeArena(1); tester.route(down1); tester.closeArena(2); tester.route(down2); tester.route(up2); GestureBinding.instance.gestureArena.sweep(2); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['down#1', 'up#1']); }); testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - after acceptance', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.addPointer(down1); tester.closeArena(1); tester.route(down1); tapAndDrag.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', 'up#1']); }); testGesture('Recognizer rejects pointer that is not the primary one (FILO) - after acceptance', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.addPointer(down1); tester.closeArena(1); tester.route(down1); tapAndDrag.addPointer(down2); tester.closeArena(2); tester.route(down2); tester.route(up2); GestureBinding.instance.gestureArena.sweep(2); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['down#1', 'up#1']); }); testGesture('Recognizer detects tap gesture when pointer does not move past tap tolerance', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); // In this test the tap has not travelled past the tap tolerance defined by // [kDoubleTapTouchSlop]. It is expected for the recognizer to detect a tap // and fire drag cancel. tapAndDrag.addPointer(down1); tester.closeArena(1); tester.route(down1); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['down#1', 'up#1']); }); testGesture('Recognizer detects drag gesture when pointer moves past tap tolerance but not the drag minimum', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); // In this test, the pointer has moved past the tap tolerance but it has // not reached the distance travelled to be considered a drag gesture. In // this case it is expected for the recognizer to detect a drag and fire tap cancel. tapAndDrag.addPointer(down5); tester.closeArena(5); tester.route(down5); tester.route(move5); tester.route(up5); GestureBinding.instance.gestureArena.sweep(5); expect(events, <String>['down#1', 'panstart#1', 'panend#1']); }); testGesture('Beats TapGestureRecognizer when mouse pointer moves past kPrecisePointerPanSlop', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); // This is a regression test for https://github.com/flutter/flutter/issues/122141. final TapGestureRecognizer taps = TapGestureRecognizer() ..onTapDown = (TapDownDetails details) { events.add('tapdown'); } ..onTapUp = (TapUpDetails details) { events.add('tapup'); } ..onTapCancel = () { events.add('tapscancel'); }; tapAndDrag.addPointer(down6); taps.addPointer(down6); tester.closeArena(6); tester.route(down6); tester.route(move6); tester.route(up6); GestureBinding.instance.gestureArena.sweep(6); expect(events, <String>['down#1', 'panstart#1', 'panupdate#1', 'panend#1']); }); testGesture('Recognizer declares self-victory in a non-empty arena when pointer travels minimum distance to be considered a drag', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final PanGestureRecognizer pans = PanGestureRecognizer() ..onStart = (DragStartDetails details) { events.add('panstart'); } ..onUpdate = (DragUpdateDetails details) { events.add('panupdate'); } ..onEnd = (DragEndDetails details) { events.add('panend'); } ..onCancel = () { events.add('pancancel'); }; final TestPointer pointer = TestPointer(5); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); // When competing against another [DragGestureRecognizer], the recognizer // that first in the arena will win after sweep is called. tapAndDrag.addPointer(downB); pans.addPointer(downB); tester.closeArena(5); tester.route(downB); tester.route(pointer.move(const Offset(40.0, 45.0))); tester.route(pointer.up()); expect(events, <String>[ 'pancancel', 'down#1', 'panstart#1', 'panupdate#1', 'panend#1']); }); testGesture('TapAndHorizontalDragGestureRecognizer accepts drag on a pan when the arena has already been won by the primary pointer', (GestureTester tester) { setUpTapAndHorizontalDragGestureRecognizer(); final TestPointer pointer = TestPointer(5); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downB); tester.closeArena(5); tester.route(downB); tester.route(pointer.move(const Offset(25.0, 45.0))); tester.route(pointer.up()); expect(events, <String>[ 'down#1', 'horizontaldragstart#1', 'horizontaldragupdate#1', 'horizontaldragend#1']); }); testGesture('TapAndHorizontalDragGestureRecognizer loses to VerticalDragGestureRecognizer on a vertical drag', (GestureTester tester) { setUpTapAndHorizontalDragGestureRecognizer(); final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer() ..onStart = (DragStartDetails details) { events.add('verticalstart'); } ..onUpdate = (DragUpdateDetails details) { events.add('verticalupdate'); } ..onEnd = (DragEndDetails details) { events.add('verticalend'); } ..onCancel = () { events.add('verticalcancel'); }; final TestPointer pointer = TestPointer(5); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downB); verticalDrag.addPointer(downB); tester.closeArena(5); tester.route(downB); tester.route(pointer.move(const Offset(10.0, 45.0))); tester.route(pointer.move(const Offset(10.0, 100.0))); tester.route(pointer.up()); expect(events, <String>[ 'verticalstart', 'verticalupdate', 'verticalend']); }); testGesture('TapAndPanGestureRecognizer loses to VerticalDragGestureRecognizer on a vertical drag', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer() ..onStart = (DragStartDetails details) { events.add('verticalstart'); } ..onUpdate = (DragUpdateDetails details) { events.add('verticalupdate'); } ..onEnd = (DragEndDetails details) { events.add('verticalend'); } ..onCancel = () { events.add('verticalcancel'); }; final TestPointer pointer = TestPointer(5); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downB); verticalDrag.addPointer(downB); tester.closeArena(5); tester.route(downB); tester.route(pointer.move(const Offset(10.0, 45.0))); tester.route(pointer.move(const Offset(10.0, 100.0))); tester.route(pointer.up()); expect(events, <String>[ 'verticalstart', 'verticalupdate', 'verticalend']); }); testGesture('TapAndHorizontalDragGestureRecognizer beats VerticalDragGestureRecognizer on a horizontal drag', (GestureTester tester) { setUpTapAndHorizontalDragGestureRecognizer(); final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer() ..onStart = (DragStartDetails details) { events.add('verticalstart'); } ..onUpdate = (DragUpdateDetails details) { events.add('verticalupdate'); } ..onEnd = (DragEndDetails details) { events.add('verticalend'); } ..onCancel = () { events.add('verticalcancel'); }; final TestPointer pointer = TestPointer(5); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downB); verticalDrag.addPointer(downB); tester.closeArena(5); tester.route(downB); tester.route(pointer.move(const Offset(45.0, 10.0))); tester.route(pointer.up()); expect(events, <String>[ 'verticalcancel', 'down#1', 'horizontaldragstart#1', 'horizontaldragupdate#1', 'horizontaldragend#1']); }); testGesture('TapAndPanGestureRecognizer beats VerticalDragGestureRecognizer on a horizontal pan', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer() ..onStart = (DragStartDetails details) { events.add('verticalstart'); } ..onUpdate = (DragUpdateDetails details) { events.add('verticalupdate'); } ..onEnd = (DragEndDetails details) { events.add('verticalend'); } ..onCancel = () { events.add('verticalcancel'); }; final TestPointer pointer = TestPointer(5); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downB); verticalDrag.addPointer(downB); tester.closeArena(5); tester.route(downB); tester.route(pointer.move(const Offset(45.0, 25.0))); tester.route(pointer.up()); expect(events, <String>[ 'verticalcancel', 'down#1', 'panstart#1', 'panupdate#1', 'panend#1']); }); testGesture('Beats LongPressGestureRecognizer on a consecutive tap greater than one', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final LongPressGestureRecognizer longpress = LongPressGestureRecognizer() ..onLongPressStart = (LongPressStartDetails details) { events.add('longpressstart'); } ..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) { events.add('longpressmoveupdate'); } ..onLongPressEnd = (LongPressEndDetails details) { events.add('longpressend'); } ..onLongPressCancel = () { events.add('longpresscancel'); }; final TestPointer pointer = TestPointer(5); final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downA); longpress.addPointer(downA); tester.closeArena(5); tester.route(downA); tester.route(pointer.up()); GestureBinding.instance.gestureArena.sweep(5); tester.async.elapse(kConsecutiveTapDelay); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); tapAndDrag.addPointer(downB); longpress.addPointer(downB); tester.closeArena(5); tester.route(downB); tester.async.elapse(const Duration(milliseconds: 500)); tester.route(pointer.move(const Offset(40.0, 45.0))); tester.route(pointer.up()); expect(events, <String>[ 'longpresscancel', 'down#1', 'up#1', 'down#2', 'panstart#2', 'panupdate#2', 'panend#2']); }); // This is a regression test for https://github.com/flutter/flutter/issues/129161. testGesture('Beats TapGestureRecognizer and DoubleTapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final TapGestureRecognizer taps = TapGestureRecognizer() ..onTapDown = (TapDownDetails details) { events.add('tapdown'); } ..onTapUp = (TapUpDetails details) { events.add('tapup'); } ..onTapCancel = () { events.add('tapscancel'); }; final DoubleTapGestureRecognizer doubleTaps = DoubleTapGestureRecognizer() ..onDoubleTapDown = (TapDownDetails details) { events.add('doubletapdown'); } ..onDoubleTap = () { events.add('doubletapup'); } ..onDoubleTapCancel = () { events.add('doubletapcancel'); }; tapAndDrag.addPointer(down1); taps.addPointer(down1); doubleTaps.addPointer(down1); tester.closeArena(1); tester.route(down1); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); // Wait for GestureArena to resolve itself. tester.async.elapse(kDoubleTapTimeout); expect(events, <String>['down#1', 'up#1']); }); testGesture('Beats TapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final TapGestureRecognizer taps = TapGestureRecognizer() ..onTapDown = (TapDownDetails details) { events.add('tapdown'); } ..onTapUp = (TapUpDetails details) { events.add('tapup'); } ..onTapCancel = () { events.add('tapscancel'); }; tapAndDrag.addPointer(down1); taps.addPointer(down1); tester.closeArena(1); tester.route(down1); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['down#1', 'up#1']); }); testGesture('Beats TapGestureRecognizer when the pointer has exceeded the slop tolerance', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final TapGestureRecognizer taps = TapGestureRecognizer() ..onTapDown = (TapDownDetails details) { events.add('tapdown'); } ..onTapUp = (TapUpDetails details) { events.add('tapup'); } ..onTapCancel = () { events.add('tapscancel'); }; tapAndDrag.addPointer(down5); taps.addPointer(down5); tester.closeArena(5); tester.route(down5); tester.route(move5); tester.route(up5); GestureBinding.instance.gestureArena.sweep(5); expect(events, <String>['down#1', 'panstart#1', 'panend#1']); events.clear(); tester.async.elapse(const Duration(milliseconds: 1000)); taps.addPointer(down1); tapAndDrag.addPointer(down1); tester.closeArena(1); tester.route(down1); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['tapdown', 'tapup']); }); testGesture('Ties with PanGestureRecognizer when pointer has not met sufficient global distance to be a drag', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); final PanGestureRecognizer pans = PanGestureRecognizer() ..onStart = (DragStartDetails details) { events.add('panstart'); } ..onUpdate = (DragUpdateDetails details) { events.add('panupdate'); } ..onEnd = (DragEndDetails details) { events.add('panend'); } ..onCancel = () { events.add('pancancel'); }; tapAndDrag.addPointer(down5); pans.addPointer(down5); tester.closeArena(5); tester.route(down5); tester.route(move5); tester.route(up5); GestureBinding.instance.gestureArena.sweep(5); expect(events, <String>['pancancel']); }); testGesture('Defaults to drag when pointer dragged past slop tolerance', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.addPointer(down5); tester.closeArena(5); tester.route(down5); tester.route(move5); tester.route(up5); GestureBinding.instance.gestureArena.sweep(5); expect(events, <String>['down#1', 'panstart#1', 'panend#1']); events.clear(); tester.async.elapse(const Duration(milliseconds: 1000)); tapAndDrag.addPointer(down1); tester.closeArena(1); tester.route(down1); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['down#1', 'up#1']); }); testGesture('Fires cancel and resets for PointerCancelEvent', (GestureTester tester) { setUpTapAndPanGestureRecognizer(); tapAndDrag.addPointer(down1); tester.closeArena(1); tester.route(down1); tester.route(cancel1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['down#1', 'cancel']); events.clear(); tester.async.elapse(const Duration(milliseconds: 100)); tapAndDrag.addPointer(down2); tester.closeArena(2); tester.route(down2); tester.route(up2); GestureBinding.instance.gestureArena.sweep(2); expect(events, <String>['down#1', 'up#1']); }); // This is a regression test for https://github.com/flutter/flutter/issues/102084. testGesture('Does not call onDragEnd if not provided', (GestureTester tester) { tapAndDrag = TapAndDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down ..maxConsecutiveTap = 3 ..onTapDown = (TapDragDownDetails details) { events.add('down#${details.consecutiveTapCount}'); }; FlutterErrorDetails? errorDetails; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { errorDetails = details; }; addTearDown(() { FlutterError.onError = oldHandler; }); tapAndDrag.addPointer(down5); tester.closeArena(5); tester.route(down5); tester.route(move5); tester.route(up5); GestureBinding.instance.gestureArena.sweep(5); expect(events, <String>['down#1']); expect(errorDetails, isNull); events.clear(); tester.async.elapse(const Duration(milliseconds: 1000)); tapAndDrag.addPointer(down1); tester.closeArena(1); tester.route(down1); tester.route(up1); GestureBinding.instance.gestureArena.sweep(1); expect(events, <String>['down#1']); }); }