| // 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' show kSecondaryButton, PointerDeviceKind; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'semantics_tester.dart'; |
| |
| void main() { |
| late bool tapped; |
| late bool hovered; |
| late Widget tapTarget; |
| late Widget hoverTarget; |
| |
| setUp(() { |
| tapped = false; |
| tapTarget = GestureDetector( |
| onTap: () { |
| tapped = true; |
| }, |
| child: const SizedBox( |
| width: 10.0, |
| height: 10.0, |
| child: Text('target', textDirection: TextDirection.ltr), |
| ), |
| ); |
| |
| hovered = false; |
| hoverTarget = MouseRegion( |
| onHover: (_) { hovered = true; }, |
| onEnter: (_) { hovered = true; }, |
| onExit: (_) { hovered = true; }, |
| child: const SizedBox( |
| width: 10.0, |
| height: 10.0, |
| child: Text('target', textDirection: TextDirection.ltr), |
| ), |
| ); |
| }); |
| |
| testWidgets('ModalBarrier prevents interactions with widgets behind it', (WidgetTester tester) async { |
| final Widget subject = Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| tapTarget, |
| const ModalBarrier(dismissible: false), |
| ], |
| ); |
| |
| await tester.pumpWidget(subject); |
| await tester.tap(find.text('target'), warnIfMissed: false); |
| await tester.pumpWidget(subject); |
| expect(tapped, isFalse, reason: 'because the tap is not prevented by ModalBarrier'); |
| }); |
| |
| testWidgets('ModalBarrier prevents hover interactions with widgets behind it', (WidgetTester tester) async { |
| final Widget subject = Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| hoverTarget, |
| const ModalBarrier(dismissible: false), |
| ], |
| ); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| // Start out of hoverTarget |
| await gesture.moveTo(const Offset(100, 100)); |
| |
| await tester.pumpWidget(subject); |
| // Move into hoverTarget and tap |
| await gesture.down(const Offset(5, 5)); |
| await tester.pumpWidget(subject); |
| await gesture.up(); |
| await tester.pumpWidget(subject); |
| |
| // Move out |
| await gesture.moveTo(const Offset(100, 100)); |
| await tester.pumpWidget(subject); |
| |
| expect(hovered, isFalse, reason: 'because the hover is not prevented by ModalBarrier'); |
| }); |
| |
| testWidgets('ModalBarrier does not prevent interactions with widgets in front of it', (WidgetTester tester) async { |
| final Widget subject = Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| const ModalBarrier(dismissible: false), |
| tapTarget, |
| ], |
| ); |
| |
| await tester.pumpWidget(subject); |
| await tester.tap(find.text('target')); |
| await tester.pumpWidget(subject); |
| expect(tapped, isTrue, reason: 'because the tap is prevented by ModalBarrier'); |
| }); |
| |
| testWidgets('ModalBarrier does not prevent interactions with translucent widgets in front of it', (WidgetTester tester) async { |
| bool dragged = false; |
| final Widget subject = Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| const ModalBarrier(dismissible: false), |
| GestureDetector( |
| behavior: HitTestBehavior.translucent, |
| onHorizontalDragStart: (_) { |
| dragged = true; |
| }, |
| child: const Center( |
| child: Text('target', textDirection: TextDirection.ltr), |
| ), |
| ), |
| ], |
| ); |
| |
| await tester.pumpWidget(subject); |
| await tester.dragFrom( |
| tester.getBottomRight(find.byType(GestureDetector)) - const Offset(10, 10), |
| const Offset(-20, 0), |
| ); |
| await tester.pumpWidget(subject); |
| expect(dragged, isTrue, reason: 'because the drag is prevented by ModalBarrier'); |
| }); |
| |
| testWidgets('ModalBarrier does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async { |
| final Widget subject = Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| const ModalBarrier(dismissible: false), |
| hoverTarget, |
| ], |
| ); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| // Start out of hoverTarget |
| await gesture.moveTo(const Offset(100, 100)); |
| await tester.pumpWidget(subject); |
| expect(hovered, isFalse); |
| |
| // Move into hoverTarget |
| await gesture.moveTo(const Offset(5, 5)); |
| await tester.pumpWidget(subject); |
| expect(hovered, isTrue, reason: 'because the hover is prevented by ModalBarrier'); |
| hovered = false; |
| |
| // Move out |
| await gesture.moveTo(const Offset(100, 100)); |
| await tester.pumpWidget(subject); |
| expect(hovered, isTrue, reason: 'because the hover is prevented by ModalBarrier'); |
| hovered = false; |
| }); |
| |
| testWidgets('ModalBarrier plays system alert sound when user tries to dismiss it', (WidgetTester tester) async { |
| final List<String> playedSystemSounds = <String>[]; |
| try { |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| if (methodCall.method == 'SystemSound.play') |
| playedSystemSounds.add(methodCall.arguments as String); |
| }); |
| |
| final Widget subject = Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| tapTarget, |
| const ModalBarrier(dismissible: false), |
| ], |
| ); |
| |
| await tester.pumpWidget(subject); |
| await tester.tap(find.text('target'), warnIfMissed: false); |
| await tester.pumpWidget(subject); |
| } finally { |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); |
| } |
| expect(playedSystemSounds, hasLength(1)); |
| expect(playedSystemSounds[0], SystemSoundType.alert.toString()); |
| }); |
| |
| testWidgets('ModalBarrier pops the Navigator when dismissed by primary tap', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const FirstWidget(), |
| '/modal': (BuildContext context) => const SecondWidget(), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| // Initially the barrier is not visible |
| expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); |
| |
| // Tapping on X routes to the barrier |
| await tester.tap(find.text('X')); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| |
| // Press the barrier; it shouldn't dismiss yet |
| final TestGesture gesture = await tester.press( |
| find.byKey(const ValueKey<String>('barrier')), |
| ); |
| await tester.pumpAndSettle(); // begin transition |
| expect(find.byKey(const ValueKey<String>('barrier')), findsOneWidget); |
| |
| // Release the pointer; the barrier should be dismissed |
| await gesture.up(); |
| await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition |
| expect( |
| find.byKey(const ValueKey<String>('barrier')), |
| findsNothing, |
| reason: 'The route should have been dismissed by tapping the barrier.', |
| ); |
| }); |
| |
| testWidgets('ModalBarrier pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const FirstWidget(), |
| '/modal': (BuildContext context) => const SecondWidget(), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| // Initially the barrier is not visible |
| expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); |
| |
| // Tapping on X routes to the barrier |
| await tester.tap(find.text('X')); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| |
| // Press the barrier; it shouldn't dismiss yet |
| final TestGesture gesture = await tester.press( |
| find.byKey(const ValueKey<String>('barrier')), |
| buttons: kSecondaryButton, |
| ); |
| await tester.pumpAndSettle(); // begin transition |
| expect(find.byKey(const ValueKey<String>('barrier')), findsOneWidget); |
| |
| // Release the pointer; the barrier should be dismissed |
| await gesture.up(); |
| await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition |
| expect( |
| find.byKey(const ValueKey<String>('barrier')), |
| findsNothing, |
| reason: 'The route should have been dismissed by tapping the barrier.', |
| ); |
| }); |
| |
| testWidgets('ModalBarrier may pop the Navigator when competing with other gestures', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const FirstWidget(), |
| '/modal': (BuildContext context) => const SecondWidgetWithCompetence(), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| // Initially the barrier is not visible |
| expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); |
| |
| // Tapping on X routes to the barrier |
| await tester.tap(find.text('X')); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| |
| // Tap on the barrier to dismiss it |
| await tester.tap(find.byKey(const ValueKey<String>('barrier'))); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| |
| expect( |
| find.byKey(const ValueKey<String>('barrier')), |
| findsNothing, |
| reason: 'The route should have been dismissed by tapping the barrier.', |
| ); |
| }); |
| |
| testWidgets('ModalBarrier does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async { |
| bool willPopCalled = false; |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const FirstWidget(), |
| '/modal': (BuildContext context) => Stack( |
| children: <Widget>[ |
| const SecondWidget(), |
| WillPopScope( |
| child: const SizedBox(), |
| onWillPop: () async { |
| willPopCalled = true; |
| return false; |
| }, |
| ), |
| ], |
| ), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| // Initially the barrier is not visible |
| expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); |
| |
| // Tapping on X routes to the barrier |
| await tester.tap(find.text('X')); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| |
| expect(willPopCalled, isFalse); |
| |
| // Tap on the barrier to attempt to dismiss it |
| await tester.tap(find.byKey(const ValueKey<String>('barrier'))); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| |
| expect( |
| find.byKey(const ValueKey<String>('barrier')), |
| findsOneWidget, |
| reason: 'The route should still be present if the pop is vetoed.', |
| ); |
| |
| expect(willPopCalled, isTrue); |
| }); |
| |
| testWidgets('ModalBarrier pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async { |
| bool willPopCalled = false; |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const FirstWidget(), |
| '/modal': (BuildContext context) => Stack( |
| children: <Widget>[ |
| const SecondWidget(), |
| WillPopScope( |
| child: const SizedBox(), |
| onWillPop: () async { |
| willPopCalled = true; |
| return true; |
| }, |
| ), |
| ], |
| ), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| // Initially the barrier is not visible |
| expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); |
| |
| // Tapping on X routes to the barrier |
| await tester.tap(find.text('X')); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| |
| expect(willPopCalled, isFalse); |
| |
| // Tap on the barrier to attempt to dismiss it |
| await tester.tap(find.byKey(const ValueKey<String>('barrier'))); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| |
| expect( |
| find.byKey(const ValueKey<String>('barrier')), |
| findsNothing, |
| reason: 'The route should not be present if the pop is permitted.', |
| ); |
| |
| expect(willPopCalled, isTrue); |
| }); |
| |
| testWidgets('ModalBarrier will call onDismiss callback', (WidgetTester tester) async { |
| bool dismissCallbackCalled = false; |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const FirstWidget(), |
| '/modal': (BuildContext context) => SecondWidget(onDismiss: () { |
| dismissCallbackCalled = true; |
| }), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| // Initially the barrier is not visible |
| expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); |
| |
| // Tapping on X routes to the barrier |
| await tester.tap(find.text('X')); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| expect(find.byKey(const ValueKey<String>('barrier')), findsOneWidget); |
| expect(dismissCallbackCalled, false); |
| |
| // Tap on the barrier |
| await tester.tap(find.byKey(const ValueKey<String>('barrier'))); |
| await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition |
| expect(dismissCallbackCalled, true); |
| }); |
| |
| testWidgets('ModalBarrier will not pop when given an onDismiss callback', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const FirstWidget(), |
| '/modal': (BuildContext context) => SecondWidget(onDismiss: () {}), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| // Initially the barrier is not visible |
| expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); |
| |
| // Tapping on X routes to the barrier |
| await tester.tap(find.text('X')); |
| await tester.pump(); // begin transition |
| await tester.pump(const Duration(seconds: 1)); // end transition |
| expect(find.byKey(const ValueKey<String>('barrier')), findsOneWidget); |
| |
| // Tap on the barrier |
| await tester.tap(find.byKey(const ValueKey<String>('barrier'))); |
| await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition |
| expect( |
| find.byKey(const ValueKey<String>('barrier')), |
| findsOneWidget, |
| reason: 'The route should not have been dismissed by tapping the barrier, as there was a onDismiss callback given.', |
| ); |
| }); |
| |
| testWidgets('Undismissible ModalBarrier hidden in semantic tree', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget(const ModalBarrier(dismissible: false)); |
| |
| final TestSemantics expectedSemantics = TestSemantics.root(); |
| expect(semantics, hasSemantics(expectedSemantics)); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Dismissible ModalBarrier includes button in semantic tree on iOS', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget(const Directionality( |
| textDirection: TextDirection.ltr, |
| child: ModalBarrier( |
| semanticsLabel: 'Dismiss', |
| ), |
| )); |
| |
| final TestSemantics expectedSemantics = TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| rect: TestSemantics.fullScreen, |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss], |
| label: 'Dismiss', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ); |
| expect(semantics, hasSemantics(expectedSemantics, ignoreId: true)); |
| |
| semantics.dispose(); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('Dismissible ModalBarrier is hidden on Android (back button is used to dismiss)', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget(const ModalBarrier()); |
| |
| final TestSemantics expectedSemantics = TestSemantics.root(); |
| expect(semantics, hasSemantics(expectedSemantics)); |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('ModalBarrier uses default mouse cursor', (WidgetTester tester) async { |
| await tester.pumpWidget(Stack( |
| textDirection: TextDirection.ltr, |
| children: const <Widget>[ |
| MouseRegion(cursor: SystemMouseCursors.click), |
| ModalBarrier(dismissible: false), |
| ], |
| )); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); |
| await gesture.addPointer(location: tester.getCenter(find.byType(ModalBarrier))); |
| addTearDown(gesture.removePointer); |
| |
| await tester.pump(); |
| |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| }); |
| } |
| |
| class FirstWidget extends StatelessWidget { |
| const FirstWidget({ Key? key }) : super(key: key); |
| @override |
| Widget build(BuildContext context) { |
| return GestureDetector( |
| onTap: () { |
| Navigator.pushNamed(context, '/modal'); |
| }, |
| child: const Text('X'), |
| ); |
| } |
| } |
| |
| class SecondWidget extends StatelessWidget { |
| const SecondWidget({ Key? key, this.onDismiss }) : super(key: key); |
| |
| final VoidCallback? onDismiss; |
| |
| @override |
| Widget build(BuildContext context) { |
| return ModalBarrier( |
| key: const ValueKey<String>('barrier'), |
| onDismiss: onDismiss, |
| ); |
| } |
| } |
| |
| class SecondWidgetWithCompetence extends StatelessWidget { |
| const SecondWidgetWithCompetence({ Key? key }) : super(key: key); |
| @override |
| Widget build(BuildContext context) { |
| return Stack( |
| children: <Widget>[ |
| const ModalBarrier( |
| key: ValueKey<String>('barrier'), |
| ), |
| GestureDetector( |
| onVerticalDragStart: (_) {}, |
| behavior: HitTestBehavior.translucent, |
| child: Container(), |
| ), |
| ], |
| ); |
| } |
| } |