// 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 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const Offset forcePressOffset = Offset(400.0, 50.0); testWidgets('Uncontested scrolls start immediately', (WidgetTester tester) async { bool didStartDrag = false; double? updatedDragDelta; bool didEndDrag = false; final Widget widget = GestureDetector( onVerticalDragStart: (DragStartDetails details) { didStartDrag = true; }, onVerticalDragUpdate: (DragUpdateDetails details) { updatedDragDelta = details.primaryDelta; }, onVerticalDragEnd: (DragEndDetails details) { didEndDrag = true; }, child: Container( color: const Color(0xFF00FF00), ), ); await tester.pumpWidget(widget); expect(didStartDrag, isFalse); expect(updatedDragDelta, isNull); expect(didEndDrag, isFalse); const Offset firstLocation = Offset(10.0, 10.0); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); expect(didStartDrag, isTrue); didStartDrag = false; expect(updatedDragDelta, isNull); expect(didEndDrag, isFalse); const Offset secondLocation = Offset(10.0, 9.0); await gesture.moveTo(secondLocation); expect(didStartDrag, isFalse); expect(updatedDragDelta, -1.0); updatedDragDelta = null; expect(didEndDrag, isFalse); await gesture.up(); expect(didStartDrag, isFalse); expect(updatedDragDelta, isNull); expect(didEndDrag, isTrue); didEndDrag = false; await tester.pumpWidget(Container()); }); testWidgets('Match two scroll gestures in succession', (WidgetTester tester) async { int gestureCount = 0; double dragDistance = 0.0; const Offset downLocation = Offset(10.0, 10.0); const Offset upLocation = Offset(10.0, 50.0); // must be far enough to be more than kTouchSlop final Widget widget = GestureDetector( dragStartBehavior: DragStartBehavior.down, onVerticalDragUpdate: (DragUpdateDetails details) { dragDistance += details.primaryDelta ?? 0; }, onVerticalDragEnd: (DragEndDetails details) { gestureCount += 1; }, onHorizontalDragUpdate: (DragUpdateDetails details) { fail('gesture should not match'); }, onHorizontalDragEnd: (DragEndDetails details) { fail('gesture should not match'); }, child: Container( color: const Color(0xFF00FF00), ), ); await tester.pumpWidget(widget); TestGesture gesture = await tester.startGesture(downLocation, pointer: 7); await gesture.moveTo(upLocation); await gesture.up(); gesture = await tester.startGesture(downLocation, pointer: 7); await gesture.moveTo(upLocation); await gesture.up(); expect(gestureCount, 2); expect(dragDistance, 40.0 * 2.0); // delta between down and up, twice await tester.pumpWidget(Container()); }); testWidgets("Pan doesn't crash", (WidgetTester tester) async { bool didStartPan = false; Offset? panDelta; bool didEndPan = false; await tester.pumpWidget( GestureDetector( onPanStart: (DragStartDetails details) { didStartPan = true; }, onPanUpdate: (DragUpdateDetails details) { panDelta = (panDelta ?? Offset.zero) + details.delta; }, onPanEnd: (DragEndDetails details) { didEndPan = true; }, child: Container( color: const Color(0xFF00FF00), ), ), ); expect(didStartPan, isFalse); expect(panDelta, isNull); expect(didEndPan, isFalse); await tester.dragFrom(const Offset(10.0, 10.0), const Offset(20.0, 30.0)); expect(didStartPan, isTrue); expect(panDelta!.dx, 20.0); expect(panDelta!.dy, 30.0); expect(didEndPan, isTrue); }); group('Tap', () { final ButtonVariant buttonVariant = ButtonVariant( values: <int>[kPrimaryButton, kSecondaryButton, kTertiaryButton], descriptions: <int, String>{ kPrimaryButton: 'primary', kSecondaryButton: 'secondary', kTertiaryButton: 'tertiary', }, ); testWidgets('Translucent', (WidgetTester tester) async { bool didReceivePointerDown; bool didTap; Future<void> pumpWidgetTree(HitTestBehavior? behavior) { return tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Stack( children: <Widget>[ Listener( onPointerDown: (_) { didReceivePointerDown = true; }, child: Container( width: 100.0, height: 100.0, color: const Color(0xFF00FF00), ), ), SizedBox( width: 100.0, height: 100.0, child: GestureDetector( onTap: ButtonVariant.button == kPrimaryButton ? () { didTap = true; } : null, onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () { didTap = true; } : null, onTertiaryTapDown: ButtonVariant.button == kTertiaryButton ? (_) { didTap = true; } : null, behavior: behavior, ), ), ], ), ), ); } didReceivePointerDown = false; didTap = false; await pumpWidgetTree(null); await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); expect(didReceivePointerDown, isTrue); expect(didTap, isTrue); didReceivePointerDown = false; didTap = false; await pumpWidgetTree(HitTestBehavior.deferToChild); await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); expect(didReceivePointerDown, isTrue); expect(didTap, isFalse); didReceivePointerDown = false; didTap = false; await pumpWidgetTree(HitTestBehavior.opaque); await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); expect(didReceivePointerDown, isFalse); expect(didTap, isTrue); didReceivePointerDown = false; didTap = false; await pumpWidgetTree(HitTestBehavior.translucent); await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); expect(didReceivePointerDown, isTrue); expect(didTap, isTrue); }, variant: buttonVariant); testWidgets('Empty', (WidgetTester tester) async { bool didTap = false; await tester.pumpWidget( Center( child: GestureDetector( onTap: ButtonVariant.button == kPrimaryButton ? () { didTap = true; } : null, onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () { didTap = true; } : null, onTertiaryTapUp: ButtonVariant.button == kTertiaryButton ? (_) { didTap = true; } : null, ), ), ); expect(didTap, isFalse); await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); expect(didTap, isTrue); }, variant: buttonVariant); testWidgets('Only container', (WidgetTester tester) async { bool didTap = false; await tester.pumpWidget( Center( child: GestureDetector( onTap: ButtonVariant.button == kPrimaryButton ? () { didTap = true; } : null, onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () { didTap = true; } : null, onTertiaryTapUp: ButtonVariant.button == kTertiaryButton ? (_) { didTap = true; } : null, child: Container(), ), ), ); expect(didTap, isFalse); await tester.tapAt(const Offset(10.0, 10.0)); expect(didTap, isFalse); }, variant: buttonVariant); testWidgets('cache render object', (WidgetTester tester) async { void inputCallback() { } await tester.pumpWidget( Center( child: GestureDetector( onTap: ButtonVariant.button == kPrimaryButton ? inputCallback : null, onSecondaryTap: ButtonVariant.button == kSecondaryButton ? inputCallback : null, onTertiaryTapUp: ButtonVariant.button == kTertiaryButton ? (_) => inputCallback() : null, child: Container(), ), ), ); final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector)); await tester.pumpWidget( Center( child: GestureDetector( onTap: ButtonVariant.button == kPrimaryButton ? inputCallback : null, onSecondaryTap: ButtonVariant.button == kSecondaryButton ? inputCallback : null, onTertiaryTapUp: ButtonVariant.button == kTertiaryButton ? (_) => inputCallback() : null, child: Container(), ), ), ); final RenderSemanticsGestureHandler renderObj2 = tester.renderObject(find.byType(GestureDetector)); expect(renderObj1, same(renderObj2)); }, variant: buttonVariant); testWidgets('Tap down occurs after kPressTimeout', (WidgetTester tester) async { int tapDown = 0; int tap = 0; int tapCancel = 0; int longPress = 0; await tester.pumpWidget( Container( alignment: Alignment.topLeft, child: Container( alignment: Alignment.center, height: 100.0, color: const Color(0xFF00FF00), child: RawGestureDetector( behavior: HitTestBehavior.translucent, // Adding long press callbacks here will cause the on*TapDown callbacks to be executed only after // kPressTimeout has passed. Without the long press callbacks, there would be no press pointers // competing in the arena. Hence, we add them to the arena to test this behavior. // // We use a raw gesture detector directly here because gesture detector does // not expose callbacks for the tertiary variant of long presses, i.e. no onTertiaryLongPress* // callbacks are exposed in GestureDetector. // // The primary and secondary long press callbacks could also be put into the gesture detector below, // however, it is clearer when they are all in one place. gestures: <Type, GestureRecognizerFactory>{ LongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>( () => LongPressGestureRecognizer(), (LongPressGestureRecognizer instance) { instance ..onLongPress = ButtonVariant.button == kPrimaryButton ? () { longPress += 1; } : null ..onSecondaryLongPress = ButtonVariant.button == kSecondaryButton ? () { longPress += 1; } : null ..onTertiaryLongPress = ButtonVariant.button == kTertiaryButton ? () { longPress += 1; } : null; }, ), }, child: GestureDetector( onTapDown: ButtonVariant.button == kPrimaryButton ? (TapDownDetails details) { tapDown += 1; } : null, onSecondaryTapDown: ButtonVariant.button == kSecondaryButton ? (TapDownDetails details) { tapDown += 1; } : null, onTertiaryTapDown: ButtonVariant.button == kTertiaryButton ? (TapDownDetails details) { tapDown += 1; } : null, onTap: ButtonVariant.button == kPrimaryButton ? () { tap += 1; } : null, onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () { tap += 1; } : null, onTertiaryTapUp: ButtonVariant.button == kTertiaryButton ? (TapUpDetails details) { tap += 1; } : null, onTapCancel: ButtonVariant.button == kPrimaryButton ? () { tapCancel += 1; } : null, onSecondaryTapCancel: ButtonVariant.button == kSecondaryButton ? () { tapCancel += 1; } : null, onTertiaryTapCancel: ButtonVariant.button == kTertiaryButton ? () { tapCancel += 1; } : null, ), ), ), ), ); // Pointer is dragged from the center of the 800x100 gesture detector // to a point (400,300) below it. This should never call onTap. Future<void> dragOut(Duration timeout) async { final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0), buttons: ButtonVariant.button); // If the timeout is less than kPressTimeout the recognizer will not // trigger any callbacks. If the timeout is greater than kLongPressTimeout // then onTapDown, onLongPress, and onCancel will be called. await tester.pump(timeout); await gesture.moveTo(const Offset(400.0, 300.0)); await gesture.up(); } await dragOut(kPressTimeout * 0.5); // generates nothing expect(tapDown, 0); expect(tapCancel, 0); expect(tap, 0); expect(longPress, 0); await dragOut(kPressTimeout); // generates tapDown, tapCancel expect(tapDown, 1); expect(tapCancel, 1); expect(tap, 0); expect(longPress, 0); await dragOut(kLongPressTimeout); // generates tapDown, longPress, tapCancel expect(tapDown, 2); expect(tapCancel, 2); expect(tap, 0); expect(longPress, 1); }, variant: buttonVariant); testWidgets('Long Press Up Callback called after long press', (WidgetTester tester) async { int longPressUp = 0; await tester.pumpWidget( Container( alignment: Alignment.topLeft, child: Container( alignment: Alignment.center, height: 100.0, color: const Color(0xFF00FF00), child: RawGestureDetector( // We use a raw gesture detector directly here because gesture detector does // not expose callbacks for the tertiary variant of long presses, i.e. no onTertiaryLongPress* // callbacks are exposed in GestureDetector, and we want to test all three variants. // // The primary and secondary long press callbacks could also be put into the gesture detector below, // however, it is more convenient to have them all in one place. gestures: <Type, GestureRecognizerFactory>{ LongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>( () => LongPressGestureRecognizer(), (LongPressGestureRecognizer instance) { instance ..onLongPressUp = ButtonVariant.button == kPrimaryButton ? () { longPressUp += 1; } : null ..onSecondaryLongPressUp = ButtonVariant.button == kSecondaryButton ? () { longPressUp += 1; } : null ..onTertiaryLongPressUp = ButtonVariant.button == kTertiaryButton ? () { longPressUp += 1; } : null; }, ), }, ), ), ), ); Future<void> longPress(Duration timeout) async { final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0), buttons: ButtonVariant.button); await tester.pump(timeout); await gesture.up(); } await longPress(kLongPressTimeout + const Duration(seconds: 1)); // To make sure the time for long press has occurred expect(longPressUp, 1); }, variant: buttonVariant); }); testWidgets('Primary and secondary long press callbacks should work together in GestureDetector', (WidgetTester tester) async { bool primaryLongPress = false, secondaryLongPress = false; await tester.pumpWidget( Container( alignment: Alignment.topLeft, child: Container( alignment: Alignment.center, height: 100.0, color: const Color(0xFF00FF00), child: GestureDetector( onLongPress: () { primaryLongPress = true; }, onSecondaryLongPress: () { secondaryLongPress = true; }, ), ), ), ); Future<void> longPress(Duration timeout, int buttons) async { final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0), buttons: buttons); await tester.pump(timeout); await gesture.up(); } // Adding a second to make sure the time for long press has occurred. await longPress(kLongPressTimeout + const Duration(seconds: 1), kPrimaryButton); expect(primaryLongPress, isTrue); await longPress(kLongPressTimeout + const Duration(seconds: 1), kSecondaryButton); expect(secondaryLongPress, isTrue); }); testWidgets('Force Press Callback called after force press', (WidgetTester tester) async { int forcePressStart = 0; int forcePressPeaked = 0; int forcePressUpdate = 0; int forcePressEnded = 0; await tester.pumpWidget( Container( alignment: Alignment.topLeft, child: Container( alignment: Alignment.center, height: 100.0, color: const Color(0xFF00FF00), child: GestureDetector( onForcePressStart: (_) => forcePressStart += 1, onForcePressEnd: (_) => forcePressEnded += 1, onForcePressPeak: (_) => forcePressPeaked += 1, onForcePressUpdate: (_) => forcePressUpdate += 1, ), ), ), ); final int pointerValue = tester.nextPointer; final TestGesture gesture = await tester.createGesture(); await gesture.downWithCustomEvent( forcePressOffset, PointerDownEvent( pointer: pointerValue, position: forcePressOffset, pressure: 0.0, pressureMax: 6.0, pressureMin: 0.0, ), ); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.3, pressureMin: 0, )); expect(forcePressStart, 0); expect(forcePressPeaked, 0); expect(forcePressUpdate, 0); expect(forcePressEnded, 0); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.5, pressureMin: 0, )); expect(forcePressStart, 1); expect(forcePressPeaked, 0); expect(forcePressUpdate, 1); expect(forcePressEnded, 0); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.6, pressureMin: 0, )); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.7, pressureMin: 0, )); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.2, pressureMin: 0, )); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.3, pressureMin: 0, )); expect(forcePressStart, 1); expect(forcePressPeaked, 0); expect(forcePressUpdate, 5); expect(forcePressEnded, 0); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.9, pressureMin: 0, )); expect(forcePressStart, 1); expect(forcePressPeaked, 1); expect(forcePressUpdate, 6); expect(forcePressEnded, 0); await gesture.up(); expect(forcePressStart, 1); expect(forcePressPeaked, 1); expect(forcePressUpdate, 6); expect(forcePressEnded, 1); }); testWidgets('Force Press Callback not called if long press triggered before force press', (WidgetTester tester) async { int forcePressStart = 0; int longPressTimes = 0; await tester.pumpWidget( Container( alignment: Alignment.topLeft, child: Container( alignment: Alignment.center, height: 100.0, color: const Color(0xFF00FF00), child: GestureDetector( onForcePressStart: (_) => forcePressStart += 1, onLongPress: () => longPressTimes += 1, ), ), ), ); final int pointerValue = tester.nextPointer; const double maxPressure = 6.0; final TestGesture gesture = await tester.createGesture(); await gesture.downWithCustomEvent( forcePressOffset, PointerDownEvent( pointer: pointerValue, position: forcePressOffset, pressure: 0.0, pressureMax: maxPressure, pressureMin: 0.0, ), ); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, position: const Offset(400.0, 50.0), pressure: 0.3, pressureMin: 0, pressureMax: maxPressure, )); expect(forcePressStart, 0); expect(longPressTimes, 0); // Trigger the long press. await tester.pump(kLongPressTimeout + const Duration(seconds: 1)); expect(longPressTimes, 1); expect(forcePressStart, 0); // Failed attempt to trigger the force press. await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, position: const Offset(400.0, 50.0), pressure: 0.5, pressureMin: 0, pressureMax: maxPressure, )); expect(longPressTimes, 1); expect(forcePressStart, 0); }); testWidgets('Force Press Callback not called if drag triggered before force press', (WidgetTester tester) async { int forcePressStart = 0; int horizontalDragStart = 0; await tester.pumpWidget( Container( alignment: Alignment.topLeft, child: Container( alignment: Alignment.center, height: 100.0, color: const Color(0xFF00FF00), child: GestureDetector( onForcePressStart: (_) => forcePressStart += 1, onHorizontalDragStart: (_) => horizontalDragStart += 1, ), ), ), ); final int pointerValue = tester.nextPointer; final TestGesture gesture = await tester.createGesture(); await gesture.downWithCustomEvent( forcePressOffset, PointerDownEvent( pointer: pointerValue, position: forcePressOffset, pressure: 0.0, pressureMax: 6.0, pressureMin: 0.0, ), ); await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.3, pressureMin: 0, )); expect(forcePressStart, 0); expect(horizontalDragStart, 0); // Trigger horizontal drag. await gesture.moveBy(const Offset(100, 0)); expect(horizontalDragStart, 1); expect(forcePressStart, 0); // Failed attempt to trigger the force press. await gesture.updateWithCustomEvent(PointerMoveEvent( pointer: pointerValue, pressure: 0.5, pressureMin: 0, )); expect(horizontalDragStart, 1); expect(forcePressStart, 0); }); group("RawGestureDetectorState's debugFillProperties", () { testWidgets('when default', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final GlobalKey key = GlobalKey(); await tester.pumpWidget(RawGestureDetector( key: key, )); key.currentState!.debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, <String>[ 'gestures: <none>', ]); }); testWidgets('should show gestures, custom semantics and behavior', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final GlobalKey key = GlobalKey(); await tester.pumpWidget(RawGestureDetector( key: key, behavior: HitTestBehavior.deferToChild, gestures: <Type, GestureRecognizerFactory>{ TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( () => TapGestureRecognizer(), (TapGestureRecognizer recognizer) { recognizer.onTap = () {}; }, ), LongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>( () => LongPressGestureRecognizer(), (LongPressGestureRecognizer recognizer) { recognizer.onLongPress = () {}; }, ), }, semantics: _EmptySemanticsGestureDelegate(), child: Container(), )); key.currentState!.debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, <String>[ 'gestures: tap, long press', 'semantics: _EmptySemanticsGestureDelegate()', 'behavior: deferToChild', ]); }); testWidgets('should not show semantics when excludeFromSemantics is true', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final GlobalKey key = GlobalKey(); await tester.pumpWidget(RawGestureDetector( key: key, semantics: _EmptySemanticsGestureDelegate(), excludeFromSemantics: true, child: Container(), )); key.currentState!.debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, <String>[ 'gestures: <none>', 'excludeFromSemantics: true', ]); }); group('error control test', () { test('constructor redundant pan and scale', () { late FlutterError error; try { GestureDetector(onScaleStart: (_) {}, onPanStart: (_) {}); } on FlutterError catch (e) { error = e; } finally { expect( error.toStringDeep(), 'FlutterError\n' ' Incorrect GestureDetector arguments.\n' ' Having both a pan gesture recognizer and a scale gesture\n' ' recognizer is redundant; scale is a superset of pan.\n' ' Just use the scale gesture recognizer.\n', ); expect(error.diagnostics.last.level, DiagnosticLevel.hint); expect( error.diagnostics.last.toStringDeep(), equalsIgnoringHashCodes( 'Just use the scale gesture recognizer.\n', ), ); } }); test('constructor duplicate drag recognizer', () { late FlutterError error; try { GestureDetector( onVerticalDragStart: (_) {}, onHorizontalDragStart: (_) {}, onPanStart: (_) {}, ); } on FlutterError catch (e) { error = e; } finally { expect( error.toStringDeep(), 'FlutterError\n' ' Incorrect GestureDetector arguments.\n' ' Simultaneously having a vertical drag gesture recognizer, a\n' ' horizontal drag gesture recognizer, and a pan gesture recognizer\n' ' will result in the pan gesture recognizer being ignored, since\n' ' the other two will catch all drags.\n', ); } }); testWidgets('replaceGestureRecognizers not during layout', (WidgetTester tester) async { final GlobalKey<RawGestureDetectorState> key = GlobalKey<RawGestureDetectorState>(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: RawGestureDetector( key: key, child: const Text('Text'), ), ), ); late FlutterError error; try { key.currentState!.replaceGestureRecognizers(<Type, GestureRecognizerFactory>{}); } on FlutterError catch (e) { error = e; } finally { expect(error.diagnostics.last.level, DiagnosticLevel.hint); expect( error.diagnostics.last.toStringDeep(), equalsIgnoringHashCodes( 'To set the gesture recognizers at other times, trigger a new\n' 'build using setState() and provide the new gesture recognizers as\n' 'constructor arguments to the corresponding RawGestureDetector or\n' 'GestureDetector object.\n', ), ); expect( error.toStringDeep(), 'FlutterError\n' ' Unexpected call to replaceGestureRecognizers() method of\n' ' RawGestureDetectorState.\n' ' The replaceGestureRecognizers() method can only be called during\n' ' the layout phase.\n' ' To set the gesture recognizers at other times, trigger a new\n' ' build using setState() and provide the new gesture recognizers as\n' ' constructor arguments to the corresponding RawGestureDetector or\n' ' GestureDetector object.\n', ); } }); }); }); testWidgets('supportedDevices update test', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/111716 bool didStartPan = false; Offset? panDelta; bool didEndPan = false; Widget buildFrame(Set<PointerDeviceKind>? supportedDevices) { return GestureDetector( onPanStart: (DragStartDetails details) { didStartPan = true; }, onPanUpdate: (DragUpdateDetails details) { panDelta = (panDelta ?? Offset.zero) + details.delta; }, onPanEnd: (DragEndDetails details) { didEndPan = true; }, supportedDevices: supportedDevices, child: Container( color: const Color(0xFF00FF00), ) ); } await tester.pumpWidget(buildFrame(<PointerDeviceKind>{PointerDeviceKind.mouse})); expect(didStartPan, isFalse); expect(panDelta, isNull); expect(didEndPan, isFalse); await tester.dragFrom(const Offset(10.0, 10.0), const Offset(20.0, 30.0), kind: PointerDeviceKind.mouse); // Matching device should allow gesture. expect(didStartPan, isTrue); expect(panDelta!.dx, 20.0); expect(panDelta!.dy, 30.0); expect(didEndPan, isTrue); didStartPan = false; panDelta = null; didEndPan = false; await tester.pumpWidget(buildFrame(<PointerDeviceKind>{PointerDeviceKind.stylus})); await tester.dragFrom(const Offset(10.0, 10.0), const Offset(20.0, 30.0), kind: PointerDeviceKind.mouse); // Non-matching device should not lead to any callbacks. expect(didStartPan, isFalse); expect(panDelta, isNull); expect(didEndPan, isFalse); await tester.dragFrom(const Offset(10.0, 10.0), const Offset(20.0, 30.0), kind: PointerDeviceKind.stylus); // Matching device should allow gesture. expect(didStartPan, isTrue); expect(panDelta!.dx, 20.0); expect(panDelta!.dy, 30.0); expect(didEndPan, isTrue); didStartPan = false; panDelta = null; didEndPan = false; // If set to null, events from all device types will be recognized await tester.pumpWidget(buildFrame(null)); await tester.dragFrom(const Offset(10.0, 10.0), const Offset(20.0, 30.0), kind: PointerDeviceKind.unknown); expect(didStartPan, isTrue); expect(panDelta!.dx, 20.0); expect(panDelta!.dy, 30.0); expect(didEndPan, isTrue); }); testWidgets('supportedDevices is respected', (WidgetTester tester) async { bool didStartPan = false; Offset? panDelta; bool didEndPan = false; await tester.pumpWidget( GestureDetector( onPanStart: (DragStartDetails details) { didStartPan = true; }, onPanUpdate: (DragUpdateDetails details) { panDelta = (panDelta ?? Offset.zero) + details.delta; }, onPanEnd: (DragEndDetails details) { didEndPan = true; }, supportedDevices: const <PointerDeviceKind>{PointerDeviceKind.mouse}, child: Container( color: const Color(0xFF00FF00), ) ), ); expect(didStartPan, isFalse); expect(panDelta, isNull); expect(didEndPan, isFalse); await tester.dragFrom(const Offset(10.0, 10.0), const Offset(20.0, 30.0), kind: PointerDeviceKind.mouse); // Matching device should allow gesture. expect(didStartPan, isTrue); expect(panDelta!.dx, 20.0); expect(panDelta!.dy, 30.0); expect(didEndPan, isTrue); didStartPan = false; panDelta = null; didEndPan = false; await tester.dragFrom(const Offset(10.0, 10.0), const Offset(20.0, 30.0), kind: PointerDeviceKind.stylus); // Non-matching device should not lead to any callbacks. expect(didStartPan, isFalse); expect(panDelta, isNull); expect(didEndPan, isFalse); }); group('DoubleTap', () { testWidgets('onDoubleTap is called even if onDoubleTapDown has not been not provided', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GestureDetector( onDoubleTap: () => log.add('double-tap'), child: Container( width: 100.0, height: 100.0, color: const Color(0xFF00FF00), ), ), ), ); await tester.tap(find.byType(Container)); await tester.pump(kDoubleTapMinTime); await tester.tap(find.byType(Container)); await tester.pumpAndSettle(); expect(log, <String>['double-tap']); }); testWidgets('onDoubleTapDown is called even if onDoubleTap has not been not provided', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GestureDetector( onDoubleTapDown: (_) => log.add('double-tap-down'), child: Container( width: 100.0, height: 100.0, color: const Color(0xFF00FF00), ), ), ), ); await tester.tap(find.byType(Container)); await tester.pump(kDoubleTapMinTime); await tester.tap(find.byType(Container)); await tester.pumpAndSettle(); expect(log, <String>['double-tap-down']); }); }); } class _EmptySemanticsGestureDelegate extends SemanticsGestureDelegate { @override void assignSemantics(RenderSemanticsGestureHandler renderObject) { } } /// A [TestVariant] that runs tests multiple times with different buttons. class ButtonVariant extends TestVariant<int> { const ButtonVariant({ required this.values, required this.descriptions, }) : assert(values.length != 0); @override final List<int> values; final Map<int, String> descriptions; static int button = 0; @override String describeValue(int value) { assert(descriptions.containsKey(value), 'Unknown button'); return descriptions[value]!; } @override Future<int> setUp(int value) async { final int oldValue = button; button = value; return oldValue; } @override Future<void> tearDown(int value, int memento) async { button = memento; } }