Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
e0ed12c7
Unverified
Commit
e0ed12c7
authored
Jun 16, 2020
by
Justin McCandless
Committed by
GitHub
Jun 16, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Characters Package (#53381)
parent
c5527dc8
Changes
9
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
737 additions
and
331 deletions
+737
-331
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+3
-2
text_painter.dart
packages/flutter/lib/src/painting/text_painter.dart
+3
-7
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+81
-46
text_formatter.dart
packages/flutter/lib/src/services/text_formatter.dart
+17
-24
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+162
-0
text_painter_test.dart
packages/flutter/test/painting/text_painter_test.dart
+2
-1
editable_test.dart
packages/flutter/test/rendering/editable_test.dart
+194
-0
text_formatter_test.dart
packages/flutter/test/services/text_formatter_test.dart
+275
-0
text_formatter_test.dart
packages/flutter/test/widgets/text_formatter_test.dart
+0
-251
No files found.
packages/flutter/lib/src/material/text_field.dart
View file @
e0ed12c7
...
...
@@ -6,6 +6,7 @@
import
'dart:ui'
as
ui
show
BoxHeightStyle
,
BoxWidthStyle
;
import
'package:characters/characters.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
...
...
@@ -797,7 +798,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
bool
get
_isEnabled
=>
widget
.
enabled
??
widget
.
decoration
?.
enabled
??
true
;
int
get
_currentLength
=>
_effectiveController
.
value
.
text
.
rune
s
.
length
;
int
get
_currentLength
=>
_effectiveController
.
value
.
text
.
character
s
.
length
;
InputDecoration
_getEffectiveDecoration
()
{
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
...
...
@@ -851,7 +852,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
semanticCounterText
=
localizations
.
remainingTextFieldCharacterCount
(
remaining
);
// Handle length exceeds maxLength
if
(
_effectiveController
.
value
.
text
.
rune
s
.
length
>
widget
.
maxLength
)
{
if
(
_effectiveController
.
value
.
text
.
character
s
.
length
>
widget
.
maxLength
)
{
return
effectiveDecoration
.
copyWith
(
errorText:
effectiveDecoration
.
errorText
??
''
,
counterStyle:
effectiveDecoration
.
errorStyle
...
...
packages/flutter/lib/src/painting/text_painter.dart
View file @
e0ed12c7
...
...
@@ -600,7 +600,7 @@ class TextPainter {
// Complex glyphs can be represented by two or more UTF16 codepoints. This
// checks if the value represents a UTF16 glyph by itself or is a 'surrogate'.
bool
_isUtf16Surrogate
(
int
value
)
{
static
bool
_isUtf16Surrogate
(
int
value
)
{
return
value
&
0xF800
==
0xD800
;
}
...
...
@@ -608,7 +608,7 @@ class TextPainter {
// up zero space and do not have valid bounding boxes around them.
//
// We do not directly use the [Unicode] constants since they are strings.
bool
_isUnicodeDirectionality
(
int
value
)
{
static
bool
_isUnicodeDirectionality
(
int
value
)
{
return
value
==
0x200F
||
value
==
0x200E
;
}
...
...
@@ -637,15 +637,13 @@ class TextPainter {
// Get the Rect of the cursor (in logical pixels) based off the near edge
// of the character upstream from the given string offset.
// TODO(garyq): Use actual extended grapheme cluster length instead of
// an increasing cluster length amount to achieve deterministic performance.
Rect
_getRectFromUpstream
(
int
offset
,
Rect
caretPrototype
)
{
final
String
flattenedText
=
_text
.
toPlainText
(
includePlaceholders:
false
);
final
int
prevCodeUnit
=
_text
.
codeUnitAt
(
max
(
0
,
offset
-
1
));
if
(
prevCodeUnit
==
null
)
return
null
;
// Check for multi-code-unit glyphs such as emojis or zero width joiner
// Check for multi-code-unit glyphs such as emojis or zero width joiner
.
final
bool
needsSearch
=
_isUtf16Surrogate
(
prevCodeUnit
)
||
_text
.
codeUnitAt
(
offset
)
==
_zwjUtf16
||
_isUnicodeDirectionality
(
prevCodeUnit
);
int
graphemeClusterLength
=
needsSearch
?
2
:
1
;
List
<
TextBox
>
boxes
=
<
TextBox
>[];
...
...
@@ -688,8 +686,6 @@ class TextPainter {
// Get the Rect of the cursor (in logical pixels) based off the near edge
// of the character downstream from the given string offset.
// TODO(garyq): Use actual extended grapheme cluster length instead of
// an increasing cluster length amount to achieve deterministic performance.
Rect
_getRectFromDownstream
(
int
offset
,
Rect
caretPrototype
)
{
final
String
flattenedText
=
_text
.
toPlainText
(
includePlaceholders:
false
);
// We cap the offset at the final index of the _text.
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
e0ed12c7
This diff is collapsed.
Click to expand it.
packages/flutter/lib/src/services/text_formatter.dart
View file @
e0ed12c7
...
...
@@ -6,6 +6,7 @@
import
'dart:math'
as
math
;
import
'package:characters/characters.dart'
;
import
'package:flutter/foundation.dart'
show
visibleForTesting
;
import
'text_editing.dart'
;
import
'text_input.dart'
;
...
...
@@ -169,24 +170,24 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
/// characters.
final
int
maxLength
;
// TODO(justinmc): This should be updated to use characters instead of runes,
// see the comment in formatEditUpdate.
/// Truncate the given TextEditingValue to maxLength runes.
/// Truncate the given TextEditingValue to maxLength characters.
///
/// See also:
/// * [Dart's characters package](https://pub.dev/packages/characters).
/// * [Dart's documenetation on runes and grapheme clusters](https://dart.dev/guides/language/language-tour#runes-and-grapheme-clusters).
@visibleForTesting
static
TextEditingValue
truncate
(
TextEditingValue
value
,
int
maxLength
)
{
final
TextSelection
newSelection
=
value
.
selection
.
copyWith
(
baseOffset:
math
.
min
(
value
.
selection
.
start
,
maxLength
),
extentOffset:
math
.
min
(
value
.
selection
.
end
,
maxLength
),
);
final
RuneIterator
iterator
=
RuneIterator
(
value
.
text
);
if
(
iterator
.
moveNext
())
for
(
int
count
=
0
;
count
<
maxLength
;
++
count
)
if
(!
iterator
.
moveNext
())
break
;
final
String
truncated
=
value
.
text
.
substring
(
0
,
iterator
.
rawIndex
);
final
CharacterRange
iterator
=
CharacterRange
(
value
.
text
);
if
(
value
.
text
.
characters
.
length
>
maxLength
)
{
iterator
.
expandNext
(
maxLength
);
}
final
String
truncated
=
iterator
.
current
;
return
TextEditingValue
(
text:
truncated
,
selection:
newSelection
,
selection:
value
.
selection
.
copyWith
(
baseOffset:
math
.
min
(
value
.
selection
.
start
,
truncated
.
length
),
extentOffset:
math
.
min
(
value
.
selection
.
end
,
truncated
.
length
),
),
composing:
TextRange
.
empty
,
);
}
...
...
@@ -196,18 +197,10 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
TextEditingValue
oldValue
,
// unused.
TextEditingValue
newValue
,
)
{
// This does not count grapheme clusters (i.e. characters visible to the user),
// it counts Unicode runes, which leaves out a number of useful possible
// characters (like many emoji), so this will be inaccurate in the
// presence of those characters. The Dart lang bug
// https://github.com/dart-lang/sdk/issues/28404 has been filed to
// address this in Dart.
// TODO(justinmc): convert this to count actual characters using Dart's
// characters package (https://pub.dev/packages/characters).
if
(
maxLength
!=
null
&&
maxLength
>
0
&&
newValue
.
text
.
runes
.
length
>
maxLength
)
{
if
(
maxLength
!=
null
&&
maxLength
>
0
&&
newValue
.
text
.
characters
.
length
>
maxLength
)
{
// If already at the maximum and tried to enter even more, keep the old
// value.
if
(
oldValue
.
text
.
rune
s
.
length
==
maxLength
)
{
if
(
oldValue
.
text
.
character
s
.
length
==
maxLength
)
{
return
oldValue
;
}
return
truncate
(
newValue
,
maxLength
);
...
...
packages/flutter/test/material/text_field_test.dart
View file @
e0ed12c7
...
...
@@ -3433,6 +3433,36 @@ void main() {
expect
(
textController
.
text
,
'0123456789'
);
});
testWidgets
(
'maxLength limits input with surrogate pairs.'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
textController
=
TextEditingController
();
await
tester
.
pumpWidget
(
boilerplate
(
child:
TextField
(
controller:
textController
,
maxLength:
10
,
),
));
const
String
surrogatePair
=
'😆'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
surrogatePair
+
'0123456789101112'
);
expect
(
textController
.
text
,
surrogatePair
+
'012345678'
);
});
testWidgets
(
'maxLength limits input with grapheme clusters.'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
textController
=
TextEditingController
();
await
tester
.
pumpWidget
(
boilerplate
(
child:
TextField
(
controller:
textController
,
maxLength:
10
,
),
));
const
String
graphemeCluster
=
'👨👩👦'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
graphemeCluster
+
'0123456789101112'
);
expect
(
textController
.
text
,
graphemeCluster
+
'012345678'
);
});
testWidgets
(
'maxLength limits input in the center of a maxed-out field.'
,
(
WidgetTester
tester
)
async
{
// Regression test for https://github.com/flutter/flutter/issues/37420.
final
TextEditingController
textController
=
TextEditingController
();
...
...
@@ -3539,6 +3569,96 @@ void main() {
expect
(
counterTextWidget
.
style
.
color
,
isNot
(
equals
(
Colors
.
deepPurpleAccent
)));
});
testWidgets
(
'maxLength shows warning when maxLengthEnforced is false with surrogate pairs.'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
textController
=
TextEditingController
();
const
TextStyle
testStyle
=
TextStyle
(
color:
Colors
.
deepPurpleAccent
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
TextField
(
decoration:
const
InputDecoration
(
errorStyle:
testStyle
),
controller:
textController
,
maxLength:
10
,
maxLengthEnforced:
false
,
),
));
await
tester
.
enterText
(
find
.
byType
(
TextField
),
'😆012345678910111'
);
await
tester
.
pump
();
expect
(
textController
.
text
,
'😆012345678910111'
);
expect
(
find
.
text
(
'16/10'
),
findsOneWidget
);
Text
counterTextWidget
=
tester
.
widget
(
find
.
text
(
'16/10'
));
expect
(
counterTextWidget
.
style
.
color
,
equals
(
Colors
.
deepPurpleAccent
));
await
tester
.
enterText
(
find
.
byType
(
TextField
),
'😆012345678'
);
await
tester
.
pump
();
expect
(
textController
.
text
,
'😆012345678'
);
expect
(
find
.
text
(
'10/10'
),
findsOneWidget
);
counterTextWidget
=
tester
.
widget
(
find
.
text
(
'10/10'
));
expect
(
counterTextWidget
.
style
.
color
,
isNot
(
equals
(
Colors
.
deepPurpleAccent
)));
});
testWidgets
(
'maxLength shows warning when maxLengthEnforced is false with grapheme clusters.'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
textController
=
TextEditingController
();
const
TextStyle
testStyle
=
TextStyle
(
color:
Colors
.
deepPurpleAccent
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
TextField
(
decoration:
const
InputDecoration
(
errorStyle:
testStyle
),
controller:
textController
,
maxLength:
10
,
maxLengthEnforced:
false
,
),
));
await
tester
.
enterText
(
find
.
byType
(
TextField
),
'👨👩👦012345678910111'
);
await
tester
.
pump
();
expect
(
textController
.
text
,
'👨👩👦012345678910111'
);
expect
(
find
.
text
(
'16/10'
),
findsOneWidget
);
Text
counterTextWidget
=
tester
.
widget
(
find
.
text
(
'16/10'
));
expect
(
counterTextWidget
.
style
.
color
,
equals
(
Colors
.
deepPurpleAccent
));
await
tester
.
enterText
(
find
.
byType
(
TextField
),
'👨👩👦012345678'
);
await
tester
.
pump
();
expect
(
textController
.
text
,
'👨👩👦012345678'
);
expect
(
find
.
text
(
'10/10'
),
findsOneWidget
);
counterTextWidget
=
tester
.
widget
(
find
.
text
(
'10/10'
));
expect
(
counterTextWidget
.
style
.
color
,
isNot
(
equals
(
Colors
.
deepPurpleAccent
)));
});
testWidgets
(
'maxLength limits input with surrogate pairs.'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
textController
=
TextEditingController
();
await
tester
.
pumpWidget
(
boilerplate
(
child:
TextField
(
controller:
textController
,
maxLength:
10
,
),
));
const
String
surrogatePair
=
'😆'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
surrogatePair
+
'0123456789101112'
);
expect
(
textController
.
text
,
surrogatePair
+
'012345678'
);
});
testWidgets
(
'maxLength limits input with grapheme clusters.'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
textController
=
TextEditingController
();
await
tester
.
pumpWidget
(
boilerplate
(
child:
TextField
(
controller:
textController
,
maxLength:
10
,
),
));
const
String
graphemeCluster
=
'👨👩👦'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
graphemeCluster
+
'0123456789101112'
);
expect
(
textController
.
text
,
graphemeCluster
+
'012345678'
);
});
testWidgets
(
'setting maxLength shows counter'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Material
(
...
...
@@ -3559,6 +3679,48 @@ void main() {
expect
(
find
.
text
(
'5/10'
),
findsOneWidget
);
});
testWidgets
(
'maxLength counter measures surrogate pairs as one character'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Material
(
child:
Center
(
child:
TextField
(
maxLength:
10
,
),
),
),
),
);
expect
(
find
.
text
(
'0/10'
),
findsOneWidget
);
const
String
surrogatePair
=
'😆'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
surrogatePair
);
await
tester
.
pump
();
expect
(
find
.
text
(
'1/10'
),
findsOneWidget
);
});
testWidgets
(
'maxLength counter measures grapheme clusters as one character'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Material
(
child:
Center
(
child:
TextField
(
maxLength:
10
,
),
),
),
),
);
expect
(
find
.
text
(
'0/10'
),
findsOneWidget
);
const
String
familyEmoji
=
'👨👩👦'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
familyEmoji
);
await
tester
.
pump
();
expect
(
find
.
text
(
'1/10'
),
findsOneWidget
);
});
testWidgets
(
'setting maxLength to TextField.noMaxLength shows only entered length'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Material
(
...
...
packages/flutter/test/painting/text_painter_test.dart
View file @
e0ed12c7
...
...
@@ -27,7 +27,8 @@ void main() {
caretOffset
=
painter
.
getOffsetForCaret
(
ui
.
TextPosition
(
offset:
text
.
length
),
ui
.
Rect
.
zero
);
expect
(
caretOffset
.
dx
,
painter
.
width
);
// Check that getOffsetForCaret handles a character that is encoded as a surrogate pair.
// Check that getOffsetForCaret handles a character that is encoded as a
// surrogate pair.
text
=
'A
\
u{1F600}'
;
painter
.
text
=
TextSpan
(
text:
text
);
painter
.
layout
();
...
...
packages/flutter/test/rendering/editable_test.dart
View file @
e0ed12c7
...
...
@@ -750,6 +750,106 @@ void main() {
expect
(
delegate
.
textEditingValue
.
text
,
'est'
);
},
skip:
kIsWeb
);
test
(
'arrow keys and delete handle surrogate pairs correctly'
,
()
async
{
final
TextSelectionDelegate
delegate
=
FakeEditableTextState
();
final
ViewportOffset
viewportOffset
=
ViewportOffset
.
zero
();
TextSelection
currentSelection
;
final
RenderEditable
editable
=
RenderEditable
(
backgroundCursorColor:
Colors
.
grey
,
selectionColor:
Colors
.
black
,
textDirection:
TextDirection
.
ltr
,
cursorColor:
Colors
.
red
,
offset:
viewportOffset
,
textSelectionDelegate:
delegate
,
onSelectionChanged:
(
TextSelection
selection
,
RenderEditable
renderObject
,
SelectionChangedCause
cause
)
{
currentSelection
=
selection
;
},
startHandleLayerLink:
LayerLink
(),
endHandleLayerLink:
LayerLink
(),
text:
const
TextSpan
(
text:
'0123😆6789'
,
style:
TextStyle
(
height:
1.0
,
fontSize:
10.0
,
fontFamily:
'Ahem'
,
),
),
selection:
const
TextSelection
.
collapsed
(
offset:
0
,
),
);
layout
(
editable
);
editable
.
hasFocus
=
true
;
editable
.
selection
=
const
TextSelection
.
collapsed
(
offset:
4
);
pumpFrame
();
await
simulateKeyDownEvent
(
LogicalKeyboardKey
.
arrowRight
,
platform:
'android'
);
await
simulateKeyUpEvent
(
LogicalKeyboardKey
.
arrowRight
,
platform:
'android'
);
expect
(
currentSelection
.
isCollapsed
,
true
);
expect
(
currentSelection
.
baseOffset
,
6
);
editable
.
selection
=
currentSelection
;
await
simulateKeyDownEvent
(
LogicalKeyboardKey
.
arrowLeft
,
platform:
'android'
);
await
simulateKeyUpEvent
(
LogicalKeyboardKey
.
arrowLeft
,
platform:
'android'
);
expect
(
currentSelection
.
isCollapsed
,
true
);
expect
(
currentSelection
.
baseOffset
,
4
);
editable
.
selection
=
currentSelection
;
await
simulateKeyDownEvent
(
LogicalKeyboardKey
.
delete
,
platform:
'android'
);
await
simulateKeyUpEvent
(
LogicalKeyboardKey
.
delete
,
platform:
'android'
);
expect
(
delegate
.
textEditingValue
.
text
,
'01236789'
);
},
skip:
kIsWeb
);
test
(
'arrow keys and delete handle grapheme clusters correctly'
,
()
async
{
final
TextSelectionDelegate
delegate
=
FakeEditableTextState
();
final
ViewportOffset
viewportOffset
=
ViewportOffset
.
zero
();
TextSelection
currentSelection
;
final
RenderEditable
editable
=
RenderEditable
(
backgroundCursorColor:
Colors
.
grey
,
selectionColor:
Colors
.
black
,
textDirection:
TextDirection
.
ltr
,
cursorColor:
Colors
.
red
,
offset:
viewportOffset
,
textSelectionDelegate:
delegate
,
onSelectionChanged:
(
TextSelection
selection
,
RenderEditable
renderObject
,
SelectionChangedCause
cause
)
{
currentSelection
=
selection
;
},
startHandleLayerLink:
LayerLink
(),
endHandleLayerLink:
LayerLink
(),
text:
const
TextSpan
(
text:
'0123👨👩👦2345'
,
style:
TextStyle
(
height:
1.0
,
fontSize:
10.0
,
fontFamily:
'Ahem'
,
),
),
selection:
const
TextSelection
.
collapsed
(
offset:
0
,
),
);
layout
(
editable
);
editable
.
hasFocus
=
true
;
editable
.
selection
=
const
TextSelection
.
collapsed
(
offset:
4
);
pumpFrame
();
await
simulateKeyDownEvent
(
LogicalKeyboardKey
.
arrowRight
,
platform:
'android'
);
await
simulateKeyUpEvent
(
LogicalKeyboardKey
.
arrowRight
,
platform:
'android'
);
expect
(
currentSelection
.
isCollapsed
,
true
);
expect
(
currentSelection
.
baseOffset
,
12
);
editable
.
selection
=
currentSelection
;
await
simulateKeyDownEvent
(
LogicalKeyboardKey
.
arrowLeft
,
platform:
'android'
);
await
simulateKeyUpEvent
(
LogicalKeyboardKey
.
arrowLeft
,
platform:
'android'
);
expect
(
currentSelection
.
isCollapsed
,
true
);
expect
(
currentSelection
.
baseOffset
,
4
);
editable
.
selection
=
currentSelection
;
await
simulateKeyDownEvent
(
LogicalKeyboardKey
.
delete
,
platform:
'android'
);
await
simulateKeyUpEvent
(
LogicalKeyboardKey
.
delete
,
platform:
'android'
);
expect
(
delegate
.
textEditingValue
.
text
,
'01232345'
);
},
skip:
kIsWeb
);
test
(
'arrow keys and delete handle surrogate pairs correctly'
,
()
async
{
final
TextSelectionDelegate
delegate
=
FakeEditableTextState
();
final
ViewportOffset
viewportOffset
=
ViewportOffset
.
zero
();
...
...
@@ -817,4 +917,98 @@ void main() {
const
TextSelection
(
baseOffset:
0
,
extentOffset:
1
));
expect
(
endpoints
[
0
].
point
.
dx
,
0
);
});
group
(
'nextCharacter'
,
()
{
test
(
'handles normal strings correctly'
,
()
{
expect
(
RenderEditable
.
nextCharacter
(
0
,
'01234567'
),
1
);
expect
(
RenderEditable
.
nextCharacter
(
3
,
'01234567'
),
4
);
expect
(
RenderEditable
.
nextCharacter
(
7
,
'01234567'
),
8
);
expect
(
RenderEditable
.
nextCharacter
(
8
,
'01234567'
),
8
);
});
test
(
'throws for invalid indices'
,
()
{
expect
(()
=>
RenderEditable
.
nextCharacter
(-
1
,
'01234567'
),
throwsAssertionError
);
expect
(()
=>
RenderEditable
.
nextCharacter
(
9
,
'01234567'
),
throwsAssertionError
);
});
test
(
'skips spaces in normal strings when includeWhitespace is false'
,
()
{
expect
(
RenderEditable
.
nextCharacter
(
3
,
'0123 5678'
,
false
),
5
);
expect
(
RenderEditable
.
nextCharacter
(
4
,
'0123 5678'
,
false
),
5
);
expect
(
RenderEditable
.
nextCharacter
(
3
,
'0123 0123'
,
false
),
10
);
expect
(
RenderEditable
.
nextCharacter
(
2
,
'0123 0123'
,
false
),
3
);
expect
(
RenderEditable
.
nextCharacter
(
4
,
'0123 0123'
,
false
),
10
);
expect
(
RenderEditable
.
nextCharacter
(
9
,
'0123 0123'
,
false
),
10
);
expect
(
RenderEditable
.
nextCharacter
(
10
,
'0123 0123'
,
false
),
11
);
// If the subsequent characters are all whitespace, it returns the length
// of the string.
expect
(
RenderEditable
.
nextCharacter
(
5
,
'0123 '
,
false
),
10
);
});
test
(
'handles surrogate pairs correctly'
,
()
{
expect
(
RenderEditable
.
nextCharacter
(
3
,
'0123👨👩👦0123'
),
4
);
expect
(
RenderEditable
.
nextCharacter
(
4
,
'0123👨👩👦0123'
),
6
);
expect
(
RenderEditable
.
nextCharacter
(
5
,
'0123👨👩👦0123'
),
6
);
expect
(
RenderEditable
.
nextCharacter
(
6
,
'0123👨👩👦0123'
),
8
);
expect
(
RenderEditable
.
nextCharacter
(
7
,
'0123👨👩👦0123'
),
8
);
expect
(
RenderEditable
.
nextCharacter
(
8
,
'0123👨👩👦0123'
),
10
);
expect
(
RenderEditable
.
nextCharacter
(
9
,
'0123👨👩👦0123'
),
10
);
expect
(
RenderEditable
.
nextCharacter
(
10
,
'0123👨👩👦0123'
),
11
);
});
test
(
'handles extended grapheme clusters correctly'
,
()
{
expect
(
RenderEditable
.
nextCharacter
(
3
,
'0123👨👩👦2345'
),
4
);
expect
(
RenderEditable
.
nextCharacter
(
4
,
'0123👨👩👦2345'
),
12
);
// Even when extent falls within an extended grapheme cluster, it still
// identifies the whole grapheme cluster.
expect
(
RenderEditable
.
nextCharacter
(
5
,
'0123👨👩👦2345'
),
12
);
expect
(
RenderEditable
.
nextCharacter
(
12
,
'0123👨👩👦2345'
),
13
);
});
});
group
(
'previousCharacter'
,
()
{
test
(
'handles normal strings correctly'
,
()
{
expect
(
RenderEditable
.
previousCharacter
(
8
,
'01234567'
),
7
);
expect
(
RenderEditable
.
previousCharacter
(
0
,
'01234567'
),
0
);
expect
(
RenderEditable
.
previousCharacter
(
1
,
'01234567'
),
0
);
expect
(
RenderEditable
.
previousCharacter
(
5
,
'01234567'
),
4
);
expect
(
RenderEditable
.
previousCharacter
(
8
,
'01234567'
),
7
);
});
test
(
'throws for invalid indices'
,
()
{
expect
(()
=>
RenderEditable
.
previousCharacter
(-
1
,
'01234567'
),
throwsAssertionError
);
expect
(()
=>
RenderEditable
.
previousCharacter
(
9
,
'01234567'
),
throwsAssertionError
);
});
test
(
'skips spaces in normal strings when includeWhitespace is false'
,
()
{
expect
(
RenderEditable
.
previousCharacter
(
10
,
'0123 0123'
,
false
),
3
);
expect
(
RenderEditable
.
previousCharacter
(
11
,
'0123 0123'
,
false
),
10
);
expect
(
RenderEditable
.
previousCharacter
(
9
,
'0123 0123'
,
false
),
3
);
expect
(
RenderEditable
.
previousCharacter
(
4
,
'0123 0123'
,
false
),
3
);
expect
(
RenderEditable
.
previousCharacter
(
3
,
'0123 0123'
,
false
),
2
);
// If the previous characters are all whitespace, it returns zero.
expect
(
RenderEditable
.
previousCharacter
(
3
,
' 0123'
,
false
),
0
);
});
test
(
'handles surrogate pairs correctly'
,
()
{
expect
(
RenderEditable
.
previousCharacter
(
11
,
'0123👨👩👦0123'
),
10
);
expect
(
RenderEditable
.
previousCharacter
(
10
,
'0123👨👩👦0123'
),
8
);
expect
(
RenderEditable
.
previousCharacter
(
9
,
'0123👨👩👦0123'
),
8
);
expect
(
RenderEditable
.
previousCharacter
(
8
,
'0123👨👩👦0123'
),
6
);
expect
(
RenderEditable
.
previousCharacter
(
7
,
'0123👨👩👦0123'
),
6
);
expect
(
RenderEditable
.
previousCharacter
(
6
,
'0123👨👩👦0123'
),
4
);
expect
(
RenderEditable
.
previousCharacter
(
5
,
'0123👨👩👦0123'
),
4
);
expect
(
RenderEditable
.
previousCharacter
(
4
,
'0123👨👩👦0123'
),
3
);
expect
(
RenderEditable
.
previousCharacter
(
3
,
'0123👨👩👦0123'
),
2
);
});
test
(
'handles extended grapheme clusters correctly'
,
()
{
expect
(
RenderEditable
.
previousCharacter
(
13
,
'0123👨👩👦2345'
),
12
);
// Even when extent falls within an extended grapheme cluster, it still
// identifies the whole grapheme cluster.
expect
(
RenderEditable
.
previousCharacter
(
12
,
'0123👨👩👦2345'
),
4
);
expect
(
RenderEditable
.
previousCharacter
(
11
,
'0123👨👩👦2345'
),
4
);
expect
(
RenderEditable
.
previousCharacter
(
5
,
'0123👨👩👦2345'
),
4
);
expect
(
RenderEditable
.
previousCharacter
(
4
,
'0123👨👩👦2345'
),
3
);
});
});
}
packages/flutter/test/services/text_formatter_test.dart
View file @
e0ed12c7
...
...
@@ -8,6 +8,247 @@ import 'package:flutter/services.dart';
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
TextEditingValue
testOldValue
;
TextEditingValue
testNewValue
;
test
(
'withFunction wraps formatting function'
,
()
{
testOldValue
=
const
TextEditingValue
();
testNewValue
=
const
TextEditingValue
();
TextEditingValue
calledOldValue
;
TextEditingValue
calledNewValue
;
final
TextInputFormatter
formatterUnderTest
=
TextInputFormatter
.
withFunction
(
(
TextEditingValue
oldValue
,
TextEditingValue
newValue
)
{
calledOldValue
=
oldValue
;
calledNewValue
=
newValue
;
return
null
;
}
);
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
\n
d4e5f6'
,
selection:
TextSelection
(
baseOffset:
3
,
extentOffset:
9
,
),
);
});
test
(
'test blacklisting formatter'
,
()
{
final
TextEditingValue
actualValue
=
BlacklistingTextInputFormatter
(
RegExp
(
r'[a-z]'
))
.
formatEditUpdate
(
testOldValue
,
testNewValue
);
// Expecting
// 1(23
// 4)56
expect
(
actualValue
,
const
TextEditingValue
(
text:
'123
\n
456'
,
selection:
TextSelection
(
baseOffset:
1
,
extentOffset:
5
,
),
));
});
test
(
'test single line formatter'
,
()
{
final
TextEditingValue
actualValue
=
BlacklistingTextInputFormatter
.
singleLineFormatter
.
formatEditUpdate
(
testOldValue
,
testNewValue
);
// Expecting
// a1b(2c3d4)e5f6
expect
(
actualValue
,
const
TextEditingValue
(
text:
'a1b2c3d4e5f6'
,
selection:
TextSelection
(
baseOffset:
3
,
extentOffset:
8
,
),
));
});
test
(
'test whitelisting formatter'
,
()
{
final
TextEditingValue
actualValue
=
WhitelistingTextInputFormatter
(
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
=
WhitelistingTextInputFormatter
.
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
(
text:
''
,
selection:
TextSelection
(
baseOffset:
0
,
extentOffset:
0
,
),
);
final
TextEditingValue
actualValue
=
LengthLimitingTextInputFormatter
(
1
)
.
formatEditUpdate
(
testOldValue
,
testNewValue
);
// Expecting the empty string.
expect
(
actualValue
,
const
TextEditingValue
(
text:
''
,
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
(
// Each character is a surrogate pair and has a length of 2, so the
// full length is 8.
baseOffset:
8
,
extentOffset:
8
,
),
);
final
TextEditingValue
actualValue
=
LengthLimitingTextInputFormatter
(
2
)
.
formatEditUpdate
(
testOldValue
,
testNewValue
);
// Expecting two runes.
expect
(
actualValue
,
const
TextEditingValue
(
text:
'
\
u{1f984}
\
u{1f984}'
,
selection:
TextSelection
(
// The maxLength is set to 2 characters, and since the unicorn face
// emoji is a surrogate pair, the length of the string is 4.
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
{
...
...
@@ -20,6 +261,40 @@ void main() {
.
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
),
composing:
TextRange
.
empty
,
);
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
),
composing:
TextRange
.
empty
,
);
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'
,
()
{
...
...
packages/flutter/test/widgets/text_formatter_test.dart
deleted
100644 → 0
View file @
c5527dc8
// 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.
// @dart = 2.8
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
void
main
(
)
{
TextEditingValue
testOldValue
;
TextEditingValue
testNewValue
;
test
(
'withFunction wraps formatting function'
,
()
{
testOldValue
=
const
TextEditingValue
();
testNewValue
=
const
TextEditingValue
();
TextEditingValue
calledOldValue
;
TextEditingValue
calledNewValue
;
final
TextInputFormatter
formatterUnderTest
=
TextInputFormatter
.
withFunction
(
(
TextEditingValue
oldValue
,
TextEditingValue
newValue
)
{
calledOldValue
=
oldValue
;
calledNewValue
=
newValue
;
return
null
;
}
);
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
\n
d4e5f6'
,
selection:
TextSelection
(
baseOffset:
3
,
extentOffset:
9
,
),
);
});
test
(
'test blacklisting formatter'
,
()
{
final
TextEditingValue
actualValue
=
BlacklistingTextInputFormatter
(
RegExp
(
r'[a-z]'
))
.
formatEditUpdate
(
testOldValue
,
testNewValue
);
// Expecting
// 1(23
// 4)56
expect
(
actualValue
,
const
TextEditingValue
(
text:
'123
\n
456'
,
selection:
TextSelection
(
baseOffset:
1
,
extentOffset:
5
,
),
));
});
test
(
'test single line formatter'
,
()
{
final
TextEditingValue
actualValue
=
BlacklistingTextInputFormatter
.
singleLineFormatter
.
formatEditUpdate
(
testOldValue
,
testNewValue
);
// Expecting
// a1b(2c3d4)e5f6
expect
(
actualValue
,
const
TextEditingValue
(
text:
'a1b2c3d4e5f6'
,
selection:
TextSelection
(
baseOffset:
3
,
extentOffset:
8
,
),
));
});
test
(
'test whitelisting formatter'
,
()
{
final
TextEditingValue
actualValue
=
WhitelistingTextInputFormatter
(
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
=
WhitelistingTextInputFormatter
.
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
(
text:
''
,
selection:
TextSelection
(
baseOffset:
0
,
extentOffset:
0
,
),
);
final
TextEditingValue
actualValue
=
LengthLimitingTextInputFormatter
(
1
)
.
formatEditUpdate
(
testOldValue
,
testNewValue
);
// Expecting the empty string.
expect
(
actualValue
,
const
TextEditingValue
(
text:
''
,
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
(
baseOffset:
4
,
extentOffset:
4
,
),
);
final
TextEditingValue
actualValue
=
LengthLimitingTextInputFormatter
(
2
)
.
formatEditUpdate
(
testOldValue
,
testNewValue
);
// Expecting two runes.
expect
(
actualValue
,
const
TextEditingValue
(
text:
'
\
u{1f984}
\
u{1f984}'
,
selection:
TextSelection
(
baseOffset:
2
,
extentOffset:
2
,
),
));
});
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
,
),
));
});
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment