blob: 99363055a73f7f4b5cdcbf53d1769dc1b1c76843 [file] [log] [blame]
// 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';
import 'clipboard_utils.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,
);
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() {
final MockClipboard mockClipboard = MockClipboard();
// Ensure that all TestRenderingFlutterBinding bindings are initialized.
renderer;
setUp(() async {
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
mockClipboard.handleMethodCall,
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
);
});
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));
});
});
test("When a selection is needed but it's invalid, nothing is changed", () 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,
),
);
final RenderEditable editable = editableTextState.renderEditable;
layout(editable);
editable.hasFocus = true;
editableTextState.delete(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteForward(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteForwardByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteToEnd(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteToStart(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.expandSelectionToEnd(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.expandSelectionToStart(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.expandSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.expandSelectionRightByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionDown(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionLeft(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionLeft(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionRight(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionRightByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionLeftByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionRightByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionUp(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionDown(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionLeftByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionRight(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionRightByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionRightByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionUp(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.copySelection(SelectionChangedCause.keyboard);
ClipboardData? clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
expect(clipboardData?.text, null);
editableTextState.cutSelection(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
expect(clipboardData?.text, null);
editableTextState.pasteText(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
});
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);
});
});
}