| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:io' show Platform; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../rendering/rendering_tester.dart'; |
| |
| class _FakeEditableTextState with TextSelectionDelegate, TextEditingActionTarget { |
| _FakeEditableTextState({ |
| required this.textEditingValue, |
| // Render editable parameters: |
| this.obscureText = false, |
| required this.textSpan, |
| this.textDirection = TextDirection.ltr, |
| }); |
| |
| final TextDirection textDirection; |
| final TextSpan textSpan; |
| |
| RenderEditable? _renderEditable; |
| RenderEditable get renderEditable { |
| if (_renderEditable != null) { |
| return _renderEditable!; |
| } |
| _renderEditable = RenderEditable( |
| backgroundCursorColor: Colors.grey, |
| selectionColor: Colors.black, |
| textDirection: textDirection, |
| cursorColor: Colors.red, |
| offset: ViewportOffset.zero(), |
| textSelectionDelegate: this, |
| startHandleLayerLink: LayerLink(), |
| endHandleLayerLink: LayerLink(), |
| text: textSpan, |
| selection: textEditingValue.selection, |
| textAlign: TextAlign.start, |
| ); |
| return _renderEditable!; |
| } |
| |
| // Start TextSelectionDelegate |
| |
| @override |
| TextEditingValue textEditingValue; |
| |
| @override |
| void hideToolbar([bool hideHandles = true]) { } |
| |
| @override |
| void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { } |
| |
| @override |
| void bringIntoView(TextPosition position) { } |
| |
| // End TextSelectionDelegate |
| // Start TextEditingActionTarget |
| |
| @override |
| bool get readOnly => false; |
| |
| @override |
| final bool obscureText; |
| |
| @override |
| bool get selectionEnabled => true; |
| |
| @override |
| TextLayoutMetrics get textLayoutMetrics => renderEditable; |
| |
| @override |
| void setSelection(TextSelection selection, SelectionChangedCause cause) { |
| renderEditable.selection = selection; |
| textEditingValue = textEditingValue.copyWith( |
| selection: selection, |
| ); |
| } |
| |
| @override |
| void setTextEditingValue(TextEditingValue newValue, SelectionChangedCause cause) { |
| textEditingValue = newValue; |
| } |
| |
| @override |
| void debugAssertLayoutUpToDate() {} |
| |
| // End TextEditingActionTarget |
| } |
| |
| void main() { |
| // Ensure that all TestRenderingFlutterBinding bindings are initialized. |
| renderer; |
| |
| test('moveSelectionLeft/RightByLine stays on the current line', () async { |
| const String text = 'one two three\n\nfour five six'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| |
| editableTextState.setSelection(const TextSelection.collapsed(offset: 0), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| // Move to the end of the first line. |
| editableTextState.moveSelectionRightByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 13); |
| expect(editableTextState.textEditingValue.selection.affinity, TextAffinity.upstream); |
| // RenderEditable relies on its parent that passes onSelectionChanged to set |
| // the selection. |
| |
| // Try moveSelectionRightByLine again and nothing happens because we're |
| // already at the end of a line. |
| editableTextState.moveSelectionRightByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 13); |
| expect(editableTextState.textEditingValue.selection.affinity, TextAffinity.upstream); |
| |
| // Move back to the start of the line. |
| editableTextState.moveSelectionLeftByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| expect(editableTextState.textEditingValue.selection.affinity, TextAffinity.downstream); |
| |
| // Trying moveSelectionLeftByLine does nothing at the leftmost of the field. |
| editableTextState.moveSelectionLeftByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| expect(editableTextState.textEditingValue.selection.affinity, TextAffinity.downstream); |
| |
| // Move the selection to the empty line. |
| editableTextState.moveSelectionRightByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 13); |
| expect(editableTextState.textEditingValue.selection.affinity, TextAffinity.upstream); |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 14); |
| expect(editableTextState.textEditingValue.selection.affinity, TextAffinity.downstream); |
| |
| // Neither moveSelectionLeftByLine nor moveSelectionRightByLine do anything |
| // here, because we're at both the beginning and end of the line. |
| editableTextState.moveSelectionLeftByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 14); |
| expect(editableTextState.textEditingValue.selection.affinity, TextAffinity.downstream); |
| editableTextState.moveSelectionRightByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 14); |
| expect(editableTextState.textEditingValue.selection.affinity, TextAffinity.downstream); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('arrow keys and delete handle simple text correctly', () async { |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: 'test', |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: 'test', |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| |
| editableTextState.setSelection(const TextSelection.collapsed(offset: 0), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 1); |
| |
| editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'est'); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('arrow keys and delete handle surrogate pairs correctly', () async { |
| const String text = '0123😆6789'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| |
| editableTextState.setSelection(const TextSelection.collapsed(offset: 4), SelectionChangedCause.keyboard); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 6); |
| |
| editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 4); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, '01236789'); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('arrow keys and delete handle grapheme clusters correctly', () async { |
| const String text = '0123👨👩👦2345'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| |
| editableTextState.setSelection(const TextSelection.collapsed(offset: 4), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 12); |
| |
| editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 4); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, '01232345'); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('arrow keys and delete handle surrogate pairs correctly case 2', () async { |
| const String text = '\u{1F44D}'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| |
| editableTextState.setSelection(const TextSelection.collapsed(offset: 0), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 2); |
| |
| editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, ''); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('arrow keys work after detaching the widget and attaching it again', () async { |
| const String text = 'W Szczebrzeszynie chrząszcz brzmi w trzcinie'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| final PipelineOwner pipelineOwner = PipelineOwner(); |
| editable.attach(pipelineOwner); |
| editable.hasFocus = true; |
| editable.detach(); |
| layout(editable); |
| editable.hasFocus = true; |
| editableTextState.setSelection(const TextSelection.collapsed(offset: 0), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editable.selection?.isCollapsed, true); |
| expect(editable.selection?.baseOffset, 4); |
| |
| editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editable.selection?.isCollapsed, true); |
| expect(editable.selection?.baseOffset, 3); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'W Sczebrzeszynie chrząszcz brzmi w trzcinie'); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('RenderEditable registers and unregisters raw keyboard listener correctly', () async { |
| const String text = 'how are you'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| final PipelineOwner pipelineOwner = PipelineOwner(); |
| editable.attach(pipelineOwner); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'ow are you'); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('arrow keys with selection text', () async { |
| const String text = '012345'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| |
| editableTextState.setSelection(const TextSelection(baseOffset: 2, extentOffset: 4), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 4); |
| |
| editableTextState.setSelection(const TextSelection(baseOffset: 4, extentOffset: 2), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 4); |
| |
| editableTextState.setSelection(const TextSelection(baseOffset: 2, extentOffset: 4), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 2); |
| |
| editableTextState.setSelection(const TextSelection(baseOffset: 4, extentOffset: 2), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 2); |
| }); |
| |
| test('arrow keys with selection text and shift', () async { |
| const String text = '012345'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| |
| editableTextState.setSelection(const TextSelection(baseOffset: 2, extentOffset: 4), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.extendSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, false); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 2); |
| expect(editableTextState.textEditingValue.selection.extentOffset, 5); |
| |
| editableTextState.setSelection(const TextSelection(baseOffset: 4, extentOffset: 2), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.extendSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, false); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 4); |
| expect(editableTextState.textEditingValue.selection.extentOffset, 3); |
| |
| editableTextState.setSelection(const TextSelection(baseOffset: 2, extentOffset: 4), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.extendSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, false); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 2); |
| expect(editableTextState.textEditingValue.selection.extentOffset, 3); |
| |
| editableTextState.setSelection(const TextSelection(baseOffset: 4, extentOffset: 2), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| editableTextState.extendSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, false); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 4); |
| expect(editableTextState.textEditingValue.selection.extentOffset, 1); |
| }); |
| |
| test('respects enableInteractiveSelection', () async { |
| const String text = '012345'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| |
| editableTextState.setSelection(const TextSelection.collapsed(offset: 2), SelectionChangedCause.tap); |
| pumpFrame(); |
| |
| await simulateKeyDownEvent(LogicalKeyboardKey.shift); |
| |
| editableTextState.moveSelectionRight(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 3); |
| |
| editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 2); |
| |
| final LogicalKeyboardKey wordModifier = |
| Platform.isMacOS ? LogicalKeyboardKey.alt : LogicalKeyboardKey.control; |
| |
| await simulateKeyDownEvent(wordModifier); |
| |
| editableTextState.moveSelectionRightByWord(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 6); |
| |
| editableTextState.moveSelectionLeftByWord(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| |
| await simulateKeyUpEvent(wordModifier); |
| await simulateKeyUpEvent(LogicalKeyboardKey.shift); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87681 |
| |
| group('delete', () { |
| test('when as a non-collapsed selection, it should delete a selection', () async { |
| const String text = 'test'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection(baseOffset: 1, extentOffset: 3), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.delete(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'tt'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 1); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('when as simple text, it should delete the character to the left', () async { |
| const String text = 'test'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 3), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.delete(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'tet'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 2); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('when has surrogate pairs, it should delete the pair', () async { |
| const String text = '\u{1F44D}'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 2), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.delete(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, ''); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('when has grapheme clusters, it should delete the grapheme cluster', () async { |
| const String text = '0123👨👩👦2345'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 12), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.delete(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, '01232345'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 4); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('when is at the start of the text, it should be a no-op', () async { |
| const String text = 'test'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.delete(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'test'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('when input has obscured text, it should delete the character to the left', () async { |
| const String text = 'test'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| obscureText: true, |
| textSpan: const TextSpan( |
| text: '****', |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 4), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.delete(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'tes'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 3); |
| }); |
| |
| test('when using cjk characters', () async { |
| const String text = '用多個塊測試'; |
| const int offset = 4; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.delete(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, '用多個測試'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 3); |
| }); |
| |
| test('when using rtl', () async { |
| const String text = 'برنامج أهلا بالعالم'; |
| const int offset = text.length; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textDirection: TextDirection.rtl, |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.delete(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'برنامج أهلا بالعال'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, text.length - 1); |
| }); |
| }); |
| |
| group('deleteByWord', () { |
| test('when cursor is on the middle of a word, it should delete the left part of the word', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 8; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test h multiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 5); |
| }); |
| |
| test('when includeWhiteSpace is true, it should treat a whiteSpace as a single word', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 10; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'test withmultiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 9); |
| }); |
| |
| test('when cursor is after a word, it should delete the whole word', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 9; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test multiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 5); |
| }); |
| |
| test('when cursor is preceeded by white spaces, it should delete the spaces and the next word to the left', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 12; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test multiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 5); |
| }); |
| |
| test('when cursor is preceeded by tabs spaces', () async { |
| const String text = 'test with\t\t\tmultiple blocks'; |
| const int offset = 12; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test multiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 5); |
| }); |
| |
| test('when cursor is preceeded by break line, it should delete the breaking line and the word right before it', () async { |
| const String text = 'test with\nmultiple blocks'; |
| const int offset = 10; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test multiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 5); |
| }); |
| |
| test('when using cjk characters', () async { |
| const String text = '用多個塊測試'; |
| const int offset = 4; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, '用多個測試'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 3); |
| }); |
| |
| test('when using rtl', () async { |
| const String text = 'برنامج أهلا بالعالم'; |
| const int offset = text.length; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textDirection: TextDirection.rtl, |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'برنامج أهلا '); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 12); |
| }); |
| |
| test('when input has obscured text, it should delete everything before the selection', () async { |
| const int offset = 21; |
| const String text = 'test with multiple\n\n words'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| obscureText: true, |
| textSpan: const TextSpan( |
| text: '****', |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'words'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }); |
| }); |
| |
| group('deleteByLine', () { |
| test('when cursor is on last character of a line, it should delete everything to the left', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = text.length; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, ''); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }); |
| |
| test('when cursor is on the middle of a word, it should delete delete everything to the left', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 8; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'h multiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }); |
| |
| test('when previous character is a breakline, it should preserve it', () async { |
| const String text = 'test with\nmultiple blocks'; |
| const int offset = 10; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, text); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when text is multiline, it should delete until the first line break it finds', () async { |
| const String text = 'test with\n\nMore stuff right here.\nmultiple blocks'; |
| const int offset = 22; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'test with\n\nright here.\nmultiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 11); |
| }); |
| |
| test('when input has obscured text, it should delete everything before the selection', () async { |
| const int offset = 21; |
| const String text = 'test with multiple\n\n words'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| obscureText: true, |
| textSpan: const TextSpan( |
| text: '****', |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'words'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }); |
| }); |
| |
| group('deleteForward', () { |
| test('when as a non-collapsed selection, it should delete a selection', () async { |
| const String text = 'test'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection(baseOffset: 1, extentOffset: 3), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'tt'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 1); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('when includeWhiteSpace is true, it should treat a whiteSpace as a single word', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 9; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'test withmultiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 9); |
| }); |
| |
| test('when at the end of a text, it should be a no-op', () async { |
| const String text = 'test'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 4), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'test'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 4); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 |
| |
| test('when the input has obscured text, it should delete the forward character', () async { |
| const String text = 'test'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| obscureText: true, |
| textSpan: const TextSpan( |
| text: '****', |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'est'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }); |
| |
| test('when using cjk characters', () async { |
| const String text = '用多個塊測試'; |
| const int offset = 0; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, '多個塊測試'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }); |
| |
| test('when using rtl', () async { |
| const String text = 'برنامج أهلا بالعالم'; |
| const int offset = 0; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textDirection: TextDirection.rtl, |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForward(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'رنامج أهلا بالعالم'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, 0); |
| }); |
| }); |
| |
| group('deleteForwardByWord', () { |
| test('when cursor is on the middle of a word, it should delete the next part of the word', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 6; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test w multiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when cursor is before a word, it should delete the whole word', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 10; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test with blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when cursor is preceeded by white spaces, it should delete the spaces and the next word', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 9; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test with blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when cursor is before tabs, it should delete the tabs and the next word', () async { |
| const String text = 'test with\t\t\tmultiple blocks'; |
| const int offset = 9; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test with blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when cursor is followed by break line, it should delete the next word', () async { |
| const String text = 'test with\n\n\nmultiple blocks'; |
| const int offset = 9; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test with blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when using cjk characters', () async { |
| const String text = '用多個塊測試'; |
| const int offset = 0; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, '多個塊測試'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when using rtl', () async { |
| const String text = 'برنامج أهلا بالعالم'; |
| const int offset = 0; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textDirection: TextDirection.rtl, |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, ' أهلا بالعالم'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when input has obscured text, it should delete everything after the selection', () async { |
| const int offset = 4; |
| const String text = 'test'; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| obscureText: true, |
| textSpan: const TextSpan( |
| text: '****', |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard, false); |
| expect(editableTextState.textEditingValue.text, 'test'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| }); |
| |
| group('deleteForwardByLine', () { |
| test('when cursor is on first character of a line, it should delete everything that follows', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 4; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'test'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when cursor is on the middle of a word, it should delete delete everything that follows', () async { |
| const String text = 'test with multiple blocks'; |
| const int offset = 8; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'test wit'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when next character is a breakline, it should preserve it', () async { |
| const String text = 'test with\n\n\nmultiple blocks'; |
| const int offset = 9; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, text); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| |
| test('when text is multiline, it should delete until the first line break it finds', () async { |
| const String text = 'test with\n\nMore stuff right here.\nmultiple blocks'; |
| const int offset = 2; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| textSpan: const TextSpan( |
| text: text, |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'te\n\nMore stuff right here.\nmultiple blocks'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87685 |
| |
| test('when input has obscured text, it should delete everything after the selection', () async { |
| const String text = 'test with multiple\n\n words'; |
| const int offset = 4; |
| final _FakeEditableTextState editableTextState = _FakeEditableTextState( |
| obscureText: true, |
| textSpan: const TextSpan( |
| text: '****', |
| style: TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: text, |
| selection: TextSelection.collapsed(offset: offset), |
| ), |
| ); |
| final RenderEditable editable = editableTextState.renderEditable; |
| |
| layout(editable); |
| editable.hasFocus = true; |
| pumpFrame(); |
| |
| editableTextState.deleteForwardByLine(SelectionChangedCause.keyboard); |
| expect(editableTextState.textEditingValue.text, 'test'); |
| expect(editableTextState.textEditingValue.selection.isCollapsed, true); |
| expect(editableTextState.textEditingValue.selection.baseOffset, offset); |
| }); |
| }); |
| |
| group('delete API implementations', () { |
| // Regression test for: https://github.com/flutter/flutter/issues/80226. |
| // |
| // This textSelectionDelegate has different text and selection from the |
| // render editable. |
| late _FakeEditableTextState delegate; |
| |
| late RenderEditable editable; |
| |
| setUp(() { |
| delegate = _FakeEditableTextState( |
| textSpan: TextSpan( |
| text: 'A ' * 50, |
| style: const TextStyle( |
| height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', |
| ), |
| ), |
| textEditingValue: const TextEditingValue( |
| text: 'BBB', |
| selection: TextSelection.collapsed(offset: 0), |
| ), |
| ); |
| editable = delegate.renderEditable; |
| }); |
| |
| void verifyDoesNotCrashWithInconsistentTextEditingValue(void Function(SelectionChangedCause) method) { |
| editable = RenderEditable( |
| text: TextSpan( |
| text: 'A ' * 50, |
| ), |
| startHandleLayerLink: LayerLink(), |
| endHandleLayerLink: LayerLink(), |
| textDirection: TextDirection.ltr, |
| offset: ViewportOffset.fixed(0), |
| textSelectionDelegate: delegate, |
| selection: const TextSelection(baseOffset: 0, extentOffset: 50), |
| ); |
| |
| layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0))); |
| dynamic error; |
| try { |
| method(SelectionChangedCause.tap); |
| } catch (e) { |
| error = e; |
| } |
| expect(error, isNull); |
| } |
| |
| test('delete is not racy and handles composing region correctly', () { |
| delegate.textEditingValue = const TextEditingValue( |
| text: 'ABCDEF', |
| selection: TextSelection.collapsed(offset: 2), |
| composing: TextRange(start: 1, end: 6), |
| ); |
| verifyDoesNotCrashWithInconsistentTextEditingValue(delegate.delete); |
| final TextEditingValue textEditingValue = editable.textSelectionDelegate.textEditingValue; |
| expect(textEditingValue.text, 'ACDEF'); |
| expect(textEditingValue.selection.isCollapsed, isTrue); |
| expect(textEditingValue.selection.baseOffset, 1); |
| expect(textEditingValue.composing, const TextRange(start: 1, end: 5)); |
| }); |
| |
| test('deleteForward is not racy and handles composing region correctly', () { |
| delegate.textEditingValue = const TextEditingValue( |
| text: 'ABCDEF', |
| selection: TextSelection.collapsed(offset: 2), |
| composing: TextRange(start: 2, end: 6), |
| ); |
| final TextEditingActionTarget target = delegate; |
| verifyDoesNotCrashWithInconsistentTextEditingValue(target.deleteForward); |
| final TextEditingValue textEditingValue = editable.textSelectionDelegate.textEditingValue; |
| expect(textEditingValue.text, 'ABDEF'); |
| expect(textEditingValue.selection.isCollapsed, isTrue); |
| expect(textEditingValue.selection.baseOffset, 2); |
| expect(textEditingValue.composing, const TextRange(start: 2, end: 5)); |
| }); |
| }); |
| |
| group('nextCharacter', () { |
| test('handles normal strings correctly', () { |
| expect(TextEditingActionTarget.nextCharacter(0, '01234567'), 1); |
| expect(TextEditingActionTarget.nextCharacter(3, '01234567'), 4); |
| expect(TextEditingActionTarget.nextCharacter(7, '01234567'), 8); |
| expect(TextEditingActionTarget.nextCharacter(8, '01234567'), 8); |
| }); |
| |
| test('throws for invalid indices', () { |
| expect(() => TextEditingActionTarget.nextCharacter(-1, '01234567'), throwsAssertionError); |
| expect(() => TextEditingActionTarget.nextCharacter(9, '01234567'), throwsAssertionError); |
| }); |
| |
| test('skips spaces in normal strings when includeWhitespace is false', () { |
| expect(TextEditingActionTarget.nextCharacter(3, '0123 5678', false), 5); |
| expect(TextEditingActionTarget.nextCharacter(4, '0123 5678', false), 5); |
| expect(TextEditingActionTarget.nextCharacter(3, '0123 0123', false), 10); |
| expect(TextEditingActionTarget.nextCharacter(2, '0123 0123', false), 3); |
| expect(TextEditingActionTarget.nextCharacter(4, '0123 0123', false), 10); |
| expect(TextEditingActionTarget.nextCharacter(9, '0123 0123', false), 10); |
| expect(TextEditingActionTarget.nextCharacter(10, '0123 0123', false), 11); |
| // If the subsequent characters are all whitespace, it returns the length |
| // of the string. |
| expect(TextEditingActionTarget.nextCharacter(5, '0123 ', false), 10); |
| }); |
| |
| test('handles surrogate pairs correctly', () { |
| expect(TextEditingActionTarget.nextCharacter(3, '0123👨👩👦0123'), 4); |
| expect(TextEditingActionTarget.nextCharacter(4, '0123👨👩👦0123'), 6); |
| expect(TextEditingActionTarget.nextCharacter(5, '0123👨👩👦0123'), 6); |
| expect(TextEditingActionTarget.nextCharacter(6, '0123👨👩👦0123'), 8); |
| expect(TextEditingActionTarget.nextCharacter(7, '0123👨👩👦0123'), 8); |
| expect(TextEditingActionTarget.nextCharacter(8, '0123👨👩👦0123'), 10); |
| expect(TextEditingActionTarget.nextCharacter(9, '0123👨👩👦0123'), 10); |
| expect(TextEditingActionTarget.nextCharacter(10, '0123👨👩👦0123'), 11); |
| }); |
| |
| test('handles extended grapheme clusters correctly', () { |
| expect(TextEditingActionTarget.nextCharacter(3, '0123👨👩👦2345'), 4); |
| expect(TextEditingActionTarget.nextCharacter(4, '0123👨👩👦2345'), 12); |
| // Even when extent falls within an extended grapheme cluster, it still |
| // identifies the whole grapheme cluster. |
| expect(TextEditingActionTarget.nextCharacter(5, '0123👨👩👦2345'), 12); |
| expect(TextEditingActionTarget.nextCharacter(12, '0123👨👩👦2345'), 13); |
| }); |
| }); |
| |
| group('previousCharacter', () { |
| test('handles normal strings correctly', () { |
| expect(TextEditingActionTarget.previousCharacter(8, '01234567'), 7); |
| expect(TextEditingActionTarget.previousCharacter(0, '01234567'), 0); |
| expect(TextEditingActionTarget.previousCharacter(1, '01234567'), 0); |
| expect(TextEditingActionTarget.previousCharacter(5, '01234567'), 4); |
| expect(TextEditingActionTarget.previousCharacter(8, '01234567'), 7); |
| }); |
| |
| test('throws for invalid indices', () { |
| expect(() => TextEditingActionTarget.previousCharacter(-1, '01234567'), throwsAssertionError); |
| expect(() => TextEditingActionTarget.previousCharacter(9, '01234567'), throwsAssertionError); |
| }); |
| |
| test('skips spaces in normal strings when includeWhitespace is false', () { |
| expect(TextEditingActionTarget.previousCharacter(5, '0123 0123', false), 3); |
| expect(TextEditingActionTarget.previousCharacter(10, '0123 0123', false), 3); |
| expect(TextEditingActionTarget.previousCharacter(11, '0123 0123', false), 10); |
| expect(TextEditingActionTarget.previousCharacter(9, '0123 0123', false), 3); |
| expect(TextEditingActionTarget.previousCharacter(4, '0123 0123', false), 3); |
| expect(TextEditingActionTarget.previousCharacter(3, '0123 0123', false), 2); |
| // If the previous characters are all whitespace, it returns zero. |
| expect(TextEditingActionTarget.previousCharacter(3, ' 0123', false), 0); |
| }); |
| |
| test('handles surrogate pairs correctly', () { |
| expect(TextEditingActionTarget.previousCharacter(11, '0123👨👩👦0123'), 10); |
| expect(TextEditingActionTarget.previousCharacter(10, '0123👨👩👦0123'), 8); |
| expect(TextEditingActionTarget.previousCharacter(9, '0123👨👩👦0123'), 8); |
| expect(TextEditingActionTarget.previousCharacter(8, '0123👨👩👦0123'), 6); |
| expect(TextEditingActionTarget.previousCharacter(7, '0123👨👩👦0123'), 6); |
| expect(TextEditingActionTarget.previousCharacter(6, '0123👨👩👦0123'), 4); |
| expect(TextEditingActionTarget.previousCharacter(5, '0123👨👩👦0123'), 4); |
| expect(TextEditingActionTarget.previousCharacter(4, '0123👨👩👦0123'), 3); |
| expect(TextEditingActionTarget.previousCharacter(3, '0123👨👩👦0123'), 2); |
| }); |
| |
| test('handles extended grapheme clusters correctly', () { |
| expect(TextEditingActionTarget.previousCharacter(13, '0123👨👩👦2345'), 12); |
| // Even when extent falls within an extended grapheme cluster, it still |
| // identifies the whole grapheme cluster. |
| expect(TextEditingActionTarget.previousCharacter(12, '0123👨👩👦2345'), 4); |
| expect(TextEditingActionTarget.previousCharacter(11, '0123👨👩👦2345'), 4); |
| expect(TextEditingActionTarget.previousCharacter(5, '0123👨👩👦2345'), 4); |
| expect(TextEditingActionTarget.previousCharacter(4, '0123👨👩👦2345'), 3); |
| }); |
| }); |
| } |