| // 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 TestTextInputFormatter testValue1 = TestTextInputFormatter(); |
| const TestTextInputFormatter testValue2 = TestTextInputFormatter(); |
| |
| expect(testValue1, same(testValue2)); |
| }); |
| |
| test('withFunction wraps formatting function', () { |
| testOldValue = TextEditingValue.empty; |
| testNewValue = TextEditingValue.empty; |
| |
| late TextEditingValue calledOldValue; |
| late TextEditingValue calledNewValue; |
| |
| final TextInputFormatter 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 TextEditingValue 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 TextEditingValue 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 TextEditingValue value = TextEditingValue( |
| text: '01234567890', |
| ); |
| final TextEditingValue truncated = LengthLimitingTextInputFormatter |
| .truncate(value, 10); |
| expect(truncated.text, '0123456789'); |
| }); |
| |
| test('Counts surrogate pairs as single characters', () async { |
| const String stringOverflowing = '😆01234567890'; |
| const TextEditingValue 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 String stringTruncated = '😆012345678'; |
| expect(truncated.text, stringTruncated); |
| expect(truncated.selection.baseOffset, stringTruncated.length); |
| expect(truncated.selection.extentOffset, stringTruncated.length); |
| }); |
| |
| test('Counts grapheme clustsers as single characters', () async { |
| const String stringOverflowing = '👨👩👦01234567890'; |
| const TextEditingValue 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 String stringTruncated = '👨👩👦012345678'; |
| expect(truncated.text, stringTruncated); |
| expect(truncated.selection.baseOffset, stringTruncated.length); |
| expect(truncated.selection.extentOffset, stringTruncated.length); |
| }); |
| }); |
| |
| group('formatEditUpdate', () { |
| const int maxLength = 10; |
| |
| test('Passes through when under limit', () async { |
| const TextEditingValue oldValue = TextEditingValue( |
| text: 'aaa', |
| ); |
| const TextEditingValue newValue = TextEditingValue( |
| text: 'aaab', |
| ); |
| final LengthLimitingTextInputFormatter 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 TextEditingValue oldValue = TextEditingValue( |
| text: 'aaaaaaaaaa', |
| ); |
| const TextEditingValue newValue = TextEditingValue( |
| text: 'aaaaabbbbbaaaaa', |
| ); |
| final LengthLimitingTextInputFormatter 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 TextEditingValue oldValue = TextEditingValue( |
| text: 'aaaaaaaaaaaaaaaaaaaa', |
| ); |
| const TextEditingValue newValue = TextEditingValue( |
| text: 'bbbbbbbbbbbbbbbbbbbb', |
| ); |
| final LengthLimitingTextInputFormatter 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 TextEditingValue oldValue = TextEditingValue(text: '12345'); |
| const TextEditingValue 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 TextEditingValue oldValue = TextEditingValue(text: '12345'); |
| const TextEditingValue 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 TextEditingValue oldValue = TextEditingValue(text: '12345'); |
| const TextEditingValue 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 TextEditingValue newValue = TextEditingValue(text: 'abcabcabc'); |
| |
| final String filteredText = formatter.formatEditUpdate(oldValue, newValue).text; |
| |
| for (int 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 TextEditingValue newValue = TextEditingValue(text: 'abcabcabc'); |
| |
| final String filteredText = formatter.formatEditUpdate(oldValue, newValue).text; |
| |
| for (int 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 RegExp filter = RegExp('[A-Za-z0-9.@-]*'); |
| final TextInputFormatter formatter = FilteringTextInputFormatter.allow(filter); |
| |
| const TextEditingValue oldValue = TextEditingValue.empty; |
| const TextEditingValue 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 TextEditingValue 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 TextEditingValue 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 TextEditingValue 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, |
| ); |
| }); |
| }); |
| } |