// 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({ required this.performLayout, }); 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; } }