Unverified Commit 24db45e7 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Disable backspace/delete handling on iOS & macOS (#115900)

* Disable backspace/delete handling on iOS

* fix tests

* review

* macOS too

* review
parent e6696836
...@@ -19,6 +19,12 @@ import 'text_editing_intents.dart'; ...@@ -19,6 +19,12 @@ import 'text_editing_intents.dart';
/// lower in the widget tree than this. See the [Action] class for an example /// lower in the widget tree than this. See the [Action] class for an example
/// of remapping an [Intent] to a custom [Action]. /// of remapping an [Intent] to a custom [Action].
/// ///
/// The [Shortcuts] widget usually takes precedence over system keybindings.
/// Proceed with caution if the shortcut you wish to override is also used by
/// the system. For example, overriding [LogicalKeyboardKey.backspace] could
/// cause CJK input methods to discard more text than they should when the
/// backspace key is pressed during text composition on iOS.
///
/// {@tool snippet} /// {@tool snippet}
/// ///
/// This example shows how to use an additional [Shortcuts] widget to override /// This example shows how to use an additional [Shortcuts] widget to override
...@@ -440,6 +446,7 @@ class DefaultTextEditingShortcuts extends StatelessWidget { ...@@ -440,6 +446,7 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
static final Map<ShortcutActivator, Intent> _macDisablingTextShortcuts = <ShortcutActivator, Intent>{ static final Map<ShortcutActivator, Intent> _macDisablingTextShortcuts = <ShortcutActivator, Intent>{
..._commonDisablingTextShortcuts, ..._commonDisablingTextShortcuts,
..._iOSDisablingTextShortcuts,
const SingleActivator(LogicalKeyboardKey.escape): const DoNothingAndStopPropagationTextIntent(), const SingleActivator(LogicalKeyboardKey.escape): const DoNothingAndStopPropagationTextIntent(),
const SingleActivator(LogicalKeyboardKey.tab): const DoNothingAndStopPropagationTextIntent(), const SingleActivator(LogicalKeyboardKey.tab): const DoNothingAndStopPropagationTextIntent(),
const SingleActivator(LogicalKeyboardKey.tab, shift: true): const DoNothingAndStopPropagationTextIntent(), const SingleActivator(LogicalKeyboardKey.tab, shift: true): const DoNothingAndStopPropagationTextIntent(),
...@@ -447,6 +454,20 @@ class DefaultTextEditingShortcuts extends StatelessWidget { ...@@ -447,6 +454,20 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(), const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
}; };
// Hand backspace/delete events that do not depend on text layout (delete
// character and delete to the next word) back to the IME to allow it to
// update composing text properly.
static const Map<ShortcutActivator, Intent> _iOSDisablingTextShortcuts = <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.backspace): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.backspace, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.delete): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.delete, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.backspace, alt: true, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.delete, alt: true, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.delete, alt: true): DoNothingAndStopPropagationTextIntent(),
};
static Map<ShortcutActivator, Intent> get _shortcuts { static Map<ShortcutActivator, Intent> get _shortcuts {
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
...@@ -469,13 +490,13 @@ class DefaultTextEditingShortcuts extends StatelessWidget { ...@@ -469,13 +490,13 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
return _webDisablingTextShortcuts; return _webDisablingTextShortcuts;
} }
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.windows: case TargetPlatform.windows:
return null; return null;
case TargetPlatform.iOS:
return _iOSDisablingTextShortcuts;
case TargetPlatform.macOS: case TargetPlatform.macOS:
return _macDisablingTextShortcuts; return _macDisablingTextShortcuts;
} }
......
...@@ -44,6 +44,87 @@ void main() { ...@@ -44,6 +44,87 @@ void main() {
), ),
); );
} }
group('iOS: do not delete/backspace events', () {
final TargetPlatformVariant iOS = TargetPlatformVariant.only(TargetPlatform.iOS);
final FocusNode editable = FocusNode();
final FocusNode spy = FocusNode();
testWidgets('backspace with and without word modifier', (WidgetTester tester) async {
tester.binding.testTextInput.unregister();
addTearDown(tester.binding.testTextInput.register);
await tester.pumpWidget(
buildSpyAboveEditableText(
editableFocusNode: editable,
spyFocusNode: spy,
),
);
editable.requestFocus();
await tester.pump();
final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));
for (int altShiftState = 0; altShiftState < 1 << 2; altShiftState += 1) {
final bool alt = altShiftState & 0x1 != 0;
final bool shift = altShiftState & 0x2 != 0;
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.backspace, alt: alt, shift: shift));
}
await tester.pump();
expect(state.lastIntent, isNull);
}, variant: iOS);
testWidgets('delete with and without word modifier', (WidgetTester tester) async {
tester.binding.testTextInput.unregister();
addTearDown(tester.binding.testTextInput.register);
await tester.pumpWidget(
buildSpyAboveEditableText(
editableFocusNode: editable,
spyFocusNode: spy,
),
);
editable.requestFocus();
await tester.pump();
final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));
for (int altShiftState = 0; altShiftState < 1 << 2; altShiftState += 1) {
final bool alt = altShiftState & 0x1 != 0;
final bool shift = altShiftState & 0x2 != 0;
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.delete, alt: alt, shift: shift));
}
await tester.pump();
expect(state.lastIntent, isNull);
}, variant: iOS);
testWidgets('Exception: deleting to line boundary is handled by the framework', (WidgetTester tester) async {
tester.binding.testTextInput.unregister();
addTearDown(tester.binding.testTextInput.register);
await tester.pumpWidget(
buildSpyAboveEditableText(
editableFocusNode: editable,
spyFocusNode: spy,
),
);
editable.requestFocus();
await tester.pump();
final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));
for (int keyState = 0; keyState < 1 << 2; keyState += 1) {
final bool shift = keyState & 0x1 != 0;
final LogicalKeyboardKey key = keyState & 0x2 != 0 ? LogicalKeyboardKey.delete : LogicalKeyboardKey.backspace;
state.lastIntent = null;
final SingleActivator activator = SingleActivator(key, meta: true, shift: shift);
await sendKeyCombination(tester, activator);
await tester.pump();
expect(state.lastIntent, isA<DeleteToLineBreakIntent>(), reason: '$activator');
}
}, variant: iOS);
}, skip: kIsWeb); // [intended] specific tests target non-web.
group('macOS does not accept shortcuts if focus under EditableText', () { group('macOS does not accept shortcuts if focus under EditableText', () {
final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS); final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS);
...@@ -400,6 +481,10 @@ class ActionSpyState extends State<ActionSpy> { ...@@ -400,6 +481,10 @@ class ActionSpyState extends State<ActionSpy> {
ExtendSelectionVerticallyToAdjacentLineIntent: CallbackAction<ExtendSelectionVerticallyToAdjacentLineIntent>(onInvoke: _captureIntent), ExtendSelectionVerticallyToAdjacentLineIntent: CallbackAction<ExtendSelectionVerticallyToAdjacentLineIntent>(onInvoke: _captureIntent),
ExtendSelectionToDocumentBoundaryIntent: CallbackAction<ExtendSelectionToDocumentBoundaryIntent>(onInvoke: _captureIntent), ExtendSelectionToDocumentBoundaryIntent: CallbackAction<ExtendSelectionToDocumentBoundaryIntent>(onInvoke: _captureIntent),
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: CallbackAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(onInvoke: _captureIntent), ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: CallbackAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(onInvoke: _captureIntent),
DeleteToLineBreakIntent: CallbackAction<DeleteToLineBreakIntent>(onInvoke: _captureIntent),
DeleteToNextWordBoundaryIntent: CallbackAction<DeleteToNextWordBoundaryIntent>(onInvoke: _captureIntent),
DeleteCharacterIntent: CallbackAction<DeleteCharacterIntent>(onInvoke: _captureIntent),
}; };
// ignore: use_setters_to_change_properties // ignore: use_setters_to_change_properties
......
...@@ -150,7 +150,7 @@ void main() { ...@@ -150,7 +150,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 19), const TextSelection.collapsed(offset: 19),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('backspace readonly', (WidgetTester tester) async { testWidgets('backspace readonly', (WidgetTester tester) async {
controller.text = testText; controller.text = testText;
...@@ -215,7 +215,7 @@ void main() { ...@@ -215,7 +215,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 71), const TextSelection.collapsed(offset: 71),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('backspace inside of a cluster', (WidgetTester tester) async { testWidgets('backspace inside of a cluster', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -236,7 +236,7 @@ void main() { ...@@ -236,7 +236,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('backspace at cluster boundary', (WidgetTester tester) async { testWidgets('backspace at cluster boundary', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -257,7 +257,7 @@ void main() { ...@@ -257,7 +257,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
}); });
group('delete: ', () { group('delete: ', () {
...@@ -287,7 +287,7 @@ void main() { ...@@ -287,7 +287,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 20), const TextSelection.collapsed(offset: 20),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('delete readonly', (WidgetTester tester) async { testWidgets('delete readonly', (WidgetTester tester) async {
controller.text = testText; controller.text = testText;
...@@ -305,7 +305,7 @@ void main() { ...@@ -305,7 +305,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 20, affinity: TextAffinity.upstream), const TextSelection.collapsed(offset: 20, affinity: TextAffinity.upstream),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('delete at start', (WidgetTester tester) async { testWidgets('delete at start', (WidgetTester tester) async {
controller.text = testText; controller.text = testText;
...@@ -328,7 +328,7 @@ void main() { ...@@ -328,7 +328,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('delete at end', (WidgetTester tester) async { testWidgets('delete at end', (WidgetTester tester) async {
controller.text = testText; controller.text = testText;
...@@ -373,7 +373,7 @@ void main() { ...@@ -373,7 +373,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('delete at cluster boundary', (WidgetTester tester) async { testWidgets('delete at cluster boundary', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -394,7 +394,7 @@ void main() { ...@@ -394,7 +394,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 8), const TextSelection.collapsed(offset: 8),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
}); });
group('Non-collapsed delete', () { group('Non-collapsed delete', () {
...@@ -420,7 +420,7 @@ void main() { ...@@ -420,7 +420,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 8), const TextSelection.collapsed(offset: 8),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('at the boundaries of a cluster', (WidgetTester tester) async { testWidgets('at the boundaries of a cluster', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -441,7 +441,7 @@ void main() { ...@@ -441,7 +441,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 8), const TextSelection.collapsed(offset: 8),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('cross-cluster', (WidgetTester tester) async { testWidgets('cross-cluster', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -462,7 +462,7 @@ void main() { ...@@ -462,7 +462,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('cross-cluster obscured text', (WidgetTester tester) async { testWidgets('cross-cluster obscured text', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -483,7 +483,7 @@ void main() { ...@@ -483,7 +483,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 1), const TextSelection.collapsed(offset: 1),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
}); });
group('word modifier + backspace', () { group('word modifier + backspace', () {
...@@ -516,7 +516,7 @@ void main() { ...@@ -516,7 +516,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 24), const TextSelection.collapsed(offset: 24),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('readonly', (WidgetTester tester) async { testWidgets('readonly', (WidgetTester tester) async {
controller.text = testText; controller.text = testText;
...@@ -581,7 +581,7 @@ void main() { ...@@ -581,7 +581,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 71), const TextSelection.collapsed(offset: 71),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('inside of a cluster', (WidgetTester tester) async { testWidgets('inside of a cluster', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -602,7 +602,7 @@ void main() { ...@@ -602,7 +602,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('at cluster boundary', (WidgetTester tester) async { testWidgets('at cluster boundary', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -623,7 +623,7 @@ void main() { ...@@ -623,7 +623,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
}); });
group('word modifier + delete', () { group('word modifier + delete', () {
...@@ -656,7 +656,7 @@ void main() { ...@@ -656,7 +656,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 23), const TextSelection.collapsed(offset: 23),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('readonly', (WidgetTester tester) async { testWidgets('readonly', (WidgetTester tester) async {
controller.text = testText; controller.text = testText;
...@@ -697,7 +697,7 @@ void main() { ...@@ -697,7 +697,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('at end', (WidgetTester tester) async { testWidgets('at end', (WidgetTester tester) async {
controller.text = testText; controller.text = testText;
...@@ -735,7 +735,7 @@ void main() { ...@@ -735,7 +735,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 0), const TextSelection.collapsed(offset: 0),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('at cluster boundary', (WidgetTester tester) async { testWidgets('at cluster boundary', (WidgetTester tester) async {
controller.text = testCluster; controller.text = testCluster;
...@@ -756,7 +756,7 @@ void main() { ...@@ -756,7 +756,7 @@ void main() {
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 8), const TextSelection.collapsed(offset: 8),
); );
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
}); });
group('line modifier + backspace', () { group('line modifier + backspace', () {
......
...@@ -6330,69 +6330,71 @@ void main() { ...@@ -6330,69 +6330,71 @@ void main() {
expect(controller.text, equals(testText), reason: 'on $platform'); expect(controller.text, equals(testText), reason: 'on $platform');
expect((await Clipboard.getData(Clipboard.kTextPlain))!.text, equals(testText)); expect((await Clipboard.getData(Clipboard.kTextPlain))!.text, equals(testText));
// Delete if (defaultTargetPlatform != TargetPlatform.iOS) {
await sendKeys( // Delete
tester, await sendKeys(
<LogicalKeyboardKey>[ tester,
LogicalKeyboardKey.delete, <LogicalKeyboardKey>[
], LogicalKeyboardKey.delete,
targetPlatform: defaultTargetPlatform, ],
); targetPlatform: defaultTargetPlatform,
expect( );
selection, expect(
equals( selection,
const TextSelection( equals(
baseOffset: 0, const TextSelection(
extentOffset: 0, baseOffset: 0,
extentOffset: 0,
),
), ),
), reason: 'on $platform',
reason: 'on $platform', );
); expect(controller.text, isEmpty, reason: 'on $platform');
expect(controller.text, isEmpty, reason: 'on $platform');
controller.text = 'abc'; controller.text = 'abc';
controller.selection = const TextSelection(baseOffset: 2, extentOffset: 2); controller.selection = const TextSelection(baseOffset: 2, extentOffset: 2);
// Backspace // Backspace
await sendKeys( await sendKeys(
tester, tester,
<LogicalKeyboardKey>[ <LogicalKeyboardKey>[
LogicalKeyboardKey.backspace, LogicalKeyboardKey.backspace,
], ],
targetPlatform: defaultTargetPlatform, targetPlatform: defaultTargetPlatform,
); );
expect( expect(
selection, selection,
equals( equals(
const TextSelection( const TextSelection(
baseOffset: 1, baseOffset: 1,
extentOffset: 1, extentOffset: 1,
),
), ),
), reason: 'on $platform',
reason: 'on $platform', );
); expect(controller.text, 'ac', reason: 'on $platform');
expect(controller.text, 'ac', reason: 'on $platform');
// Shift-backspace (same as backspace) // Shift-backspace (same as backspace)
await sendKeys( await sendKeys(
tester, tester,
<LogicalKeyboardKey>[ <LogicalKeyboardKey>[
LogicalKeyboardKey.backspace, LogicalKeyboardKey.backspace,
], ],
shift: true, shift: true,
targetPlatform: defaultTargetPlatform, targetPlatform: defaultTargetPlatform,
); );
expect( expect(
selection, selection,
equals( equals(
const TextSelection( const TextSelection(
baseOffset: 0, baseOffset: 0,
extentOffset: 0, extentOffset: 0,
),
), ),
), reason: 'on $platform',
reason: 'on $platform', );
); expect(controller.text, 'c', reason: 'on $platform');
expect(controller.text, 'c', reason: 'on $platform'); }
} }
testWidgets('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async { testWidgets('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async {
......
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