blob: 389e2a9b9323c515e426c03638aced02786b224c [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/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('Vertical gesture detector has up/down actions', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
int callCount = 0;
final GlobalKey detectorKey = GlobalKey();
await tester.pumpWidget(
Center(
child: GestureDetector(
key: detectorKey,
onVerticalDragStart: (DragStartDetails _) {
callCount += 1;
},
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
));
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollLeft);
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollRight);
expect(callCount, 0);
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollUp);
expect(callCount, 1);
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollDown);
expect(callCount, 2);
semantics.dispose();
});
testWidgets('Horizontal gesture detector has up/down actions', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
int callCount = 0;
final GlobalKey detectorKey = GlobalKey();
await tester.pumpWidget(
Center(
child: GestureDetector(
key: detectorKey,
onHorizontalDragStart: (DragStartDetails _) {
callCount += 1;
},
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight],
));
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollUp);
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollDown);
expect(callCount, 0);
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollLeft);
expect(callCount, 1);
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollRight);
expect(callCount, 2);
semantics.dispose();
});
testWidgets('All registered handlers for the gesture kind are called', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final Set<String> logs = <String>{};
final GlobalKey detectorKey = GlobalKey();
await tester.pumpWidget(
Center(
child: GestureDetector(
key: detectorKey,
onHorizontalDragStart: (_) { logs.add('horizontal'); },
onPanStart: (_) { logs.add('pan'); },
child: Container(),
),
),
);
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollLeft);
expect(logs, <String>{'horizontal', 'pan'});
semantics.dispose();
});
testWidgets('Replacing recognizers should update semantic handlers', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
// How the test is set up:
//
// * In the base state, RawGestureDetector's recognizer is a HorizontalGR
// * Calling `introduceLayoutPerformer()` adds a `_TestLayoutPerformer` as
// child of RawGestureDetector, which invokes a given callback during
// layout phase.
// * The aforementioned callback replaces the detector's recognizer with a
// TapGR.
// * This test makes sure the replacement correctly updates semantics.
final Set<String> logs = <String>{};
final GlobalKey<RawGestureDetectorState> detectorKey = GlobalKey();
void performLayout() {
detectorKey.currentState!.replaceGestureRecognizers(<Type, GestureRecognizerFactory>{
TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(TapGestureRecognizer instance) {
instance.onTap = () { logs.add('tap'); };
},
),
});
}
bool hasLayoutPerformer = false;
late VoidCallback introduceLayoutPerformer;
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
introduceLayoutPerformer = () {
setter(() {
hasLayoutPerformer = true;
});
};
return Center(
child: RawGestureDetector(
key: detectorKey,
gestures: <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(),
(HorizontalDragGestureRecognizer instance) {
instance.onStart = (_) { logs.add('horizontal'); };
},
),
},
child: hasLayoutPerformer ? _TestLayoutPerformer(performLayout: performLayout) : null,
),
);
},
),
);
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollLeft);
expect(logs, <String>{'horizontal'});
logs.clear();
introduceLayoutPerformer();
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollLeft);
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.tap);
expect(logs, <String>{'tap'});
logs.clear();
semantics.dispose();
});
group("RawGestureDetector's custom semantics delegate", () {
testWidgets('should update semantics notations when switching from the default delegate', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final Map<Type, GestureRecognizerFactory> gestures =
_buildGestureMap(() => LongPressGestureRecognizer(), null)
..addAll( _buildGestureMap(() => TapGestureRecognizer(), null));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: gestures,
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.longPress, SemanticsAction.tap],
));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: gestures,
semantics: _TestSemanticsGestureDelegate(onTap: () {}),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
));
semantics.dispose();
});
testWidgets('should update semantics notations when switching to the default delegate', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final Map<Type, GestureRecognizerFactory> gestures =
_buildGestureMap(() => LongPressGestureRecognizer(), null)
..addAll( _buildGestureMap(() => TapGestureRecognizer(), null));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: gestures,
semantics: _TestSemanticsGestureDelegate(onTap: () {}),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: gestures,
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.longPress, SemanticsAction.tap],
));
semantics.dispose();
});
testWidgets('should update semantics notations when switching from a different custom delegate', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final Map<Type, GestureRecognizerFactory> gestures =
_buildGestureMap(() => LongPressGestureRecognizer(), null)
..addAll( _buildGestureMap(() => TapGestureRecognizer(), null));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: gestures,
semantics: _TestSemanticsGestureDelegate(onTap: () {}),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: gestures,
semantics: _TestSemanticsGestureDelegate(onLongPress: () {}),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.longPress],
));
semantics.dispose();
});
testWidgets('should correctly call callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final List<String> logs = <String>[];
final GlobalKey<RawGestureDetectorState> detectorKey = GlobalKey();
await tester.pumpWidget(
Center(
child: RawGestureDetector(
key: detectorKey,
semantics: _TestSemanticsGestureDelegate(
onTap: () { logs.add('tap'); },
onLongPress: () { logs.add('longPress'); },
onHorizontalDragUpdate: (_) { logs.add('horizontal'); },
onVerticalDragUpdate: (_) { logs.add('vertical'); },
),
child: Container(),
),
),
);
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.tap);
expect(logs, <String>['tap']);
logs.clear();
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.longPress);
expect(logs, <String>['longPress']);
logs.clear();
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollLeft);
expect(logs, <String>['horizontal']);
logs.clear();
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollUp);
expect(logs, <String>['vertical']);
logs.clear();
semantics.dispose();
});
});
group("RawGestureDetector's default semantics delegate", () {
group('should map onTap to', () {
testWidgets('null when there is no TapGR', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(null, null),
child: Container(),
),
),
);
expect(semantics, isNot(includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
)));
semantics.dispose();
});
testWidgets('non-null when there is TapGR with no callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(
() => TapGestureRecognizer(),
null,
),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
));
semantics.dispose();
});
testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey detectorKey = GlobalKey();
final List<String> logs = <String>[];
await tester.pumpWidget(
Center(
child: RawGestureDetector(
key: detectorKey,
gestures: _buildGestureMap(
() => TapGestureRecognizer(),
(TapGestureRecognizer tap) {
tap
..onTap = () {logs.add('tap');}
..onTapUp = (_) {logs.add('tapUp');}
..onTapDown = (_) {logs.add('tapDown');}
..onTapCancel = () {logs.add('WRONG');}
..onSecondaryTapDown = (_) {logs.add('WRONG');}
..onTertiaryTapDown = (_) {logs.add('WRONG');};
},
),
child: Container(),
),
),
);
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.tap);
expect(logs, <String>['tapDown', 'tapUp', 'tap']);
semantics.dispose();
});
});
group('should map onLongPress to', () {
testWidgets('null when there is no LongPressGR ', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(null, null),
child: Container(),
),
),
);
expect(semantics, isNot(includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.longPress],
)));
semantics.dispose();
});
testWidgets('non-null when there is LongPressGR with no callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(
() => LongPressGestureRecognizer(),
null,
),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.longPress],
));
semantics.dispose();
});
testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey detectorKey = GlobalKey();
final List<String> logs = <String>[];
await tester.pumpWidget(
Center(
child: RawGestureDetector(
key: detectorKey,
gestures: _buildGestureMap(
() => LongPressGestureRecognizer(),
(LongPressGestureRecognizer longPress) {
longPress
..onLongPress = () {logs.add('LP');}
..onLongPressStart = (_) {logs.add('LPStart');}
..onLongPressUp = () {logs.add('LPUp');}
..onLongPressEnd = (_) {logs.add('LPEnd');}
..onLongPressMoveUpdate = (_) {logs.add('WRONG');};
},
),
child: Container(),
),
),
);
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.longPress);
expect(logs, <String>['LPStart', 'LP', 'LPEnd', 'LPUp']);
semantics.dispose();
});
});
group('should map onHorizontalDragUpdate to', () {
testWidgets('null when there is no matching recognizers ', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(null, null),
child: Container(),
),
),
);
expect(semantics, isNot(includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight],
)));
semantics.dispose();
});
testWidgets('non-null when there is either matching recognizer with no callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(
() => HorizontalDragGestureRecognizer(),
null,
),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight],
));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(
() => PanGestureRecognizer(),
null,
),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[
SemanticsAction.scrollLeft,
SemanticsAction.scrollRight,
SemanticsAction.scrollDown,
SemanticsAction.scrollUp,
],
));
semantics.dispose();
});
testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey detectorKey = GlobalKey();
final List<String> logs = <String>[];
final Map<Type, GestureRecognizerFactory> gestures = _buildGestureMap(
() => HorizontalDragGestureRecognizer(),
(HorizontalDragGestureRecognizer horizontal) {
horizontal
..onStart = (_) {logs.add('HStart');}
..onDown = (_) {logs.add('HDown');}
..onEnd = (_) {logs.add('HEnd');}
..onUpdate = (_) {logs.add('HUpdate');}
..onCancel = () {logs.add('WRONG');};
},
)..addAll(_buildGestureMap(
() => PanGestureRecognizer(),
(PanGestureRecognizer pan) {
pan
..onStart = (_) {logs.add('PStart');}
..onDown = (_) {logs.add('PDown');}
..onEnd = (_) {logs.add('PEnd');}
..onUpdate = (_) {logs.add('PUpdate');}
..onCancel = () {logs.add('WRONG');};
},
));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
key: detectorKey,
gestures: gestures,
child: Container(),
),
),
);
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollLeft);
expect(logs, <String>['HDown', 'HStart', 'HUpdate', 'HEnd',
'PDown', 'PStart', 'PUpdate', 'PEnd',]);
logs.clear();
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollLeft);
expect(logs, <String>['HDown', 'HStart', 'HUpdate', 'HEnd',
'PDown', 'PStart', 'PUpdate', 'PEnd',]);
semantics.dispose();
});
});
group('should map onVerticalDragUpdate to', () {
testWidgets('null when there is no matching recognizers ', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(null, null),
child: Container(),
),
),
);
expect(semantics, isNot(includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
)));
semantics.dispose();
});
testWidgets('non-null when there is either matching recognizer with no callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(
() => VerticalDragGestureRecognizer(),
null,
),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
));
// Pan has bene tested in Horizontal
semantics.dispose();
});
testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey detectorKey = GlobalKey();
final List<String> logs = <String>[];
final Map<Type, GestureRecognizerFactory> gestures = _buildGestureMap(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer horizontal) {
horizontal
..onStart = (_) {logs.add('VStart');}
..onDown = (_) {logs.add('VDown');}
..onEnd = (_) {logs.add('VEnd');}
..onUpdate = (_) {logs.add('VUpdate');}
..onCancel = () {logs.add('WRONG');};
},
)..addAll(_buildGestureMap(
() => PanGestureRecognizer(),
(PanGestureRecognizer pan) {
pan
..onStart = (_) {logs.add('PStart');}
..onDown = (_) {logs.add('PDown');}
..onEnd = (_) {logs.add('PEnd');}
..onUpdate = (_) {logs.add('PUpdate');}
..onCancel = () {logs.add('WRONG');};
},
));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
key: detectorKey,
gestures: gestures,
child: Container(),
),
),
);
final int detectorId = detectorKey.currentContext!.findRenderObject()!.debugSemantics!.id;
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollUp);
expect(logs, <String>['VDown', 'VStart', 'VUpdate', 'VEnd',
'PDown', 'PStart', 'PUpdate', 'PEnd',]);
logs.clear();
tester.binding.pipelineOwner.semanticsOwner!.performAction(detectorId, SemanticsAction.scrollDown);
expect(logs, <String>['VDown', 'VStart', 'VUpdate', 'VEnd',
'PDown', 'PStart', 'PUpdate', 'PEnd',]);
semantics.dispose();
});
});
testWidgets('should update semantics notations when receiving new gestures', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(() => LongPressGestureRecognizer(), null),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.longPress],
));
await tester.pumpWidget(
Center(
child: RawGestureDetector(
gestures: _buildGestureMap(() => TapGestureRecognizer(), null),
child: Container(),
),
),
);
expect(semantics, includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
));
semantics.dispose();
});
});
}
class _TestLayoutPerformer extends SingleChildRenderObjectWidget {
const _TestLayoutPerformer({
Key? key,
required this.performLayout,
}) : super(key: key);
final VoidCallback performLayout;
@override
_RenderTestLayoutPerformer createRenderObject(BuildContext context) {
return _RenderTestLayoutPerformer(performLayout: performLayout);
}
}
class _RenderTestLayoutPerformer extends RenderBox {
_RenderTestLayoutPerformer({required VoidCallback performLayout}) : _performLayout = performLayout;
final VoidCallback _performLayout;
@override
Size computeDryLayout(BoxConstraints constraints) {
return const Size(1, 1);
}
@override
void performLayout() {
size = const Size(1, 1);
if (_performLayout != null)
_performLayout();
}
}
Map<Type, GestureRecognizerFactory> _buildGestureMap<T extends GestureRecognizer>(
GestureRecognizerFactoryConstructor<T>? constructor,
GestureRecognizerFactoryInitializer<T>? initializer,
) {
if (constructor == null)
return <Type, GestureRecognizerFactory>{};
return <Type, GestureRecognizerFactory>{
T: GestureRecognizerFactoryWithHandlers<T>(
constructor,
initializer ?? (T o) {},
),
};
}
class _TestSemanticsGestureDelegate extends SemanticsGestureDelegate {
const _TestSemanticsGestureDelegate({
this.onTap,
this.onLongPress,
this.onHorizontalDragUpdate,
this.onVerticalDragUpdate,
});
final GestureTapCallback? onTap;
final GestureLongPressCallback? onLongPress;
final GestureDragUpdateCallback? onHorizontalDragUpdate;
final GestureDragUpdateCallback? onVerticalDragUpdate;
@override
void assignSemantics(RenderSemanticsGestureHandler renderObject) {
renderObject
..onTap = onTap
..onLongPress = onLongPress
..onHorizontalDragUpdate = onHorizontalDragUpdate
..onVerticalDragUpdate = onVerticalDragUpdate;
}
}