flutter_flutter/packages/flutter/test/widgets/gesture_detector_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

1479 lines
52 KiB
Dart

// 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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const forcePressOffset = Offset(400.0, 50.0);
testWidgets('Uncontested scrolls start immediately', (WidgetTester tester) async {
var didStartDrag = false;
double? updatedDragDelta;
var 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 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 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 {
var gestureCount = 0;
var dragDistance = 0.0;
const downLocation = Offset(10.0, 10.0);
const 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 {
var didStartPan = false;
Offset? panDelta;
var 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);
});
testWidgets('DragEndDetails returns the last known position', (WidgetTester tester) async {
var updateOffset = const Offset(10.0, 10.0);
const paddingOffset = EdgeInsets.all(10.0);
Offset? endOffset;
Offset? globalEndOffset;
await tester.pumpWidget(
Padding(
padding: paddingOffset,
child: GestureDetector(
onPanStart: (DragStartDetails details) {},
onPanUpdate: (DragUpdateDetails details) {
updateOffset += details.delta;
},
onPanEnd: (DragEndDetails details) {
endOffset = details.localPosition;
globalEndOffset = details.globalPosition;
},
child: Container(color: const Color(0xFF00FF00)),
),
),
);
await tester.dragFrom(const Offset(20.0, 20.0), const Offset(30.0, 40.0));
expect(endOffset, isNotNull);
expect(updateOffset, endOffset);
// Make sure details.globalPosition works correctly.
expect(
Offset(endOffset!.dx + paddingOffset.left, endOffset!.dy + paddingOffset.top),
globalEndOffset,
);
});
group('Tap', () {
final 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 {
var 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 {
var 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 {
var tapDown = 0;
var tap = 0;
var tapCancel = 0;
var 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 {
var 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 {
var 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 {
var forcePressStart = 0;
var forcePressPeaked = 0;
var forcePressUpdate = 0;
var 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 {
var forcePressStart = 0;
var 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 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 {
var forcePressStart = 0;
var 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('default semantics', () {
testWidgets('tap', (WidgetTester tester) async {
TapDownDetails? receivedTapDownDetails;
TapUpDetails? receivedTapUpDetails;
var tapped = false;
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawGestureDetector(
key: key,
gestures: <Type, GestureRecognizerFactory>{
TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(postAcceptSlopTolerance: null),
(TapGestureRecognizer instance) {
instance.onTapDown = (TapDownDetails details) {
receivedTapDownDetails = details;
};
instance.onTapUp = (TapUpDetails details) {
receivedTapUpDetails = details;
};
instance.onTap = () {
tapped = true;
};
},
),
},
child: const SizedBox(width: 20, height: 20),
),
),
),
),
);
final SemanticsNode node = tester.semantics.find(find.byKey(key));
expect(node.getSemanticsData().hasAction(SemanticsAction.tap), isTrue);
semanticsOwner.performAction(node.id, SemanticsAction.tap);
await tester.pump();
expect(receivedTapDownDetails!.localPosition, const Offset(10, 10));
expect(receivedTapDownDetails!.globalPosition, const Offset(400, 300));
expect(receivedTapUpDetails!.localPosition, const Offset(10, 10));
expect(receivedTapUpDetails!.globalPosition, const Offset(400, 300));
expect(tapped, isTrue);
});
testWidgets('long press', (WidgetTester tester) async {
LongPressDownDetails? receivedLongPressDownDetails;
LongPressStartDetails? receivedLongPressStartDetails;
LongPressEndDetails? receivedLongPressEndDetails;
var pressed = false;
var upped = false;
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawGestureDetector(
key: key,
gestures: <Type, GestureRecognizerFactory>{
LongPressGestureRecognizer:
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(),
(LongPressGestureRecognizer instance) {
instance.onLongPressDown = (LongPressDownDetails details) {
receivedLongPressDownDetails = details;
};
instance.onLongPressStart = (LongPressStartDetails details) {
receivedLongPressStartDetails = details;
};
instance.onLongPressEnd = (LongPressEndDetails details) {
receivedLongPressEndDetails = details;
};
instance.onLongPressUp = () {
upped = true;
};
instance.onLongPress = () {
pressed = true;
};
},
),
},
child: const SizedBox(width: 20, height: 20),
),
),
),
),
);
final SemanticsNode node = tester.semantics.find(find.byKey(key));
expect(node.getSemanticsData().hasAction(SemanticsAction.longPress), isTrue);
semanticsOwner.performAction(node.id, SemanticsAction.longPress);
await tester.pump();
expect(receivedLongPressDownDetails!.localPosition, const Offset(10, 10));
expect(receivedLongPressDownDetails!.globalPosition, const Offset(400, 300));
expect(receivedLongPressStartDetails!.localPosition, const Offset(10, 10));
expect(receivedLongPressStartDetails!.globalPosition, const Offset(400, 300));
expect(receivedLongPressEndDetails!.localPosition, const Offset(10, 10));
expect(receivedLongPressEndDetails!.globalPosition, const Offset(400, 300));
expect(pressed, isTrue);
expect(upped, isTrue);
});
testWidgets('horizontal drag', (WidgetTester tester) async {
DragDownDetails? receivedDragDownDetails;
DragStartDetails? receivedDragStartDetails;
DragUpdateDetails? receivedDragUpdateDetails;
DragEndDetails? receivedDragEndDetails;
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawGestureDetector(
key: key,
gestures: <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(),
(HorizontalDragGestureRecognizer instance) {
instance.onDown = (DragDownDetails details) {
receivedDragDownDetails = details;
};
instance.onStart = (DragStartDetails details) {
receivedDragStartDetails = details;
};
instance.onUpdate = (DragUpdateDetails details) {
receivedDragUpdateDetails = details;
};
instance.onEnd = (DragEndDetails details) {
receivedDragEndDetails = details;
};
},
),
},
child: const SizedBox(width: 20, height: 20),
),
),
),
),
);
final SemanticsNode node = tester.semantics.find(find.byKey(key));
expect(node.getSemanticsData().hasAction(SemanticsAction.scrollRight), isTrue);
semanticsOwner.performAction(node.id, SemanticsAction.scrollRight);
await tester.pump();
expect(receivedDragDownDetails!.localPosition, const Offset(10, 10));
expect(receivedDragDownDetails!.globalPosition, const Offset(400, 300));
expect(receivedDragStartDetails!.localPosition, const Offset(10, 10));
expect(receivedDragStartDetails!.globalPosition, const Offset(400, 300));
final Offset delta = receivedDragUpdateDetails!.delta;
final Offset local = const Offset(10, 10) + delta;
final RenderObject object = tester.renderObject(find.byKey(key));
final Matrix4 transform = object.getTransformTo(null);
final Offset global = MatrixUtils.transformPoint(transform, local);
expect(receivedDragEndDetails!.localPosition, local);
expect(receivedDragEndDetails!.globalPosition, global);
});
testWidgets('vertical drag', (WidgetTester tester) async {
DragDownDetails? receivedDragDownDetails;
DragStartDetails? receivedDragStartDetails;
DragUpdateDetails? receivedDragUpdateDetails;
DragEndDetails? receivedDragEndDetails;
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawGestureDetector(
key: key,
gestures: <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance.onDown = (DragDownDetails details) {
receivedDragDownDetails = details;
};
instance.onStart = (DragStartDetails details) {
receivedDragStartDetails = details;
};
instance.onUpdate = (DragUpdateDetails details) {
receivedDragUpdateDetails = details;
};
instance.onEnd = (DragEndDetails details) {
receivedDragEndDetails = details;
};
},
),
},
child: const SizedBox(width: 20, height: 20),
),
),
),
),
);
final SemanticsNode node = tester.semantics.find(find.byKey(key));
expect(node.getSemanticsData().hasAction(SemanticsAction.scrollUp), isTrue);
semanticsOwner.performAction(node.id, SemanticsAction.scrollUp);
await tester.pump();
expect(receivedDragDownDetails!.localPosition, const Offset(10, 10));
expect(receivedDragDownDetails!.globalPosition, const Offset(400, 300));
expect(receivedDragStartDetails!.localPosition, const Offset(10, 10));
expect(receivedDragStartDetails!.globalPosition, const Offset(400, 300));
final Offset delta = receivedDragUpdateDetails!.delta;
final Offset local = const Offset(10, 10) + delta;
final RenderObject object = tester.renderObject(find.byKey(key));
final Matrix4 transform = object.getTransformTo(null);
final Offset global = MatrixUtils.transformPoint(transform, local);
expect(receivedDragEndDetails!.localPosition, local);
expect(receivedDragEndDetails!.globalPosition, global);
});
testWidgets('pan', (WidgetTester tester) async {
DragDownDetails? receivedDragDownDetails;
DragStartDetails? receivedDragStartDetails;
DragUpdateDetails? receivedDragUpdateDetails;
DragEndDetails? receivedDragEndDetails;
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawGestureDetector(
key: key,
gestures: <Type, GestureRecognizerFactory>{
PanGestureRecognizer: GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(),
(PanGestureRecognizer instance) {
instance.onDown = (DragDownDetails details) {
receivedDragDownDetails = details;
};
instance.onStart = (DragStartDetails details) {
receivedDragStartDetails = details;
};
instance.onUpdate = (DragUpdateDetails details) {
receivedDragUpdateDetails = details;
};
instance.onEnd = (DragEndDetails details) {
receivedDragEndDetails = details;
};
},
),
},
child: const SizedBox(width: 20, height: 20),
),
),
),
),
);
final SemanticsNode node = tester.semantics.find(find.byKey(key));
expect(node.getSemanticsData().hasAction(SemanticsAction.scrollRight), isTrue);
semanticsOwner.performAction(node.id, SemanticsAction.scrollRight);
await tester.pump();
expect(receivedDragDownDetails!.localPosition, const Offset(10, 10));
expect(receivedDragDownDetails!.globalPosition, const Offset(400, 300));
expect(receivedDragStartDetails!.localPosition, const Offset(10, 10));
expect(receivedDragStartDetails!.globalPosition, const Offset(400, 300));
Offset delta = receivedDragUpdateDetails!.delta;
expect(receivedDragEndDetails!.localPosition, const Offset(10, 10) + delta);
expect(receivedDragEndDetails!.globalPosition, const Offset(400, 300) + delta);
// scroll vertically
receivedDragDownDetails = null;
receivedDragStartDetails = null;
receivedDragUpdateDetails = null;
receivedDragEndDetails = null;
expect(node.getSemanticsData().hasAction(SemanticsAction.scrollUp), isTrue);
semanticsOwner.performAction(node.id, SemanticsAction.scrollUp);
await tester.pump();
expect(receivedDragDownDetails!.localPosition, const Offset(10, 10));
expect(receivedDragDownDetails!.globalPosition, const Offset(400, 300));
expect(receivedDragStartDetails!.localPosition, const Offset(10, 10));
expect(receivedDragStartDetails!.globalPosition, const Offset(400, 300));
delta = receivedDragUpdateDetails!.delta;
final Offset local = const Offset(10, 10) + delta;
final RenderObject object = tester.renderObject(find.byKey(key));
final Matrix4 transform = object.getTransformTo(null);
final Offset global = MatrixUtils.transformPoint(transform, local);
expect(receivedDragEndDetails!.localPosition, local);
expect(receivedDragEndDetails!.globalPosition, global);
});
});
group("RawGestureDetectorState's debugFillProperties", () {
testWidgets('when default', (WidgetTester tester) async {
final 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 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 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 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
var didStartPan = false;
Offset? panDelta;
var 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 {
var didStartPan = false;
Offset? panDelta;
var 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 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 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;
}
}