blob: adb772147ecc4f5b6f1d18b545c61fd48b744684 [file] [log] [blame]
// 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_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.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; },
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 == null ? details.delta : panDelta + 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('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),
),
),
Container(
width: 100.0,
height: 100.0,
child: GestureDetector(
onTap: () {
didTap = true;
},
behavior: behavior,
),
),
],
),
),
);
}
didReceivePointerDown = false;
didTap = false;
await pumpWidgetTree(null);
await tester.tapAt(const Offset(10.0, 10.0));
expect(didReceivePointerDown, isTrue);
expect(didTap, isTrue);
didReceivePointerDown = false;
didTap = false;
await pumpWidgetTree(HitTestBehavior.deferToChild);
await tester.tapAt(const Offset(10.0, 10.0));
expect(didReceivePointerDown, isTrue);
expect(didTap, isFalse);
didReceivePointerDown = false;
didTap = false;
await pumpWidgetTree(HitTestBehavior.opaque);
await tester.tapAt(const Offset(10.0, 10.0));
expect(didReceivePointerDown, isFalse);
expect(didTap, isTrue);
didReceivePointerDown = false;
didTap = false;
await pumpWidgetTree(HitTestBehavior.translucent);
await tester.tapAt(const Offset(10.0, 10.0));
expect(didReceivePointerDown, isTrue);
expect(didTap, isTrue);
});
testWidgets('Empty', (WidgetTester tester) async {
bool didTap = false;
await tester.pumpWidget(
Center(
child: GestureDetector(
onTap: () {
didTap = true;
},
),
),
);
expect(didTap, isFalse);
await tester.tapAt(const Offset(10.0, 10.0));
expect(didTap, isTrue);
});
testWidgets('Only container', (WidgetTester tester) async {
bool didTap = false;
await tester.pumpWidget(
Center(
child: GestureDetector(
onTap: () {
didTap = true;
},
child: Container(),
),
),
);
expect(didTap, isFalse);
await tester.tapAt(const Offset(10.0, 10.0));
expect(didTap, isFalse);
});
testWidgets('cache render object', (WidgetTester tester) async {
final GestureTapCallback inputCallback = () { };
await tester.pumpWidget(
Center(
child: GestureDetector(
onTap: inputCallback,
child: Container(),
),
),
);
final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector));
await tester.pumpWidget(
Center(
child: GestureDetector(
onTap: inputCallback,
child: Container(),
),
),
);
final RenderSemanticsGestureHandler renderObj2 = tester.renderObject(find.byType(GestureDetector));
expect(renderObj1, same(renderObj2));
});
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: GestureDetector(
onTapDown: (TapDownDetails details) {
tapDown += 1;
},
onTap: () {
tap += 1;
},
onTapCancel: () {
tapCancel += 1;
},
onLongPress: () {
longPress += 1;
},
),
),
),
);
// 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));
// 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);
});
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: GestureDetector(
onLongPressUp: () {
longPressUp += 1;
},
),
),
),
);
Future<void> longPress(Duration timeout) async {
final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0));
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);
});
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,
),
),
),
);
const int pointerValue = 1;
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
forcePressOffset,
const PointerDownEvent(
pointer: pointerValue,
position: forcePressOffset,
pressure: 0.0,
pressureMax: 6.0,
pressureMin: 0.0,
),
);
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.3, pressureMin: 0, pressureMax: 1));
expect(forcePressStart, 0);
expect(forcePressPeaked, 0);
expect(forcePressUpdate, 0);
expect(forcePressEnded, 0);
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.5, pressureMin: 0, pressureMax: 1));
expect(forcePressStart, 1);
expect(forcePressPeaked, 0);
expect(forcePressUpdate, 1);
expect(forcePressEnded, 0);
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.6, pressureMin: 0, pressureMax: 1));
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.7, pressureMin: 0, pressureMax: 1));
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.2, pressureMin: 0, pressureMax: 1));
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.3, pressureMin: 0, pressureMax: 1));
expect(forcePressStart, 1);
expect(forcePressPeaked, 0);
expect(forcePressUpdate, 5);
expect(forcePressEnded, 0);
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.9, pressureMin: 0, pressureMax: 1));
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,
),
),
),
);
const int pointerValue = 1;
const double maxPressure = 6.0;
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
forcePressOffset,
const PointerDownEvent(
pointer: pointerValue,
position: forcePressOffset,
pressure: 0.0,
pressureMax: maxPressure,
pressureMin: 0.0,
),
);
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: 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(const PointerMoveEvent(pointer: pointerValue, position: 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,
),
),
),
);
const int pointerValue = 1;
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
forcePressOffset,
const PointerDownEvent(
pointer: pointerValue,
position: forcePressOffset,
pressure: 0.0,
pressureMax: 6.0,
pressureMin: 0.0,
),
);
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.3, pressureMin: 0, pressureMax: 1));
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(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.5, pressureMin: 0, pressureMax: 1));
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 = () {};
},
),
},
child: Container(),
semantics: _EmptySemanticsGestureDelegate(),
));
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,
gestures: const <Type, GestureRecognizerFactory>{},
child: Container(),
semantics: _EmptySemanticsGestureDelegate(),
excludeFromSemantics: true,
));
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', () {
FlutterError error;
try {
GestureDetector(onScaleStart: (_) {}, onPanStart: (_) {},);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
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('constructur duplicate drag recognizer', () {
FlutterError error;
try {
GestureDetector(
onVerticalDragStart: (_) {},
onHorizontalDragStart: (_) {},
onPanStart: (_) {},
);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
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: Container(
child: const Text('Text'),
),
),
),
);
FlutterError error;
try {
key.currentState.replaceGestureRecognizers(
<Type, GestureRecognizerFactory>{});
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
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',
);
}
});
});
});
}
class _EmptySemanticsGestureDelegate extends SemanticsGestureDelegate {
@override
void assignSemantics(RenderSemanticsGestureHandler renderObject) {
}
}