mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
854 lines
31 KiB
Dart
854 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';
|
||
|
||
void main() {
|
||
TextEditingValue testOldValue = TextEditingValue.empty;
|
||
TextEditingValue testNewValue = TextEditingValue.empty;
|
||
|
||
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,
|
||
);
|
||
});
|
||
});
|
||
}
|