Unverified Commit df28355b authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Raw keyboard shortcuts & deletions should not read from _plainText (#71236)

parent 614a6ba6
...@@ -767,22 +767,23 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -767,22 +767,23 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// Handles shortcut functionality including cut, copy, paste and select all // Handles shortcut functionality including cut, copy, paste and select all
// using control/command + (X, C, V, A). // using control/command + (X, C, V, A).
Future<void> _handleShortcuts(LogicalKeyboardKey key) async { Future<void> _handleShortcuts(LogicalKeyboardKey key) async {
final TextSelection selection = textSelectionDelegate.textEditingValue.selection;
final String text = textSelectionDelegate.textEditingValue.text;
assert(selection != null); assert(selection != null);
assert(_shortcutKeys.contains(key), 'shortcut key $key not recognized.'); assert(_shortcutKeys.contains(key), 'shortcut key $key not recognized.');
if (key == LogicalKeyboardKey.keyC) { if (key == LogicalKeyboardKey.keyC) {
if (!selection!.isCollapsed) { if (!selection.isCollapsed) {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: selection!.textInside(_plainText))); ClipboardData(text: selection.textInside(text)));
} }
return; return;
} }
if (key == LogicalKeyboardKey.keyX && !_readOnly) { if (key == LogicalKeyboardKey.keyX && !_readOnly) {
if (!selection!.isCollapsed) { if (!selection.isCollapsed) {
Clipboard.setData(ClipboardData(text: selection!.textInside(_plainText))); Clipboard.setData(ClipboardData(text: selection.textInside(text)));
textSelectionDelegate.textEditingValue = TextEditingValue( textSelectionDelegate.textEditingValue = TextEditingValue(
text: selection!.textBefore(_plainText) text: selection.textBefore(text) + selection.textAfter(text),
+ selection!.textAfter(_plainText), selection: TextSelection.collapsed(offset: math.min(selection.start, selection.end)),
selection: TextSelection.collapsed(offset: selection!.start),
); );
} }
return; return;
...@@ -790,15 +791,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -790,15 +791,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (key == LogicalKeyboardKey.keyV && !_readOnly) { if (key == LogicalKeyboardKey.keyV && !_readOnly) {
// Snapshot the input before using `await`. // Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427 // See https://github.com/flutter/flutter/issues/11427
final TextEditingValue value = textSelectionDelegate.textEditingValue;
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) { if (data != null) {
textSelectionDelegate.textEditingValue = TextEditingValue( textSelectionDelegate.textEditingValue = TextEditingValue(
text: value.selection.textBefore(value.text) text: selection.textBefore(text) + data.text! + selection.textAfter(text),
+ data.text!
+ value.selection.textAfter(value.text),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: value.selection.start + data.text!.length offset: math.min(selection.start, selection.end) + data.text!.length,
), ),
); );
} }
...@@ -806,7 +804,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -806,7 +804,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
if (key == LogicalKeyboardKey.keyA) { if (key == LogicalKeyboardKey.keyA) {
_handleSelectionChange( _handleSelectionChange(
selection!.copyWith( selection.copyWith(
baseOffset: 0, baseOffset: 0,
extentOffset: textSelectionDelegate.textEditingValue.text.length, extentOffset: textSelectionDelegate.textEditingValue.text.length,
), ),
...@@ -817,15 +815,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -817,15 +815,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
void _handleDelete({ required bool forward }) { void _handleDelete({ required bool forward }) {
final TextSelection selection = textSelectionDelegate.textEditingValue.selection;
final String text = textSelectionDelegate.textEditingValue.text;
assert(_selection != null); assert(_selection != null);
if (_readOnly) { if (_readOnly) {
return; return;
} }
String textBefore = selection!.textBefore(_plainText); String textBefore = selection.textBefore(text);
String textAfter = selection!.textAfter(_plainText); String textAfter = selection.textAfter(text);
int cursorPosition = selection!.start; int cursorPosition = math.min(selection.start, selection.end);
// If not deleting a selection, delete the next/previous character. // If not deleting a selection, delete the next/previous character.
if (selection!.isCollapsed) { if (selection.isCollapsed) {
if (!forward && textBefore.isNotEmpty) { if (!forward && textBefore.isNotEmpty) {
final int characterBoundary = previousCharacter(textBefore.length, textBefore); final int characterBoundary = previousCharacter(textBefore.length, textBefore);
textBefore = textBefore.substring(0, characterBoundary); textBefore = textBefore.substring(0, characterBoundary);
...@@ -861,8 +861,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -861,8 +861,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_textLayoutLastMinWidth = null; _textLayoutLastMinWidth = null;
} }
// Returns a cached plain text version of the text in the painter.
String? _cachedPlainText; String? _cachedPlainText;
// Returns a plain text version of the text in the painter.
//
// Returns the obscured text when [obscureText] is true. See
// [obscureText] and [obscuringCharacter].
String get _plainText { String get _plainText {
_cachedPlainText ??= _textPainter.text!.toPlainText(); _cachedPlainText ??= _textPainter.text!.toPlainText();
return _cachedPlainText!; return _cachedPlainText!;
......
...@@ -4351,6 +4351,79 @@ void main() { ...@@ -4351,6 +4351,79 @@ void main() {
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}'); expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
}); });
testWidgets('Copy paste obscured text test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField =
TextField(
controller: controller,
obscureText: true,
);
String clipboardContent = '';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.setData')
clipboardContent = methodCall.arguments['text'] as String;
else if (methodCall.method == 'Clipboard.getData')
return <String, dynamic>{'text': clipboardContent};
return null;
});
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
onKey: null,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house jumped over a mouse';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// Select the first 5 characters
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
}
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
// Copy them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyEvent(LogicalKeyboardKey.keyC);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
expect(clipboardContent, 'a big');
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
// Paste them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
const String expected = 'a biga big house jumped over a mouse';
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
});
testWidgets('Cut test', (WidgetTester tester) async { testWidgets('Cut test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
...@@ -4426,6 +4499,80 @@ void main() { ...@@ -4426,6 +4499,80 @@ void main() {
expect(find.text(expected), findsOneWidget); expect(find.text(expected), findsOneWidget);
}); });
testWidgets('Cut obscured text test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField = TextField(
controller: controller,
obscureText: true,
);
String clipboardContent = '';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.setData')
clipboardContent = methodCall.arguments['text'] as String;
else if (methodCall.method == 'Clipboard.getData')
return <String, dynamic>{'text': clipboardContent};
return null;
});
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
onKey: null,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house jumped over a mouse';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// Select the first 5 characters
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
}
// Cut them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyEvent(LogicalKeyboardKey.keyX);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
expect(clipboardContent, 'a big');
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
}
// Paste them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
const String expected = ' housa bige jumped over a mouse';
expect(find.text(expected), findsOneWidget);
});
testWidgets('Select all test', (WidgetTester tester) async { testWidgets('Select all test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
......
...@@ -738,7 +738,11 @@ void main() { ...@@ -738,7 +738,11 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42772 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42772
test('arrow keys and delete handle simple text correctly', () async { test('arrow keys and delete handle simple text correctly', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 0),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection; late TextSelection currentSelection;
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
...@@ -787,7 +791,11 @@ void main() { ...@@ -787,7 +791,11 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('arrow keys and delete handle surrogate pairs correctly', () async { test('arrow keys and delete handle surrogate pairs correctly', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: '0123😆6789',
selection: TextSelection.collapsed(offset: 0),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection; late TextSelection currentSelection;
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
...@@ -837,7 +845,11 @@ void main() { ...@@ -837,7 +845,11 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('arrow keys and delete handle grapheme clusters correctly', () async { test('arrow keys and delete handle grapheme clusters correctly', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: '0123👨‍👩‍👦2345',
selection: TextSelection.collapsed(offset: 0),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection; late TextSelection currentSelection;
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
...@@ -999,7 +1011,11 @@ void main() { ...@@ -999,7 +1011,11 @@ void main() {
group('delete', () { group('delete', () {
test('handles selection', () async { test('handles selection', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection(baseOffset: 1, extentOffset: 3),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
...@@ -1032,7 +1048,11 @@ void main() { ...@@ -1032,7 +1048,11 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('is a no-op at the end of the text', () async { test('is a no-op at the end of the text', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 4),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
...@@ -1063,11 +1083,55 @@ void main() { ...@@ -1063,11 +1083,55 @@ void main() {
expect(delegate.textEditingValue.selection.isCollapsed, true); expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 4); expect(delegate.textEditingValue.selection.baseOffset, 4);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('handles obscured text', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 0),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
obscureText: true,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '****',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 0),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
expect(delegate.textEditingValue.text, 'est');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser);
}); });
group('backspace', () { group('backspace', () {
test('handles selection', () async { test('handles selection', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection(baseOffset: 1, extentOffset: 3),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
...@@ -1100,7 +1164,11 @@ void main() { ...@@ -1100,7 +1164,11 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('handles simple text', () async { test('handles simple text', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 3),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
...@@ -1133,7 +1201,11 @@ void main() { ...@@ -1133,7 +1201,11 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('handles surrogate pairs', () async { test('handles surrogate pairs', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: '\u{1F44D}',
selection: TextSelection.collapsed(offset: 2),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
...@@ -1166,7 +1238,11 @@ void main() { ...@@ -1166,7 +1238,11 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('handles grapheme clusters', () async { test('handles grapheme clusters', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: '0123👨‍👩‍👦2345',
selection: TextSelection.collapsed(offset: 12),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
...@@ -1199,7 +1275,11 @@ void main() { ...@@ -1199,7 +1275,11 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('is a no-op at the start of the text', () async { test('is a no-op at the start of the text', () async {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 0),
);
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
...@@ -1230,6 +1310,46 @@ void main() { ...@@ -1230,6 +1310,46 @@ void main() {
expect(delegate.textEditingValue.selection.isCollapsed, true); expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0); expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('handles obscured text', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 4),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
obscureText: true,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '****',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 4),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.backspace, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.backspace, platform: 'android');
expect(delegate.textEditingValue.text, 'tes');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 3);
}, skip: isBrowser);
}); });
test('getEndpointsForSelection handles empty characters', () { test('getEndpointsForSelection handles empty characters', () {
......
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