Add onDismiss to AnimatedModalBarrier, update tests (#100162)

I noticed that AnimatedModalBarrier didn't have the onDismiss callback that ModalBarrier does, and so I added that, and while I was at it, I updated the unit tests to perform all of the tests that are done on ModalBarrier also on AnimatedModalBarrier. The tests are unchanged, other than using AnimatedModalBarrier instead.
This commit is contained in:
Greg Spencer 2022-04-06 18:39:14 -07:00 committed by GitHub
parent 548e8853e6
commit db51873250
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 874 additions and 394 deletions

View File

@ -60,6 +60,7 @@ class ModalBarrier extends StatelessWidget {
/// [ModalBarrier] built by [ModalRoute] pages. /// [ModalBarrier] built by [ModalRoute] pages.
final bool dismissible; final bool dismissible;
/// {@template flutter.widgets.ModalBarrier.onDismiss}
/// Called when the barrier is being dismissed. /// Called when the barrier is being dismissed.
/// ///
/// If non-null [onDismiss] will be called in place of popping the current /// If non-null [onDismiss] will be called in place of popping the current
@ -68,6 +69,7 @@ class ModalBarrier extends StatelessWidget {
/// If null, the ambient [Navigator]'s current route will be popped. /// If null, the ambient [Navigator]'s current route will be popped.
/// ///
/// This field is ignored if [dismissible] is false. /// This field is ignored if [dismissible] is false.
/// {@endtemplate}
final VoidCallback? onDismiss; final VoidCallback? onDismiss;
/// Whether the modal barrier semantics are included in the semantics tree. /// Whether the modal barrier semantics are included in the semantics tree.
@ -172,6 +174,7 @@ class AnimatedModalBarrier extends AnimatedWidget {
this.dismissible = true, this.dismissible = true,
this.semanticsLabel, this.semanticsLabel,
this.barrierSemanticsDismissible, this.barrierSemanticsDismissible,
this.onDismiss,
}) : super(key: key, listenable: color); }) : super(key: key, listenable: color);
/// If non-null, fill the barrier with this color. /// If non-null, fill the barrier with this color.
@ -208,6 +211,9 @@ class AnimatedModalBarrier extends AnimatedWidget {
/// the [ModalBarrier] built by [ModalRoute] pages. /// the [ModalBarrier] built by [ModalRoute] pages.
final bool? barrierSemanticsDismissible; final bool? barrierSemanticsDismissible;
/// {@macro flutter.widgets.ModalBarrier.onDismiss}
final VoidCallback? onDismiss;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ModalBarrier( return ModalBarrier(
@ -215,6 +221,7 @@ class AnimatedModalBarrier extends AnimatedWidget {
dismissible: dismissible, dismissible: dismissible,
semanticsLabel: semanticsLabel, semanticsLabel: semanticsLabel,
barrierSemanticsDismissible: barrierSemanticsDismissible, barrierSemanticsDismissible: barrierSemanticsDismissible,
onDismiss: onDismiss,
); );
} }
} }

View File

@ -15,9 +15,11 @@ void main() {
late bool hovered; late bool hovered;
late Widget tapTarget; late Widget tapTarget;
late Widget hoverTarget; late Widget hoverTarget;
late Animation<Color?> colorAnimation;
setUp(() { setUp(() {
tapped = false; tapped = false;
colorAnimation = const AlwaysStoppedAnimation<Color?>(Colors.red);
tapTarget = GestureDetector( tapTarget = GestureDetector(
onTap: () { onTap: () {
tapped = true; tapped = true;
@ -42,7 +44,8 @@ void main() {
); );
}); });
testWidgets('ModalBarrier prevents interactions with widgets behind it', (WidgetTester tester) async { group('ModalBarrier', () {
testWidgets('prevents interactions with widgets behind it', (WidgetTester tester) async {
final Widget subject = Stack( final Widget subject = Stack(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: <Widget>[ children: <Widget>[
@ -57,7 +60,7 @@ void main() {
expect(tapped, isFalse, reason: 'because the tap is not prevented by ModalBarrier'); expect(tapped, isFalse, reason: 'because the tap is not prevented by ModalBarrier');
}); });
testWidgets('ModalBarrier prevents hover interactions with widgets behind it', (WidgetTester tester) async { testWidgets('prevents hover interactions with widgets behind it', (WidgetTester tester) async {
final Widget subject = Stack( final Widget subject = Stack(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: <Widget>[ children: <Widget>[
@ -85,7 +88,7 @@ void main() {
expect(hovered, isFalse, reason: 'because the hover is not prevented by ModalBarrier'); 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 { testWidgets('does not prevent interactions with widgets in front of it', (WidgetTester tester) async {
final Widget subject = Stack( final Widget subject = Stack(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: <Widget>[ children: <Widget>[
@ -100,7 +103,7 @@ void main() {
expect(tapped, isTrue, reason: 'because the tap is prevented by ModalBarrier'); 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 { testWidgets('does not prevent interactions with translucent widgets in front of it', (WidgetTester tester) async {
bool dragged = false; bool dragged = false;
final Widget subject = Stack( final Widget subject = Stack(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
@ -127,7 +130,7 @@ void main() {
expect(dragged, isTrue, reason: 'because the drag is prevented by ModalBarrier'); 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 { testWidgets('does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async {
final Widget subject = Stack( final Widget subject = Stack(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: <Widget>[ children: <Widget>[
@ -156,10 +159,11 @@ void main() {
hovered = false; hovered = false;
}); });
testWidgets('ModalBarrier plays system alert sound when user tries to dismiss it', (WidgetTester tester) async { testWidgets('plays system alert sound when user tries to dismiss it', (WidgetTester tester) async {
final List<String> playedSystemSounds = <String>[]; final List<String> playedSystemSounds = <String>[];
try { try {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'SystemSound.play') { if (methodCall.method == 'SystemSound.play') {
playedSystemSounds.add(methodCall.arguments as String); playedSystemSounds.add(methodCall.arguments as String);
} }
@ -184,7 +188,7 @@ void main() {
expect(playedSystemSounds[0], SystemSoundType.alert.toString()); expect(playedSystemSounds[0], SystemSoundType.alert.toString());
}); });
testWidgets('ModalBarrier pops the Navigator when dismissed by primary tap', (WidgetTester tester) async { testWidgets('pops the Navigator when dismissed by primary tap', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(), '/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) => const SecondWidget(), '/modal': (BuildContext context) => const SecondWidget(),
@ -217,7 +221,7 @@ void main() {
); );
}); });
testWidgets('ModalBarrier pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async { testWidgets('pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(), '/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) => const SecondWidget(), '/modal': (BuildContext context) => const SecondWidget(),
@ -251,7 +255,7 @@ void main() {
); );
}); });
testWidgets('ModalBarrier may pop the Navigator when competing with other gestures', (WidgetTester tester) async { testWidgets('may pop the Navigator when competing with other gestures', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(), '/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) => const SecondWidgetWithCompetence(), '/modal': (BuildContext context) => const SecondWidgetWithCompetence(),
@ -279,11 +283,12 @@ void main() {
); );
}); });
testWidgets('ModalBarrier does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async { testWidgets('does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async {
bool willPopCalled = false; bool willPopCalled = false;
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(), '/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) => Stack( '/modal': (BuildContext context) =>
Stack(
children: <Widget>[ children: <Widget>[
const SecondWidget(), const SecondWidget(),
WillPopScope( WillPopScope(
@ -323,11 +328,12 @@ void main() {
expect(willPopCalled, isTrue); expect(willPopCalled, isTrue);
}); });
testWidgets('ModalBarrier pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async { testWidgets('pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async {
bool willPopCalled = false; bool willPopCalled = false;
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(), '/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) => Stack( '/modal': (BuildContext context) =>
Stack(
children: <Widget>[ children: <Widget>[
const SecondWidget(), const SecondWidget(),
WillPopScope( WillPopScope(
@ -367,11 +373,12 @@ void main() {
expect(willPopCalled, isTrue); expect(willPopCalled, isTrue);
}); });
testWidgets('ModalBarrier will call onDismiss callback', (WidgetTester tester) async { testWidgets('will call onDismiss callback', (WidgetTester tester) async {
bool dismissCallbackCalled = false; bool dismissCallbackCalled = false;
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(), '/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) => SecondWidget(onDismiss: () { '/modal': (BuildContext context) =>
SecondWidget(onDismiss: () {
dismissCallbackCalled = true; dismissCallbackCalled = true;
}), }),
}; };
@ -394,7 +401,7 @@ void main() {
expect(dismissCallbackCalled, true); expect(dismissCallbackCalled, true);
}); });
testWidgets('ModalBarrier will not pop when given an onDismiss callback', (WidgetTester tester) async { testWidgets('will not pop when given an onDismiss callback', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(), '/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) => SecondWidget(onDismiss: () {}), '/modal': (BuildContext context) => SecondWidget(onDismiss: () {}),
@ -455,7 +462,8 @@ void main() {
semantics.dispose(); semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS})); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS}));
testWidgets('Dismissible ModalBarrier is hidden on Android (back button is used to dismiss)', (WidgetTester tester) async { testWidgets(
'Dismissible ModalBarrier is hidden on Android (back button is used to dismiss)', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(const ModalBarrier()); await tester.pumpWidget(const ModalBarrier());
@ -464,8 +472,439 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
});
group('AnimatedModalBarrier', () {
testWidgets('prevents interactions with widgets behind it', (WidgetTester tester) async {
final Widget subject = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
tapTarget,
AnimatedModalBarrier(dismissible: false, color: colorAnimation),
],
);
testWidgets('ModalBarrier uses default mouse cursor', (WidgetTester tester) async { 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('prevents hover interactions with widgets behind it', (WidgetTester tester) async {
final Widget subject = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
hoverTarget,
AnimatedModalBarrier(dismissible: false, color: colorAnimation),
],
);
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 AnimatedModalBarrier');
});
testWidgets('does not prevent interactions with widgets in front of it', (WidgetTester tester) async {
final Widget subject = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
AnimatedModalBarrier(dismissible: false, color: colorAnimation),
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 AnimatedModalBarrier');
});
testWidgets('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>[
AnimatedModalBarrier(dismissible: false, color: colorAnimation),
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 AnimatedModalBarrier');
});
testWidgets('does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async {
final Widget subject = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
AnimatedModalBarrier(dismissible: false, color: colorAnimation),
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 AnimatedModalBarrier');
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 AnimatedModalBarrier');
hovered = false;
});
testWidgets('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);
}
return null;
});
final Widget subject = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
tapTarget,
AnimatedModalBarrier(dismissible: false, color: colorAnimation),
],
);
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('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 AnimatedSecondWidget(),
};
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('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 AnimatedSecondWidget(),
};
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('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 AnimatedSecondWidgetWithCompetence(),
};
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('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 AnimatedSecondWidget(),
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('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 AnimatedSecondWidget(),
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('will call onDismiss callback', (WidgetTester tester) async {
bool dismissCallbackCalled = false;
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) =>
AnimatedSecondWidget(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('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) => AnimatedSecondWidget(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 AnimatedModalBarrier hidden in semantic tree', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(AnimatedModalBarrier(dismissible: false, color: colorAnimation));
final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
});
testWidgets('Dismissible AnimatedModalBarrier includes button in semantic tree on iOS', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: AnimatedModalBarrier(
semanticsLabel: 'Dismiss',
color: colorAnimation,
),
));
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 AnimatedModalBarrier is hidden on Android (back button is used to dismiss)', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(AnimatedModalBarrier(color: colorAnimation));
final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
});
});
testWidgets('uses default mouse cursor', (WidgetTester tester) async {
await tester.pumpWidget(Stack( await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: const <Widget>[ children: const <Widget>[
@ -511,6 +950,21 @@ class SecondWidget extends StatelessWidget {
} }
} }
class AnimatedSecondWidget extends StatelessWidget {
const AnimatedSecondWidget({Key? key, this.onDismiss}) : super(key: key);
final VoidCallback? onDismiss;
@override
Widget build(BuildContext context) {
return AnimatedModalBarrier(
key: const ValueKey<String>('barrier'),
color: const AlwaysStoppedAnimation<Color?>(Colors.red),
onDismiss: onDismiss,
);
}
}
class SecondWidgetWithCompetence extends StatelessWidget { class SecondWidgetWithCompetence extends StatelessWidget {
const SecondWidgetWithCompetence({Key? key}) : super(key: key); const SecondWidgetWithCompetence({Key? key}) : super(key: key);
@override @override
@ -529,3 +983,22 @@ class SecondWidgetWithCompetence extends StatelessWidget {
); );
} }
} }
class AnimatedSecondWidgetWithCompetence extends StatelessWidget {
const AnimatedSecondWidgetWithCompetence({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
const AnimatedModalBarrier(
key: ValueKey<String>('barrier'),
color: AlwaysStoppedAnimation<Color?>(Colors.red),
),
GestureDetector(
onVerticalDragStart: (_) {},
behavior: HitTestBehavior.translucent,
child: Container(),
),
],
);
}
}