Unverified Commit db518732 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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.
parent 548e8853
...@@ -60,6 +60,7 @@ class ModalBarrier extends StatelessWidget { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -215,6 +221,7 @@ class AnimatedModalBarrier extends AnimatedWidget {
dismissible: dismissible, dismissible: dismissible,
semanticsLabel: semanticsLabel, semanticsLabel: semanticsLabel,
barrierSemanticsDismissible: barrierSemanticsDismissible, barrierSemanticsDismissible: barrierSemanticsDismissible,
onDismiss: onDismiss,
); );
} }
} }
......
...@@ -15,9 +15,11 @@ void main() { ...@@ -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,430 +44,867 @@ void main() { ...@@ -42,430 +44,867 @@ void main() {
); );
}); });
testWidgets('ModalBarrier prevents interactions with widgets behind it', (WidgetTester tester) async { group('ModalBarrier', () {
final Widget subject = Stack( testWidgets('prevents interactions with widgets behind it', (WidgetTester tester) async {
textDirection: TextDirection.ltr, final Widget subject = Stack(
children: <Widget>[ textDirection: TextDirection.ltr,
tapTarget, children: <Widget>[
const ModalBarrier(dismissible: false), tapTarget,
], const ModalBarrier(dismissible: false),
); ],
);
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
await tester.tap(find.text('target'), warnIfMissed: false); await tester.tap(find.text('target'), warnIfMissed: false);
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
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>[
hoverTarget, hoverTarget,
const ModalBarrier(dismissible: false), const ModalBarrier(dismissible: false),
], ],
); );
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
// Start out of hoverTarget // Start out of hoverTarget
await gesture.moveTo(const Offset(100, 100)); await gesture.moveTo(const Offset(100, 100));
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
// Move into hoverTarget and tap // Move into hoverTarget and tap
await gesture.down(const Offset(5, 5)); await gesture.down(const Offset(5, 5));
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
await gesture.up(); await gesture.up();
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
// Move out // Move out
await gesture.moveTo(const Offset(100, 100)); await gesture.moveTo(const Offset(100, 100));
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
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>[
const ModalBarrier(dismissible: false), const ModalBarrier(dismissible: false),
tapTarget, tapTarget,
], ],
); );
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
await tester.tap(find.text('target')); await tester.tap(find.text('target'));
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
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,
children: <Widget>[ children: <Widget>[
const ModalBarrier(dismissible: false), const ModalBarrier(dismissible: false),
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onHorizontalDragStart: (_) { onHorizontalDragStart: (_) {
dragged = true; dragged = true;
}, },
child: const Center( child: const Center(
child: Text('target', textDirection: TextDirection.ltr), 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 { await tester.pumpWidget(subject);
final List<String> playedSystemSounds = <String>[]; await tester.dragFrom(
try { tester.getBottomRight(find.byType(GestureDetector)) - const Offset(10, 10),
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { const Offset(-20, 0),
if (methodCall.method == 'SystemSound.play') { );
playedSystemSounds.add(methodCall.arguments as String); await tester.pumpWidget(subject);
} expect(dragged, isTrue, reason: 'because the drag is prevented by ModalBarrier');
return null; });
});
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>[
tapTarget,
const ModalBarrier(dismissible: false), 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); await tester.pumpWidget(subject);
await tester.tap(find.text('target'), warnIfMissed: false); expect(hovered, isFalse);
// Move into hoverTarget
await gesture.moveTo(const Offset(5, 5));
await tester.pumpWidget(subject); await tester.pumpWidget(subject);
} finally { expect(hovered, isTrue, reason: 'because the hover is prevented by ModalBarrier');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); hovered = false;
}
expect(playedSystemSounds, hasLength(1));
expect(playedSystemSounds[0], SystemSoundType.alert.toString());
});
testWidgets('ModalBarrier pops the Navigator when dismissed by primary tap', (WidgetTester tester) async { // Move out
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ await gesture.moveTo(const Offset(100, 100));
'/': (BuildContext context) => const FirstWidget(), await tester.pumpWidget(subject);
'/modal': (BuildContext context) => const SecondWidget(), expect(hovered, isTrue, reason: 'because the hover is prevented by ModalBarrier');
}; 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,
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('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.',
);
});
await tester.pumpWidget(MaterialApp(routes: routes)); 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 SecondWidget(),
};
// Initially the barrier is not visible await tester.pumpWidget(MaterialApp(routes: routes));
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
// Tapping on X routes to the barrier // Initially the barrier is not visible
await tester.tap(find.text('X')); expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
await tester.pump(); // begin transition
await tester.pump(const Duration(seconds: 1)); // end transition
// Press the barrier; it shouldn't dismiss yet // Tapping on X routes to the barrier
final TestGesture gesture = await tester.press( await tester.tap(find.text('X'));
find.byKey(const ValueKey<String>('barrier')), await tester.pump(); // begin transition
); await tester.pump(const Duration(seconds: 1)); // end transition
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 { // Press the barrier; it shouldn't dismiss yet
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final TestGesture gesture = await tester.press(
'/': (BuildContext context) => const FirstWidget(), find.byKey(const ValueKey<String>('barrier')),
'/modal': (BuildContext context) => const SecondWidget(), 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.',
);
});
await tester.pumpWidget(MaterialApp(routes: routes)); 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 SecondWidgetWithCompetence(),
};
// Initially the barrier is not visible await tester.pumpWidget(MaterialApp(routes: routes));
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
// Tapping on X routes to the barrier // Initially the barrier is not visible
await tester.tap(find.text('X')); expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
await tester.pump(); // begin transition
await tester.pump(const Duration(seconds: 1)); // end transition
// Press the barrier; it shouldn't dismiss yet // Tapping on X routes to the barrier
final TestGesture gesture = await tester.press( await tester.tap(find.text('X'));
find.byKey(const ValueKey<String>('barrier')), await tester.pump(); // begin transition
buttons: kSecondaryButton, await tester.pump(const Duration(seconds: 1)); // end transition
);
await tester.pumpAndSettle(); // begin transition // Tap on the barrier to dismiss it
expect(find.byKey(const ValueKey<String>('barrier')), findsOneWidget); await tester.tap(find.byKey(const ValueKey<String>('barrier')));
await tester.pump(); // begin transition
// Release the pointer; the barrier should be dismissed await tester.pump(const Duration(seconds: 1)); // end transition
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 { expect(
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ find.byKey(const ValueKey<String>('barrier')),
'/': (BuildContext context) => const FirstWidget(), findsNothing,
'/modal': (BuildContext context) => const SecondWidgetWithCompetence(), 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 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.',
);
await tester.pumpWidget(MaterialApp(routes: routes)); 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 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.',
);
// Initially the barrier is not visible expect(willPopCalled, isTrue);
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); });
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) =>
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('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.',
);
});
// Tapping on X routes to the barrier testWidgets('Undismissible ModalBarrier hidden in semantic tree', (WidgetTester tester) async {
await tester.tap(find.text('X')); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pump(); // begin transition await tester.pumpWidget(const ModalBarrier(dismissible: false));
await tester.pump(const Duration(seconds: 1)); // end transition
// Tap on the barrier to dismiss it final TestSemantics expectedSemantics = TestSemantics.root();
await tester.tap(find.byKey(const ValueKey<String>('barrier'))); expect(semantics, hasSemantics(expectedSemantics));
await tester.pump(); // begin transition
await tester.pump(const Duration(seconds: 1)); // end transition
expect( semantics.dispose();
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 { testWidgets('Dismissible ModalBarrier includes button in semantic tree on iOS', (WidgetTester tester) async {
bool willPopCalled = false; final SemanticsTester semantics = SemanticsTester(tester);
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ await tester.pumpWidget(const Directionality(
'/': (BuildContext context) => const FirstWidget(), textDirection: TextDirection.ltr,
'/modal': (BuildContext context) => Stack( child: ModalBarrier(
children: <Widget>[ semanticsLabel: 'Dismiss',
const SecondWidget(), ),
WillPopScope( ));
child: const SizedBox(),
onWillPop: () async { final TestSemantics expectedSemantics = TestSemantics.root(
willPopCalled = true; children: <TestSemantics>[
return false; TestSemantics.rootChild(
}, rect: TestSemantics.fullScreen,
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
label: 'Dismiss',
textDirection: TextDirection.ltr,
), ),
], ],
), );
}; expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
await tester.pumpWidget(MaterialApp(routes: routes)); semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS}));
// Initially the barrier is not visible testWidgets(
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); '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());
// Tapping on X routes to the barrier final TestSemantics expectedSemantics = TestSemantics.root();
await tester.tap(find.text('X')); expect(semantics, hasSemantics(expectedSemantics));
await tester.pump(); // begin transition
await tester.pump(const Duration(seconds: 1)); // end transition
expect(willPopCalled, isFalse); 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),
],
);
// Tap on the barrier to attempt to dismiss it await tester.pumpWidget(subject);
await tester.tap(find.byKey(const ValueKey<String>('barrier'))); await tester.tap(find.text('target'), warnIfMissed: false);
await tester.pump(); // begin transition await tester.pumpWidget(subject);
await tester.pump(const Duration(seconds: 1)); // end transition expect(tapped, isFalse, reason: 'because the tap is not prevented by ModalBarrier');
});
expect( testWidgets('prevents hover interactions with widgets behind it', (WidgetTester tester) async {
find.byKey(const ValueKey<String>('barrier')), final Widget subject = Stack(
findsOneWidget, textDirection: TextDirection.ltr,
reason: 'The route should still be present if the pop is vetoed.', children: <Widget>[
); hoverTarget,
AnimatedModalBarrier(dismissible: false, color: colorAnimation),
],
);
expect(willPopCalled, isTrue); 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);
testWidgets('ModalBarrier pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async { expect(hovered, isFalse, reason: 'because the hover is not prevented by AnimatedModalBarrier');
bool willPopCalled = false; });
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(), testWidgets('does not prevent interactions with widgets in front of it', (WidgetTester tester) async {
'/modal': (BuildContext context) => Stack( final Widget subject = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[ children: <Widget>[
const SecondWidget(), AnimatedModalBarrier(dismissible: false, color: colorAnimation),
WillPopScope( tapTarget,
child: const SizedBox(), ],
onWillPop: () async { );
willPopCalled = true;
return true; 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(MaterialApp(routes: routes)); 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');
});
// Initially the barrier is not visible testWidgets('does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async {
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing); final Widget subject = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
AnimatedModalBarrier(dismissible: false, color: colorAnimation),
hoverTarget,
],
);
// Tapping on X routes to the barrier final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await tester.tap(find.text('X')); addTearDown(gesture.removePointer);
await tester.pump(); // begin transition // Start out of hoverTarget
await tester.pump(const Duration(seconds: 1)); // end transition await gesture.moveTo(const Offset(100, 100));
await tester.pumpWidget(subject);
expect(hovered, isFalse);
expect(willPopCalled, 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;
// Tap on the barrier to attempt to dismiss it // Move out
await tester.tap(find.byKey(const ValueKey<String>('barrier'))); await gesture.moveTo(const Offset(100, 100));
await tester.pump(); // begin transition await tester.pumpWidget(subject);
await tester.pump(const Duration(seconds: 1)); // end transition 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.',
);
});
expect( testWidgets('pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async {
find.byKey(const ValueKey<String>('barrier')), final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
findsNothing, '/': (BuildContext context) => const FirstWidget(),
reason: 'The route should not be present if the pop is permitted.', '/modal': (BuildContext context) => const AnimatedSecondWidget(),
); };
expect(willPopCalled, isTrue); await tester.pumpWidget(MaterialApp(routes: routes));
});
testWidgets('ModalBarrier will call onDismiss callback', (WidgetTester tester) async { // Initially the barrier is not visible
bool dismissCallbackCalled = false; expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
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 { // Tapping on X routes to the barrier
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ await tester.tap(find.text('X'));
'/': (BuildContext context) => const FirstWidget(), await tester.pump(); // begin transition
'/modal': (BuildContext context) => SecondWidget(onDismiss: () {}), await tester.pump(const Duration(seconds: 1)); // end transition
};
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 { // Press the barrier; it shouldn't dismiss yet
final SemanticsTester semantics = SemanticsTester(tester); final TestGesture gesture = await tester.press(
await tester.pumpWidget(const ModalBarrier(dismissible: false)); 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.',
);
});
final TestSemantics expectedSemantics = TestSemantics.root(); testWidgets('may pop the Navigator when competing with other gestures', (WidgetTester tester) async {
expect(semantics, hasSemantics(expectedSemantics)); final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => const FirstWidget(),
'/modal': (BuildContext context) => const AnimatedSecondWidgetWithCompetence(),
};
semantics.dispose(); await tester.pumpWidget(MaterialApp(routes: routes));
});
testWidgets('Dismissible ModalBarrier includes button in semantic tree on iOS', (WidgetTester tester) async { // Initially the barrier is not visible
final SemanticsTester semantics = SemanticsTester(tester); expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
child: ModalBarrier(
semanticsLabel: 'Dismiss',
),
));
final TestSemantics expectedSemantics = TestSemantics.root( // Tapping on X routes to the barrier
children: <TestSemantics>[ await tester.tap(find.text('X'));
TestSemantics.rootChild( await tester.pump(); // begin transition
rect: TestSemantics.fullScreen, await tester.pump(const Duration(seconds: 1)); // end transition
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
label: 'Dismiss', // Tap on the barrier to dismiss it
textDirection: TextDirection.ltr, 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,
), ),
], ));
);
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true)); 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(); 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(
final SemanticsTester semantics = SemanticsTester(tester); 'Dismissible AnimatedModalBarrier is hidden on Android (back button is used to dismiss)', (WidgetTester tester) async {
await tester.pumpWidget(const ModalBarrier()); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(AnimatedModalBarrier(color: colorAnimation));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose(); semantics.dispose();
});
}); });
testWidgets('ModalBarrier uses default mouse cursor', (WidgetTester tester) async { 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>[
...@@ -485,7 +924,7 @@ void main() { ...@@ -485,7 +924,7 @@ void main() {
} }
class FirstWidget extends StatelessWidget { class FirstWidget extends StatelessWidget {
const FirstWidget({ Key? key }) : super(key: key); const FirstWidget({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
...@@ -498,7 +937,7 @@ class FirstWidget extends StatelessWidget { ...@@ -498,7 +937,7 @@ class FirstWidget extends StatelessWidget {
} }
class SecondWidget extends StatelessWidget { class SecondWidget extends StatelessWidget {
const SecondWidget({ Key? key, this.onDismiss }) : super(key: key); const SecondWidget({Key? key, this.onDismiss}) : super(key: key);
final VoidCallback? onDismiss; final VoidCallback? onDismiss;
...@@ -511,8 +950,23 @@ class SecondWidget extends StatelessWidget { ...@@ -511,8 +950,23 @@ 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
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
...@@ -529,3 +983,22 @@ class SecondWidgetWithCompetence extends StatelessWidget { ...@@ -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(),
),
],
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment