flutter_flutter/packages/flutter/test/services/text_formatter_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

885 lines
31 KiB
Dart
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
class TestTextInputFormatter extends TextInputFormatter {
const TestTextInputFormatter();
@override
void noSuchMethod(Invocation invocation) {
super.noSuchMethod(invocation);
}
}
void main() {
TextEditingValue testOldValue = TextEditingValue.empty;
TextEditingValue testNewValue = TextEditingValue.empty;
test('test const constructor', () {
const testValue1 = TestTextInputFormatter();
const testValue2 = TestTextInputFormatter();
expect(testValue1, same(testValue2));
});
test('withFunction wraps formatting function', () {
testOldValue = TextEditingValue.empty;
testNewValue = TextEditingValue.empty;
late TextEditingValue calledOldValue;
late TextEditingValue calledNewValue;
final formatterUnderTest = TextInputFormatter.withFunction((
TextEditingValue oldValue,
TextEditingValue newValue,
) {
calledOldValue = oldValue;
calledNewValue = newValue;
return TextEditingValue.empty;
});
formatterUnderTest.formatEditUpdate(testOldValue, testNewValue);
expect(calledOldValue, equals(testOldValue));
expect(calledNewValue, equals(testNewValue));
});
group('test provided formatters', () {
setUp(() {
// a1b(2c3
// d4)e5f6
// where the parentheses are the selection range.
testNewValue = const TextEditingValue(
text: 'a1b2c3\nd4e5f6',
selection: TextSelection(baseOffset: 3, extentOffset: 9),
);
});
test('test filtering formatter example', () {
const intoTheWoods = TextEditingValue(text: 'Into the Woods');
expect(
FilteringTextInputFormatter(
'o',
allow: true,
replacementString: '*',
).formatEditUpdate(testOldValue, intoTheWoods),
const TextEditingValue(text: '*o*oo*'),
);
expect(
FilteringTextInputFormatter(
'o',
allow: false,
replacementString: '*',
).formatEditUpdate(testOldValue, intoTheWoods),
const TextEditingValue(text: 'Int* the W**ds'),
);
expect(
FilteringTextInputFormatter(
RegExp('o+'),
allow: true,
replacementString: '*',
).formatEditUpdate(testOldValue, intoTheWoods),
const TextEditingValue(text: '*o*oo*'),
);
expect(
FilteringTextInputFormatter(
RegExp('o+'),
allow: false,
replacementString: '*',
).formatEditUpdate(testOldValue, intoTheWoods),
const TextEditingValue(text: 'Int* the W*ds'),
);
// "Into the Wo|ods|"
const selectedIntoTheWoods = TextEditingValue(
text: 'Into the Woods',
selection: TextSelection(baseOffset: 11, extentOffset: 14),
);
expect(
FilteringTextInputFormatter(
'o',
allow: true,
replacementString: '*',
).formatEditUpdate(testOldValue, selectedIntoTheWoods),
const TextEditingValue(
text: '*o*oo*',
selection: TextSelection(baseOffset: 4, extentOffset: 6),
),
);
expect(
FilteringTextInputFormatter(
'o',
allow: false,
replacementString: '*',
).formatEditUpdate(testOldValue, selectedIntoTheWoods),
const TextEditingValue(
text: 'Int* the W**ds',
selection: TextSelection(baseOffset: 11, extentOffset: 14),
),
);
expect(
FilteringTextInputFormatter(
RegExp('o+'),
allow: true,
replacementString: '*',
).formatEditUpdate(testOldValue, selectedIntoTheWoods),
const TextEditingValue(
text: '*o*oo*',
selection: TextSelection(baseOffset: 4, extentOffset: 6),
),
);
expect(
FilteringTextInputFormatter(
RegExp('o+'),
allow: false,
replacementString: '*',
).formatEditUpdate(testOldValue, selectedIntoTheWoods),
const TextEditingValue(
text: 'Int* the W*ds',
selection: TextSelection(baseOffset: 11, extentOffset: 13),
),
);
});
test('test filtering formatter, deny mode', () {
final TextEditingValue actualValue = FilteringTextInputFormatter.deny(
RegExp(r'[a-z]'),
).formatEditUpdate(testOldValue, testNewValue);
// Expecting
// 1(23
// 4)56
expect(
actualValue,
const TextEditingValue(
text: '123\n456',
selection: TextSelection(baseOffset: 1, extentOffset: 5),
),
);
});
test('test filtering formatter, deny mode (deprecated names)', () {
final TextEditingValue actualValue = FilteringTextInputFormatter.deny(
RegExp(r'[a-z]'),
).formatEditUpdate(testOldValue, testNewValue);
// Expecting
// 1(23
// 4)56
expect(
actualValue,
const TextEditingValue(
text: '123\n456',
selection: TextSelection(baseOffset: 1, extentOffset: 5),
),
);
});
test('test single line formatter', () {
final TextEditingValue actualValue = FilteringTextInputFormatter.singleLineFormatter
.formatEditUpdate(testOldValue, testNewValue);
// Expecting
// a1b(2c3d4)e5f6
expect(
actualValue,
const TextEditingValue(
text: 'a1b2c3d4e5f6',
selection: TextSelection(baseOffset: 3, extentOffset: 8),
),
);
});
test('test single line formatter (deprecated names)', () {
final TextEditingValue actualValue = FilteringTextInputFormatter.singleLineFormatter
.formatEditUpdate(testOldValue, testNewValue);
// Expecting
// a1b(2c3d4)e5f6
expect(
actualValue,
const TextEditingValue(
text: 'a1b2c3d4e5f6',
selection: TextSelection(baseOffset: 3, extentOffset: 8),
),
);
});
test('test filtering formatter, allow mode', () {
final TextEditingValue actualValue = FilteringTextInputFormatter.allow(
RegExp(r'[a-c]'),
).formatEditUpdate(testOldValue, testNewValue);
// Expecting
// ab(c)
expect(
actualValue,
const TextEditingValue(
text: 'abc',
selection: TextSelection(baseOffset: 2, extentOffset: 3),
),
);
});
test('test filtering formatter, allow mode (deprecated names)', () {
final TextEditingValue actualValue = FilteringTextInputFormatter.allow(
RegExp(r'[a-c]'),
).formatEditUpdate(testOldValue, testNewValue);
// Expecting
// ab(c)
expect(
actualValue,
const TextEditingValue(
text: 'abc',
selection: TextSelection(baseOffset: 2, extentOffset: 3),
),
);
});
test('test digits only formatter', () {
final TextEditingValue actualValue = FilteringTextInputFormatter.digitsOnly.formatEditUpdate(
testOldValue,
testNewValue,
);
// Expecting
// 1(234)56
expect(
actualValue,
const TextEditingValue(
text: '123456',
selection: TextSelection(baseOffset: 1, extentOffset: 4),
),
);
});
test('test digits only formatter (deprecated names)', () {
final TextEditingValue actualValue = FilteringTextInputFormatter.digitsOnly.formatEditUpdate(
testOldValue,
testNewValue,
);
// Expecting
// 1(234)56
expect(
actualValue,
const TextEditingValue(
text: '123456',
selection: TextSelection(baseOffset: 1, extentOffset: 4),
),
);
});
test('test length limiting formatter', () {
final TextEditingValue actualValue = LengthLimitingTextInputFormatter(
6,
).formatEditUpdate(testOldValue, testNewValue);
// Expecting
// a1b(2c3)
expect(
actualValue,
const TextEditingValue(
text: 'a1b2c3',
selection: TextSelection(baseOffset: 3, extentOffset: 6),
),
);
});
test('test length limiting formatter with zero-length string', () {
testNewValue = const TextEditingValue(
selection: TextSelection(baseOffset: 0, extentOffset: 0),
);
final TextEditingValue actualValue = LengthLimitingTextInputFormatter(
1,
).formatEditUpdate(testOldValue, testNewValue);
// Expecting the empty string.
expect(
actualValue,
const TextEditingValue(selection: TextSelection(baseOffset: 0, extentOffset: 0)),
);
});
test('test length limiting formatter with non-BMP Unicode scalar values', () {
testNewValue = const TextEditingValue(
text: '\u{1f984}\u{1f984}\u{1f984}\u{1f984}', // Unicode U+1f984 (UNICORN FACE)
selection: TextSelection(
// Caret is at the end of the string.
baseOffset: 8,
extentOffset: 8,
),
);
final TextEditingValue actualValue = LengthLimitingTextInputFormatter(
2,
).formatEditUpdate(testOldValue, testNewValue);
// Expecting two characters, with the caret moved to the new end of the
// string.
expect(
actualValue,
const TextEditingValue(
text: '\u{1f984}\u{1f984}',
selection: TextSelection(baseOffset: 4, extentOffset: 4),
),
);
});
test('test length limiting formatter with complex Unicode characters', () {
// TODO(gspencer): Test additional strings. We can do this once the
// formatter supports Unicode grapheme clusters.
//
// A formatter with max length 1 should accept:
// - The '\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}' sequence (flag followed by
// a variation selector, a zero-width joiner, and a rainbow to make a rainbow
// flag).
// - The sequence '\u{0058}\u{0346}\u{0361}\u{035E}\u{032A}\u{031C}\u{0333}\u{0326}\u{031D}\u{0332}'
// (Latin X with many composed characters).
//
// A formatter should not count as a character:
// * The '\u{0000}\u{FEFF}' sequence. (NULL followed by zero-width no-break space).
//
// A formatter with max length 1 should truncate this to one character:
// * The '\u{1F3F3}\u{FE0F}\u{1F308}' sequence (flag with ignored variation
// selector followed by rainbow, should truncate to just flag).
// The U+1F984 U+0020 sequence: Unicorn face followed by a space should
// yield only the unicorn face.
testNewValue = const TextEditingValue(
text: '\u{1F984}\u{0020}',
selection: TextSelection(baseOffset: 1, extentOffset: 1),
);
TextEditingValue actualValue = LengthLimitingTextInputFormatter(
1,
).formatEditUpdate(testOldValue, testNewValue);
expect(
actualValue,
const TextEditingValue(
text: '\u{1F984}',
selection: TextSelection(baseOffset: 1, extentOffset: 1),
),
);
// The U+0058 U+0059 sequence: Latin X followed by Latin Y, should yield
// Latin X.
testNewValue = const TextEditingValue(
text: '\u{0058}\u{0059}',
selection: TextSelection(baseOffset: 1, extentOffset: 1),
);
actualValue = LengthLimitingTextInputFormatter(
1,
).formatEditUpdate(testOldValue, testNewValue);
expect(
actualValue,
const TextEditingValue(
text: '\u{0058}',
selection: TextSelection(baseOffset: 1, extentOffset: 1),
),
);
});
test('test length limiting formatter when selection is off the end', () {
final TextEditingValue actualValue = LengthLimitingTextInputFormatter(
2,
).formatEditUpdate(testOldValue, testNewValue);
// Expecting
// a1()
expect(
actualValue,
const TextEditingValue(
text: 'a1',
selection: TextSelection(baseOffset: 2, extentOffset: 2),
),
);
});
});
group('LengthLimitingTextInputFormatter', () {
group('truncate', () {
test('Removes characters from the end', () async {
const value = TextEditingValue(text: '01234567890');
final TextEditingValue truncated = LengthLimitingTextInputFormatter.truncate(value, 10);
expect(truncated.text, '0123456789');
});
test('Counts surrogate pairs as single characters', () async {
const stringOverflowing = '😆01234567890';
const value = TextEditingValue(
text: stringOverflowing,
// Put the cursor at the end of the overflowing string to test if it
// ends up at the end of the new string after truncation.
selection: TextSelection.collapsed(offset: stringOverflowing.length),
);
final TextEditingValue truncated = LengthLimitingTextInputFormatter.truncate(value, 10);
const stringTruncated = '😆012345678';
expect(truncated.text, stringTruncated);
expect(truncated.selection.baseOffset, stringTruncated.length);
expect(truncated.selection.extentOffset, stringTruncated.length);
});
test('Counts grapheme clusters as single characters', () async {
const stringOverflowing = '👨👩👦01234567890';
const value = TextEditingValue(
text: stringOverflowing,
// Put the cursor at the end of the overflowing string to test if it
// ends up at the end of the new string after truncation.
selection: TextSelection.collapsed(offset: stringOverflowing.length),
);
final TextEditingValue truncated = LengthLimitingTextInputFormatter.truncate(value, 10);
const stringTruncated = '👨👩👦012345678';
expect(truncated.text, stringTruncated);
expect(truncated.selection.baseOffset, stringTruncated.length);
expect(truncated.selection.extentOffset, stringTruncated.length);
});
});
group('formatEditUpdate', () {
const maxLength = 10;
test('Passes through when under limit', () async {
const oldValue = TextEditingValue(text: 'aaa');
const newValue = TextEditingValue(text: 'aaab');
final formatter = LengthLimitingTextInputFormatter(maxLength);
final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
expect(formatted.text, newValue.text);
});
test('Uses old value when at the limit', () async {
const oldValue = TextEditingValue(text: 'aaaaaaaaaa');
const newValue = TextEditingValue(text: 'aaaaabbbbbaaaaa');
final formatter = LengthLimitingTextInputFormatter(maxLength);
final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
expect(formatted.text, oldValue.text);
});
test('Truncates newValue when oldValue already over limit', () async {
const oldValue = TextEditingValue(text: 'aaaaaaaaaaaaaaaaaaaa');
const newValue = TextEditingValue(text: 'bbbbbbbbbbbbbbbbbbbb');
final formatter = LengthLimitingTextInputFormatter(maxLength);
final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
expect(formatted.text, 'bbbbbbbbbb');
});
});
group('get enforcement from target platform', () {
// The enforcement on Web will be always `MaxLengthEnforcement.truncateAfterCompositionEnds`
test('with TargetPlatform.windows', () async {
final MaxLengthEnforcement enforcement =
LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement(TargetPlatform.windows);
if (kIsWeb) {
expect(enforcement, MaxLengthEnforcement.truncateAfterCompositionEnds);
} else {
expect(enforcement, MaxLengthEnforcement.enforced);
}
});
test('with TargetPlatform.macOS', () async {
final MaxLengthEnforcement enforcement =
LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement(TargetPlatform.macOS);
expect(enforcement, MaxLengthEnforcement.truncateAfterCompositionEnds);
});
});
});
test(
'FilteringTextInputFormatter should return the old value if new value contains non-white-listed character',
() {
const oldValue = TextEditingValue(text: '12345');
const newValue = TextEditingValue(text: '12345@');
final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly;
final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
// assert that we are passing digits only at the first time
expect(oldValue.text, equals('12345'));
// The new value is always the oldValue plus a non-digit character (user press @)
expect(newValue.text, equals('12345@'));
// we expect that the formatted value returns the oldValue only since the newValue does not
// satisfy the formatter condition (which is, in this case, digitsOnly)
expect(formatted.text, equals('12345'));
},
);
test('FilteringTextInputFormatter should move the cursor to the right position', () {
TextEditingValue collapsedValue(String text, int offset) => TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
TextEditingValue oldValue = collapsedValue('123', 0);
TextEditingValue newValue = collapsedValue('123456', 6);
final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly;
TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
// assert that we are passing digits only at the first time
expect(oldValue.text, equals('123'));
// assert that we are passing digits only at the second time
expect(newValue.text, equals('123456'));
// assert that cursor is at the end of the text
expect(formatted.selection.baseOffset, equals(6));
// move cursor at the middle of the text and then add the number 9.
oldValue = newValue.copyWith(selection: const TextSelection.collapsed(offset: 4));
newValue = oldValue.copyWith(text: '1239456');
formatted = formatter.formatEditUpdate(oldValue, newValue);
// cursor must be now at fourth position (right after the number 9)
expect(formatted.selection.baseOffset, equals(4));
});
test('FilteringTextInputFormatter should remove non-allowed characters', () {
const oldValue = TextEditingValue(text: '12345');
const newValue = TextEditingValue(text: '12345@');
final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly;
final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
// assert that we are passing digits only at the first time
expect(oldValue.text, equals('12345'));
// The new value is always the oldValue plus a non-digit character (user press @)
expect(newValue.text, equals('12345@'));
// we expect that the formatted value returns the oldValue only since the difference
// between the oldValue and the newValue is only material that isn't allowed
expect(formatted.text, equals('12345'));
});
test(
'WhitelistingTextInputFormatter should return the old value if new value contains non-allowed character',
() {
const oldValue = TextEditingValue(text: '12345');
const newValue = TextEditingValue(text: '12345@');
final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly;
final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
// assert that we are passing digits only at the first time
expect(oldValue.text, equals('12345'));
// The new value is always the oldValue plus a non-digit character (user press @)
expect(newValue.text, equals('12345@'));
// we expect that the formatted value returns the oldValue only since the newValue does not
// satisfy the formatter condition (which is, in this case, digitsOnly)
expect(formatted.text, equals('12345'));
},
);
test('FilteringTextInputFormatter should move the cursor to the right position', () {
TextEditingValue collapsedValue(String text, int offset) => TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
TextEditingValue oldValue = collapsedValue('123', 0);
TextEditingValue newValue = collapsedValue('123456', 6);
final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly;
TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
// assert that we are passing digits only at the first time
expect(oldValue.text, equals('123'));
// assert that we are passing digits only at the second time
expect(newValue.text, equals('123456'));
// assert that cursor is at the end of the text
expect(formatted.selection.baseOffset, equals(6));
// move cursor at the middle of the text and then add the number 9.
oldValue = newValue.copyWith(selection: const TextSelection.collapsed(offset: 4));
newValue = oldValue.copyWith(text: '1239456');
formatted = formatter.formatEditUpdate(oldValue, newValue);
// cursor must be now at fourth position (right after the number 9)
expect(formatted.selection.baseOffset, equals(4));
});
test('WhitelistingTextInputFormatter should move the cursor to the right position', () {
TextEditingValue collapsedValue(String text, int offset) => TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
TextEditingValue oldValue = collapsedValue('123', 0);
TextEditingValue newValue = collapsedValue('123456', 6);
final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly;
TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue);
// assert that we are passing digits only at the first time
expect(oldValue.text, equals('123'));
// assert that we are passing digits only at the second time
expect(newValue.text, equals('123456'));
// assert that cursor is at the end of the text
expect(formatted.selection.baseOffset, equals(6));
// move cursor at the middle of the text and then add the number 9.
oldValue = newValue.copyWith(selection: const TextSelection.collapsed(offset: 4));
newValue = oldValue.copyWith(text: '1239456');
formatted = formatter.formatEditUpdate(oldValue, newValue);
// cursor must be now at fourth position (right after the number 9)
expect(formatted.selection.baseOffset, equals(4));
});
test('FilteringTextInputFormatter should filter independent of selection', () {
// Regression test for https://github.com/flutter/flutter/issues/80842.
final TextInputFormatter formatter = FilteringTextInputFormatter.deny(
'abc',
replacementString: '*',
);
const TextEditingValue oldValue = TextEditingValue.empty;
const newValue = TextEditingValue(text: 'abcabcabc');
final String filteredText = formatter.formatEditUpdate(oldValue, newValue).text;
for (var i = 0; i < newValue.text.length; i += 1) {
final String text = formatter
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: TextSelection.collapsed(offset: i)),
)
.text;
expect(filteredText, text);
}
});
test('FilteringTextInputFormatter should filter independent of composingRegion', () {
final TextInputFormatter formatter = FilteringTextInputFormatter.deny(
'abc',
replacementString: '*',
);
const TextEditingValue oldValue = TextEditingValue.empty;
const newValue = TextEditingValue(text: 'abcabcabc');
final String filteredText = formatter.formatEditUpdate(oldValue, newValue).text;
for (var i = 0; i < newValue.text.length; i += 1) {
final String text = formatter
.formatEditUpdate(oldValue, newValue.copyWith(composing: TextRange.collapsed(i)))
.text;
expect(filteredText, text);
}
});
test('FilteringTextInputFormatter basic filtering test', () {
final filter = RegExp('[A-Za-z0-9.@-]*');
final TextInputFormatter formatter = FilteringTextInputFormatter.allow(filter);
const TextEditingValue oldValue = TextEditingValue.empty;
const newValue = TextEditingValue(text: 'ab&&ca@bcabc');
expect(formatter.formatEditUpdate(oldValue, newValue).text, 'abca@bcabc');
});
group('FilteringTextInputFormatter region', () {
const TextEditingValue oldValue = TextEditingValue.empty;
test('Preserves selection region', () {
const newValue = TextEditingValue(text: 'AAABBBCCC');
// AAA | BBB | CCC => AAA | **** | CCC
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 6, extentOffset: 3)),
)
.selection,
const TextSelection(baseOffset: 7, extentOffset: 3),
);
// AAA | BBB CCC | => AAA | **** CCC |
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 9, extentOffset: 3)),
)
.selection,
const TextSelection(baseOffset: 10, extentOffset: 3),
);
// AAA BBB | CCC | => AAA **** | CCC |
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 9, extentOffset: 6)),
)
.selection,
const TextSelection(baseOffset: 10, extentOffset: 7),
);
// AAAB | B | BCCC => AAA***|CCC
// Same length replacement, keep the selection at where it is.
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '***')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 5, extentOffset: 4)),
)
.selection,
const TextSelection(baseOffset: 5, extentOffset: 4),
);
// AAA | BBB | CCC => AAA | CCC
expect(
FilteringTextInputFormatter.deny('BBB')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 6, extentOffset: 3)),
)
.selection,
const TextSelection(baseOffset: 3, extentOffset: 3),
);
expect(
FilteringTextInputFormatter.deny('BBB')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 6, extentOffset: 3)),
)
.selection,
const TextSelection(baseOffset: 3, extentOffset: 3),
);
// The unfortunate case, we don't know for sure where to put the selection
// so put it after the replacement string.
// AAAB|B|BCCC => AAA****|CCC
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 5, extentOffset: 4)),
)
.selection,
const TextSelection(baseOffset: 7, extentOffset: 7),
);
});
test('Preserves selection region, allow', () {
const newValue = TextEditingValue(text: 'AAABBBCCC');
// AAA | BBB | CCC => **** | BBB | ****
expect(
FilteringTextInputFormatter.allow('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 6, extentOffset: 3)),
)
.selection,
const TextSelection(baseOffset: 7, extentOffset: 4),
);
// | AAABBBCCC | => | ****BBB**** |
expect(
FilteringTextInputFormatter.allow('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 9, extentOffset: 0)),
)
.selection,
const TextSelection(baseOffset: 11, extentOffset: 0),
);
// AAABBB | CCC | => ****BBB | **** |
expect(
FilteringTextInputFormatter.allow('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(selection: const TextSelection(baseOffset: 9, extentOffset: 6)),
)
.selection,
const TextSelection(baseOffset: 11, extentOffset: 7),
);
// Overlapping matches: AAA | BBBBB | CCC => | BBB |
expect(
FilteringTextInputFormatter.allow('BBB')
.formatEditUpdate(
oldValue,
const TextEditingValue(
text: 'AAABBBBBCCC',
selection: TextSelection(baseOffset: 8, extentOffset: 3),
),
)
.selection,
const TextSelection(baseOffset: 3, extentOffset: 0),
);
});
test('Preserves composing region', () {
const newValue = TextEditingValue(text: 'AAABBBCCC');
// AAA | BBB | CCC => AAA | **** | CCC
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(composing: const TextRange(start: 3, end: 6)),
)
.composing,
const TextRange(start: 3, end: 7),
);
// AAA | BBB CCC | => AAA | **** CCC |
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(composing: const TextRange(start: 3, end: 9)),
)
.composing,
const TextRange(start: 3, end: 10),
);
// AAA BBB | CCC | => AAA **** | CCC |
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****')
.formatEditUpdate(
oldValue,
newValue.copyWith(composing: const TextRange(start: 6, end: 9)),
)
.composing,
const TextRange(start: 7, end: 10),
);
// AAAB | B | BCCC => AAA*** | CCC
// Same length replacement, don't move the composing region.
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '***')
.formatEditUpdate(
oldValue,
newValue.copyWith(composing: const TextRange(start: 4, end: 5)),
)
.composing,
const TextRange(start: 4, end: 5),
);
// AAA | BBB | CCC => | AAA CCC
expect(
FilteringTextInputFormatter.deny('BBB')
.formatEditUpdate(
oldValue,
newValue.copyWith(composing: const TextRange(start: 3, end: 6)),
)
.composing,
TextRange.empty,
);
});
});
}