mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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
885 lines
31 KiB
Dart
885 lines
31 KiB
Dart
// 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,
|
||
);
|
||
});
|
||
});
|
||
}
|