Assert valid composing TextRange (#60956)

This commit is contained in:
LongCatIsLooong 2020-07-08 21:21:02 -07:00 committed by GitHub
parent c6fd42499a
commit a6ce2d4bc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 9 deletions

View file

@ -714,6 +714,17 @@ class TextEditingValue {
);
}
/// Whether the [composing] range is a valid range within [text].
///
/// Returns true if and only if the [composing] range is normalized, its start
/// is greater than or equal to 0, and its end is less than or equal to
/// [text]'s length.
///
/// If this property is false while the [composing] range's `isValid` is true,
/// it usually indicates the current [composing] range is invalid because of a
/// programming error.
bool get isComposingRangeValid => composing.isValid && composing.isNormalized && composing.end <= text.length;
@override
String toString() => '${objectRuntimeType(this, 'TextEditingValue')}(text: \u2524$text\u251C, selection: $selection, composing: $composing)';

View file

@ -134,7 +134,13 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
/// This constructor treats a null [value] argument as if it were
/// [TextEditingValue.empty].
TextEditingController.fromValue(TextEditingValue value)
: super(value ?? TextEditingValue.empty);
: assert(
value == null || !value.composing.isValid || value.isComposingRangeValid,
'New TextEditingValue $value has an invalid non-empty composing range '
'${value.composing}. It is recommended to use a valid composing range, '
'even for readonly text fields',
),
super(value ?? TextEditingValue.empty);
/// The current string the user is editing.
String get text => value.text;
@ -155,12 +161,27 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
);
}
@override
set value(TextEditingValue newValue) {
assert(
!newValue.composing.isValid || newValue.isComposingRangeValid,
'New TextEditingValue $newValue has an invalid non-empty composing range '
'${newValue.composing}. It is recommended to use a valid composing range, '
'even for readonly text fields',
);
super.value = newValue;
}
/// Builds [TextSpan] from current editing value.
///
/// By default makes text in composing range appear as underlined.
/// Descendants can override this method to customize appearance of text.
/// By default makes text in composing range appear as underlined. Descendants
/// can override this method to customize appearance of text.
TextSpan buildTextSpan({TextStyle style , bool withComposing}) {
if (!value.composing.isValid || !withComposing) {
assert(!value.composing.isValid || !withComposing || value.isComposingRangeValid);
// If the composing range is out of range for the current text, ignore it to
// preserve the tree integrity, otherwise in release mode a RangeError will
// be thrown and this EditableText will be built with a broken subtree.
if (!value.isComposingRangeValid || !withComposing) {
return TextSpan(style: style, text: text);
}
final TextStyle composingStyle = style.merge(

View file

@ -192,6 +192,33 @@ void main() {
expect(client.latestMethodCall, 'showAutocorrectionPromptRect');
});
});
test('TextEditingValue.isComposingRangeValid', () async {
// The composing range is empty.
expect(const TextEditingValue(text: '').isComposingRangeValid, isFalse);
expect(
const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 0)).isComposingRangeValid,
isFalse,
);
// The composing range is out of range for the text.
expect(
const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 5)).isComposingRangeValid,
isFalse,
);
// The composing range is out of range for the text.
expect(
const TextEditingValue(text: 'test', composing: TextRange(start: -1, end: 4)).isComposingRangeValid,
isFalse,
);
expect(
const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 4)).isComposingRangeValid,
isTrue,
);
});
}
class FakeTextInputClient implements TextInputClient {

View file

@ -4700,20 +4700,20 @@ void main() {
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = '';
controller.text = 'test';
await tester.idle();
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
expect(tester.testTextInput.editingState['text'], equals(''));
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(state.wantKeepAlive, true);
expect(formatter.formatCallCount, 0);
state.updateEditingValue(const TextEditingValue(text: ''));
state.updateEditingValue(const TextEditingValue(text: '', composing: TextRange(start: 1, end: 2)));
state.updateEditingValue(const TextEditingValue(text: 'test'));
state.updateEditingValue(const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 2)));
state.updateEditingValue(const TextEditingValue(text: '0')); // pass to formatter once to check the values.
expect(formatter.lastOldValue.composing, const TextRange(start: 1, end: 2));
expect(formatter.lastOldValue.text, '');
expect(formatter.lastOldValue.text, 'test');
});
testWidgets('Whitespace directionality formatter input Arabic', (WidgetTester tester) async {
@ -5170,6 +5170,34 @@ void main() {
expect(renderObject.textHeightBehavior, isNot(equals(inheritedTextHeightBehavior)));
expect(renderObject.textHeightBehavior, equals(customTextHeightBehavior));
});
test('Asserts if composing text is not valid', () async {
void expectToAssert(TextEditingValue value, bool shouldAssert) {
dynamic initException;
dynamic updateException;
controller = TextEditingController();
try {
controller = TextEditingController.fromValue(value);
} catch (e) {
initException = e;
}
controller = TextEditingController();
try {
controller.value = value;
} catch (e) {
updateException = e;
}
expect(initException?.toString(), shouldAssert ? contains('composing range'): isNull);
expect(updateException?.toString(), shouldAssert ? contains('composing range'): isNull);
}
expectToAssert(const TextEditingValue(text: ''), false);
expectToAssert(const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 0)), true);
expectToAssert(const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 9)), true);
expectToAssert(const TextEditingValue(text: 'test', composing: TextRange(start: -1, end: 9)), false);
});
}
class MockTextFormatter extends TextInputFormatter {