mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Create DeltaTextInputClient (#90205)
* Create DeltaTextInputClient * Remove old tests as updateEditingValueWithDeltas is no longer implemented * fix analyzer * Update docs * Make example more general * Update docs * Add assert to check that TextInputClient is a DeltaTextInputClient * Update assert * More docs * update * Clean up docs * updates * Update docs * updates * Fix test * add test * updates * remove logs * fix tests * Address reviewer comments * Add text_input_utils.dart * Address reviewer comments
This commit is contained in:
parent
b509dc04db
commit
4b330ddbed
@ -35,10 +35,10 @@ String _replace(String originalText, String replacementText, int start, int end)
|
||||
/// * [TextEditingDeltaDeletion], a delta representing a deletion.
|
||||
/// * [TextEditingDeltaReplacement], a delta representing a replacement.
|
||||
/// * [TextEditingDeltaNonTextUpdate], a delta representing an update to the
|
||||
/// selection and/or composing region.
|
||||
/// * [TextInputConfiguration], to opt-in your [TextInputClient] to receive
|
||||
/// [TextEditingDelta]'s you must set [TextInputConfiguration.enableDeltaModel]
|
||||
/// to true.
|
||||
/// selection and/or composing region.
|
||||
/// * [TextInputConfiguration], to opt-in your [DeltaTextInputClient] to receive
|
||||
/// [TextEditingDelta]'s you must set [TextInputConfiguration.enableDeltaModel]
|
||||
/// to true.
|
||||
abstract class TextEditingDelta {
|
||||
/// Creates a delta for a given change to the editing state.
|
||||
///
|
||||
@ -234,9 +234,9 @@ class TextEditingDeltaInsertion extends TextEditingDelta {
|
||||
/// {@template flutter.services.TextEditingDelta.optIn}
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextInputConfiguration], to opt-in your [TextInputClient] to receive
|
||||
/// [TextEditingDelta]'s you must set [TextInputConfiguration.enableDeltaModel]
|
||||
/// to true.
|
||||
/// * [TextInputConfiguration], to opt-in your [DeltaTextInputClient] to receive
|
||||
/// [TextEditingDelta]'s you must set [TextInputConfiguration.enableDeltaModel]
|
||||
/// to true.
|
||||
/// {@endtemplate}
|
||||
const TextEditingDeltaInsertion({
|
||||
required String oldText,
|
||||
|
||||
@ -647,19 +647,24 @@ class TextInputConfiguration {
|
||||
/// Whether to enable that the engine sends text input updates to the
|
||||
/// framework as [TextEditingDelta]'s or as one [TextEditingValue].
|
||||
///
|
||||
/// When this is enabled platform text input updates will
|
||||
/// come through [TextInputClient.updateEditingValueWithDeltas].
|
||||
///
|
||||
/// When this is disabled platform text input updates will come through
|
||||
/// [TextInputClient.updateEditingValue].
|
||||
///
|
||||
/// Enabling this flag results in granular text updates being received from the
|
||||
/// platforms text input control rather than a single new bulk editing state
|
||||
/// given by [TextInputClient.updateEditingValue].
|
||||
/// platform's text input control.
|
||||
///
|
||||
/// If the platform does not currently support the delta model then updates
|
||||
/// for the editing state will continue to come through the
|
||||
/// [TextInputClient.updateEditingValue] channel.
|
||||
/// When this is enabled:
|
||||
/// * You must implement [DeltaTextInputClient] and not [TextInputClient] to
|
||||
/// receive granular updates from the platform's text input.
|
||||
/// * Platform text input updates will come through
|
||||
/// [DeltaTextInputClient.updateEditingValueWithDeltas].
|
||||
/// * If [TextInputClient] is implemented with this property enabled then
|
||||
/// you will experience unexpected behavior as [TextInputClient] does not implement
|
||||
/// a delta channel.
|
||||
///
|
||||
/// When this is disabled:
|
||||
/// * If [DeltaTextInputClient] is implemented then updates for the
|
||||
/// editing state will continue to come through the
|
||||
/// [DeltaTextInputClient.updateEditingValue] channel.
|
||||
/// * If [TextInputClient] is implemented then updates for the editing
|
||||
/// state will come through [TextInputClient.updateEditingValue].
|
||||
///
|
||||
/// Defaults to false. Cannot be null.
|
||||
final bool enableDeltaModel;
|
||||
@ -953,10 +958,15 @@ mixin TextSelectionDelegate {
|
||||
|
||||
/// An interface to receive information from [TextInput].
|
||||
///
|
||||
/// If [TextInputConfiguration.enableDeltaModel] is set to true,
|
||||
/// [DeltaTextInputClient] must be implemented instead of this class.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextInput.attach]
|
||||
/// * [EditableText], a [TextInputClient] implementation.
|
||||
/// * [DeltaTextInputClient], a [TextInputClient] extension that receives
|
||||
/// granular information from the platform's text input.
|
||||
abstract class TextInputClient {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
@ -983,16 +993,6 @@ abstract class TextInputClient {
|
||||
/// formatting.
|
||||
void updateEditingValue(TextEditingValue value);
|
||||
|
||||
/// Requests that this client update its editing state by applying the deltas
|
||||
/// received from the engine.
|
||||
///
|
||||
/// The list of [TextEditingDelta]'s are treated as changes that will be applied
|
||||
/// to the client's editing state. A change is any mutation to the raw text
|
||||
/// value, or any updates to the selection and/or composing region.
|
||||
///
|
||||
/// {@macro flutter.services.TextEditingDelta.optIn}
|
||||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas);
|
||||
|
||||
/// Requests that this client perform the given action.
|
||||
void performAction(TextInputAction action);
|
||||
|
||||
@ -1026,6 +1026,36 @@ abstract class TextInputClient {
|
||||
void connectionClosed();
|
||||
}
|
||||
|
||||
/// An interface to receive granular information from [TextInput].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextInput.attach]
|
||||
/// * [TextInputConfiguration], to opt-in to receive [TextEditingDelta]'s from
|
||||
/// the platforms [TextInput] you must set [TextInputConfiguration.enableDeltaModel]
|
||||
/// to true.
|
||||
abstract class DeltaTextInputClient extends TextInputClient {
|
||||
/// Requests that this client update its editing state by applying the deltas
|
||||
/// received from the engine.
|
||||
///
|
||||
/// The list of [TextEditingDelta]'s are treated as changes that will be applied
|
||||
/// to the client's editing state. A change is any mutation to the raw text
|
||||
/// value, or any updates to the selection and/or composing region.
|
||||
///
|
||||
/// Here is an example of what implementation of this method could look like:
|
||||
/// {@tool snippet}
|
||||
/// @override
|
||||
/// void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
|
||||
/// TextEditingValue newValue = _previousValue;
|
||||
/// for (final TextEditingDelta delta in textEditingDeltas) {
|
||||
/// newValue = delta.apply(newValue);
|
||||
/// }
|
||||
/// _localValue = newValue;
|
||||
/// }
|
||||
/// {@end-tool}
|
||||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas);
|
||||
}
|
||||
|
||||
/// An interface for interacting with a text input control.
|
||||
///
|
||||
/// See also:
|
||||
@ -1485,6 +1515,7 @@ class TextInput {
|
||||
_currentConnection!._client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>));
|
||||
break;
|
||||
case 'TextInputClient.updateEditingStateWithDeltas':
|
||||
assert(_currentConnection!._client is DeltaTextInputClient, 'You must be using a DeltaTextInputClient if TextInputConfiguration.enableDeltaModel is set to true');
|
||||
final List<TextEditingDelta> deltas = <TextEditingDelta>[];
|
||||
|
||||
final Map<String, dynamic> encoded = args[1] as Map<String, dynamic>;
|
||||
@ -1494,7 +1525,7 @@ class TextInput {
|
||||
deltas.add(delta);
|
||||
}
|
||||
|
||||
_currentConnection!._client.updateEditingValueWithDeltas(deltas);
|
||||
(_currentConnection!._client as DeltaTextInputClient).updateEditingValueWithDeltas(deltas);
|
||||
break;
|
||||
case 'TextInputClient.performAction':
|
||||
_currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
|
||||
|
||||
@ -1795,15 +1795,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
@override
|
||||
TextEditingValue get currentTextEditingValue => _value;
|
||||
|
||||
@override
|
||||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
|
||||
TextEditingValue value = _value;
|
||||
for (final TextEditingDelta delta in textEditingDeltas) {
|
||||
value = delta.apply(value);
|
||||
}
|
||||
updateEditingValue(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
// This method handles text editing state updates from the platform text
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:convert' show utf8;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'text_input_utils.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@ -106,19 +106,6 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
|
||||
latestMethodCall = 'updateEditingValue';
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
|
||||
TextEditingValue newEditingValue = currentTextEditingValue;
|
||||
|
||||
for (final TextEditingDelta delta in textEditingDeltas) {
|
||||
newEditingValue = delta.apply(newEditingValue);
|
||||
}
|
||||
|
||||
currentTextEditingValue = newEditingValue;
|
||||
|
||||
latestMethodCall = 'updateEditingValueWithDeltas';
|
||||
}
|
||||
|
||||
@override
|
||||
AutofillScope? currentAutofillScope;
|
||||
|
||||
@ -169,62 +156,3 @@ class FakeAutofillScope with AutofillScopeMixin implements AutofillScope {
|
||||
clients.putIfAbsent(client.autofillId, () => client);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeTextChannel implements MethodChannel {
|
||||
FakeTextChannel(this.outgoing) : assert(outgoing != null);
|
||||
|
||||
Future<dynamic> Function(MethodCall) outgoing;
|
||||
Future<void> Function(MethodCall)? incoming;
|
||||
|
||||
List<MethodCall> outgoingCalls = <MethodCall>[];
|
||||
|
||||
@override
|
||||
BinaryMessenger get binaryMessenger => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
MethodCodec get codec => const JSONMethodCodec();
|
||||
|
||||
@override
|
||||
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
|
||||
final MethodCall call = MethodCall(method, arguments);
|
||||
outgoingCalls.add(call);
|
||||
return await outgoing(call) as T;
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'flutter/textinput';
|
||||
|
||||
@override
|
||||
void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) {
|
||||
incoming = handler;
|
||||
}
|
||||
|
||||
void validateOutgoingMethodCalls(List<MethodCall> calls) {
|
||||
expect(outgoingCalls.length, calls.length);
|
||||
bool hasError = false;
|
||||
for (int i = 0; i < calls.length; i++) {
|
||||
final ByteData outgoingData = codec.encodeMethodCall(outgoingCalls[i]);
|
||||
final ByteData expectedData = codec.encodeMethodCall(calls[i]);
|
||||
final String outgoingString = utf8.decode(outgoingData.buffer.asUint8List());
|
||||
final String expectedString = utf8.decode(expectedData.buffer.asUint8List());
|
||||
|
||||
if (outgoingString != expectedString) {
|
||||
print(
|
||||
'Index $i did not match:\n'
|
||||
' actual: ${outgoingCalls[i]}\n'
|
||||
' expected: ${calls[i]}',
|
||||
);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
if (hasError) {
|
||||
fail('Calls did not match.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
118
packages/flutter/test/services/delta_text_input_test.dart
Normal file
118
packages/flutter/test/services/delta_text_input_test.dart
Normal file
@ -0,0 +1,118 @@
|
||||
// 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 'dart:convert' show jsonDecode;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'text_input_utils.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('DeltaTextInputClient', () {
|
||||
late FakeTextChannel fakeTextChannel;
|
||||
|
||||
setUp(() {
|
||||
fakeTextChannel = FakeTextChannel((MethodCall call) async {});
|
||||
TextInput.setChannel(fakeTextChannel);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TextInputConnection.debugResetId();
|
||||
TextInput.setChannel(SystemChannels.textInput);
|
||||
});
|
||||
|
||||
test(
|
||||
'DeltaTextInputClient send the correct configuration to the platform and responds to updateEditingValueWithDeltas method correctly',
|
||||
() async {
|
||||
// Assemble a TextInputConnection so we can verify its change in state.
|
||||
final FakeDeltaTextInputClient client = FakeDeltaTextInputClient(TextEditingValue.empty);
|
||||
const TextInputConfiguration configuration = TextInputConfiguration(enableDeltaModel: true);
|
||||
TextInput.attach(client, configuration);
|
||||
expect(client.configuration.enableDeltaModel, true);
|
||||
|
||||
expect(client.latestMethodCall, isEmpty);
|
||||
|
||||
const String jsonDelta = '{'
|
||||
'"oldText": "",'
|
||||
' "deltaText": "let there be text",'
|
||||
' "deltaStart": 0,'
|
||||
' "deltaEnd": 0,'
|
||||
' "selectionBase": 17,'
|
||||
' "selectionExtent": 17,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream" ,'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
// Send updateEditingValueWithDeltas message.
|
||||
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
||||
'args': <dynamic>[
|
||||
1,
|
||||
jsonDecode('{"deltas": [$jsonDelta]}'),
|
||||
],
|
||||
'method': 'TextInputClient.updateEditingStateWithDeltas',
|
||||
});
|
||||
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
|
||||
'flutter/textinput',
|
||||
messageBytes,
|
||||
(ByteData? _) {},
|
||||
);
|
||||
|
||||
expect(client.latestMethodCall, 'updateEditingValueWithDeltas');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class FakeDeltaTextInputClient implements DeltaTextInputClient {
|
||||
FakeDeltaTextInputClient(this.currentTextEditingValue);
|
||||
|
||||
String latestMethodCall = '';
|
||||
|
||||
@override
|
||||
TextEditingValue currentTextEditingValue;
|
||||
|
||||
@override
|
||||
AutofillScope? get currentAutofillScope => null;
|
||||
|
||||
@override
|
||||
void performAction(TextInputAction action) {
|
||||
latestMethodCall = 'performAction';
|
||||
}
|
||||
|
||||
@override
|
||||
void performPrivateCommand(String action, Map<String, dynamic> data) {
|
||||
latestMethodCall = 'performPrivateCommand';
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
latestMethodCall = 'updateEditingValue';
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
|
||||
latestMethodCall = 'updateEditingValueWithDeltas';
|
||||
}
|
||||
|
||||
@override
|
||||
void updateFloatingCursor(RawFloatingCursorPoint point) {
|
||||
latestMethodCall = 'updateFloatingCursor';
|
||||
}
|
||||
|
||||
@override
|
||||
void connectionClosed() {
|
||||
latestMethodCall = 'connectionClosed';
|
||||
}
|
||||
|
||||
@override
|
||||
void showAutocorrectionPromptRect(int start, int end) {
|
||||
latestMethodCall = 'showAutocorrectionPromptRect';
|
||||
}
|
||||
|
||||
TextInputConfiguration get configuration => const TextInputConfiguration(enableDeltaModel: true);
|
||||
}
|
||||
@ -3,12 +3,13 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
|
||||
import 'dart:convert' show utf8;
|
||||
import 'dart:convert' show jsonDecode;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'text_input_utils.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@ -451,11 +452,6 @@ class FakeTextInputClient implements TextInputClient {
|
||||
latestMethodCall = 'updateEditingValue';
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
|
||||
latestMethodCall = 'updateEditingValueWithDeltas';
|
||||
}
|
||||
|
||||
@override
|
||||
void updateFloatingCursor(RawFloatingCursorPoint point) {
|
||||
latestMethodCall = 'updateFloatingCursor';
|
||||
@ -473,60 +469,3 @@ class FakeTextInputClient implements TextInputClient {
|
||||
|
||||
TextInputConfiguration get configuration => const TextInputConfiguration();
|
||||
}
|
||||
|
||||
class FakeTextChannel implements MethodChannel {
|
||||
FakeTextChannel(this.outgoing) : assert(outgoing != null);
|
||||
|
||||
Future<dynamic> Function(MethodCall) outgoing;
|
||||
Future<void> Function(MethodCall)? incoming;
|
||||
|
||||
List<MethodCall> outgoingCalls = <MethodCall>[];
|
||||
|
||||
@override
|
||||
BinaryMessenger get binaryMessenger => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
MethodCodec get codec => const JSONMethodCodec();
|
||||
|
||||
@override
|
||||
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
|
||||
final MethodCall call = MethodCall(method, arguments);
|
||||
outgoingCalls.add(call);
|
||||
return await outgoing(call) as T;
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'flutter/textinput';
|
||||
|
||||
@override
|
||||
void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) => incoming = handler;
|
||||
|
||||
void validateOutgoingMethodCalls(List<MethodCall> calls) {
|
||||
expect(outgoingCalls.length, calls.length);
|
||||
bool hasError = false;
|
||||
for (int i = 0; i < calls.length; i++) {
|
||||
final ByteData outgoingData = codec.encodeMethodCall(outgoingCalls[i]);
|
||||
final ByteData expectedData = codec.encodeMethodCall(calls[i]);
|
||||
final String outgoingString = utf8.decode(outgoingData.buffer.asUint8List());
|
||||
final String expectedString = utf8.decode(expectedData.buffer.asUint8List());
|
||||
|
||||
if (outgoingString != expectedString) {
|
||||
print(
|
||||
'Index $i did not match:\n'
|
||||
' actual: $outgoingString\n'
|
||||
' expected: $expectedString',
|
||||
);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
if (hasError) {
|
||||
fail('Calls did not match.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
packages/flutter/test/services/text_input_utils.dart
Normal file
65
packages/flutter/test/services/text_input_utils.dart
Normal file
@ -0,0 +1,65 @@
|
||||
// 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 'dart:convert' show utf8;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class FakeTextChannel implements MethodChannel {
|
||||
FakeTextChannel(this.outgoing) : assert(outgoing != null);
|
||||
|
||||
Future<dynamic> Function(MethodCall) outgoing;
|
||||
Future<void> Function(MethodCall)? incoming;
|
||||
|
||||
List<MethodCall> outgoingCalls = <MethodCall>[];
|
||||
|
||||
@override
|
||||
BinaryMessenger get binaryMessenger => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
MethodCodec get codec => const JSONMethodCodec();
|
||||
|
||||
@override
|
||||
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
|
||||
final MethodCall call = MethodCall(method, arguments);
|
||||
outgoingCalls.add(call);
|
||||
return await outgoing(call) as T;
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'flutter/textinput';
|
||||
|
||||
@override
|
||||
void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) => incoming = handler;
|
||||
|
||||
void validateOutgoingMethodCalls(List<MethodCall> calls) {
|
||||
expect(outgoingCalls.length, calls.length);
|
||||
bool hasError = false;
|
||||
for (int i = 0; i < calls.length; i++) {
|
||||
final ByteData outgoingData = codec.encodeMethodCall(outgoingCalls[i]);
|
||||
final ByteData expectedData = codec.encodeMethodCall(calls[i]);
|
||||
final String outgoingString = utf8.decode(outgoingData.buffer.asUint8List());
|
||||
final String expectedString = utf8.decode(expectedData.buffer.asUint8List());
|
||||
|
||||
if (outgoingString != expectedString) {
|
||||
print(
|
||||
'Index $i did not match:\n'
|
||||
' actual: $outgoingString\n'
|
||||
' expected: $expectedString',
|
||||
);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
if (hasError) {
|
||||
fail('Calls did not match.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:convert' show jsonDecode;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -6619,302 +6618,6 @@ void main() {
|
||||
expect(focusNode.hasFocus, false);
|
||||
});
|
||||
|
||||
group('TextEditingDelta', () {
|
||||
testWidgets('TextEditingDeltaInsertion verification', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: Colors.red,
|
||||
backgroundCursorColor: Colors.red,
|
||||
keyboardType: TextInputType.multiline,
|
||||
onChanged: (String value) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
const String jsonDelta = '{'
|
||||
'"oldText": "",'
|
||||
' "deltaText": "let there be text",'
|
||||
' "deltaStart": 0,'
|
||||
' "deltaEnd": 0,'
|
||||
' "selectionBase": 17,'
|
||||
' "selectionExtent": 17,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream" ,'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
final Map<String, dynamic> test = jsonDecode(jsonDelta) as Map<String, dynamic>;
|
||||
final TextEditingDelta delta = TextEditingDelta.fromJSON(test);
|
||||
expect(delta.runtimeType, TextEditingDeltaInsertion);
|
||||
|
||||
state.updateEditingValueWithDeltas(<TextEditingDelta>[delta]);
|
||||
await tester.pump();
|
||||
expect(controller.text, 'let there be text');
|
||||
expect(controller.selection, delta.selection);
|
||||
expect(state.currentTextEditingValue.composing, delta.composing);
|
||||
});
|
||||
|
||||
testWidgets('TextEditingDeltaDeletion verification', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'let there be text');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: Colors.red,
|
||||
backgroundCursorColor: Colors.red,
|
||||
keyboardType: TextInputType.multiline,
|
||||
onChanged: (String value) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
const String jsonDelta = '{'
|
||||
'"oldText": "let there be text",'
|
||||
' "deltaText": "",'
|
||||
' "deltaStart": 0,'
|
||||
' "deltaEnd": 17,'
|
||||
' "selectionBase": 0,'
|
||||
' "selectionExtent": 0,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream" ,'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
final Map<String, dynamic> test = jsonDecode(jsonDelta) as Map<String, dynamic>;
|
||||
|
||||
final TextEditingDelta delta = TextEditingDelta.fromJSON(test);
|
||||
expect(delta.runtimeType, TextEditingDeltaDeletion);
|
||||
|
||||
state.updateEditingValueWithDeltas(<TextEditingDelta>[delta]);
|
||||
await tester.pump();
|
||||
expect(controller.text, '');
|
||||
expect(controller.selection, delta.selection);
|
||||
expect(state.currentTextEditingValue.composing, delta.composing);
|
||||
});
|
||||
|
||||
testWidgets('TextEditingDeltaReplacement verification', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'let there be text');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: Colors.red,
|
||||
backgroundCursorColor: Colors.red,
|
||||
keyboardType: TextInputType.multiline,
|
||||
onChanged: (String value) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
const String jsonDelta = '{'
|
||||
'"oldText": "let there be text",'
|
||||
' "deltaText": "this is your replacement text",'
|
||||
' "deltaStart": 0,'
|
||||
' "deltaEnd": 17,'
|
||||
' "selectionBase": 0,'
|
||||
' "selectionExtent": 0,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream",'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
final Map<String, dynamic> test = jsonDecode(jsonDelta) as Map<String, dynamic>;
|
||||
|
||||
final TextEditingDelta delta = TextEditingDelta.fromJSON(test);
|
||||
expect(delta.runtimeType, TextEditingDeltaReplacement);
|
||||
|
||||
state.updateEditingValueWithDeltas(<TextEditingDelta>[delta]);
|
||||
await tester.pump();
|
||||
expect(controller.text, 'this is your replacement text');
|
||||
expect(controller.selection, delta.selection);
|
||||
expect(state.currentTextEditingValue.composing, delta.composing);
|
||||
});
|
||||
|
||||
testWidgets('TextEditingDeltaNonTextUpdate verification', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'let there be text');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: Colors.red,
|
||||
backgroundCursorColor: Colors.red,
|
||||
keyboardType: TextInputType.multiline,
|
||||
onChanged: (String value) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
const String jsonDelta = '{'
|
||||
'"oldText": "let there be text",'
|
||||
' "deltaText": "",'
|
||||
' "deltaStart": -1,'
|
||||
' "deltaEnd": -1,'
|
||||
' "selectionBase": 17,'
|
||||
' "selectionExtent": 17,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream",'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
final Map<String, dynamic> test = jsonDecode(jsonDelta) as Map<String, dynamic>;
|
||||
|
||||
final TextEditingDelta delta = TextEditingDelta.fromJSON(test);
|
||||
expect(delta.runtimeType, TextEditingDeltaNonTextUpdate);
|
||||
|
||||
state.updateEditingValueWithDeltas(<TextEditingDelta>[delta]);
|
||||
await tester.pump();
|
||||
expect(controller.text, 'let there be text');
|
||||
expect(controller.selection, delta.selection);
|
||||
expect(state.currentTextEditingValue.composing, delta.composing);
|
||||
});
|
||||
|
||||
testWidgets('TextEditingDelta verify batch deltas apply', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: Colors.red,
|
||||
backgroundCursorColor: Colors.red,
|
||||
keyboardType: TextInputType.multiline,
|
||||
onChanged: (String value) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
const String jsonInsertionDelta = '{'
|
||||
'"oldText": "",'
|
||||
' "deltaText": "let there be text",'
|
||||
' "deltaStart": 0,'
|
||||
' "deltaEnd": 0,'
|
||||
' "selectionBase": 17,'
|
||||
' "selectionExtent": 17,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream" ,'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
const String jsonDeletionDelta = '{'
|
||||
'"oldText": "let there be text",'
|
||||
' "deltaText": "",'
|
||||
' "deltaStart": 12,'
|
||||
' "deltaEnd": 17,'
|
||||
' "selectionBase": 12,'
|
||||
' "selectionExtent": 12,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream" ,'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
const String jsonReplacementDelta = '{'
|
||||
'"oldText": "let there be",'
|
||||
' "deltaText": "b light",'
|
||||
' "deltaStart": 10,'
|
||||
' "deltaEnd": 12,'
|
||||
' "selectionBase": 17,'
|
||||
' "selectionExtent": 17,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream" ,'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
const String jsonNonTextUpdateDelta = '{'
|
||||
'"oldText": "let there b light",'
|
||||
' "deltaText": "",'
|
||||
' "deltaStart": -1,'
|
||||
' "deltaEnd": -1,'
|
||||
' "selectionBase": 17,'
|
||||
' "selectionExtent": 17,'
|
||||
' "selectionAffinity" : "TextAffinity.downstream",'
|
||||
' "selectionIsDirectional": false,'
|
||||
' "composingBase": -1,'
|
||||
' "composingExtent": -1}';
|
||||
|
||||
final TextEditingDelta insertionDelta = TextEditingDelta.fromJSON(jsonDecode(jsonInsertionDelta) as Map<String, dynamic>);
|
||||
final TextEditingDelta deletionDelta = TextEditingDelta.fromJSON(jsonDecode(jsonDeletionDelta) as Map<String, dynamic>);
|
||||
final TextEditingDelta replacementDelta = TextEditingDelta.fromJSON(jsonDecode(jsonReplacementDelta) as Map<String, dynamic>);
|
||||
final TextEditingDelta nonTextUpdateDelta = TextEditingDelta.fromJSON(jsonDecode(jsonNonTextUpdateDelta) as Map<String, dynamic>);
|
||||
expect(insertionDelta.runtimeType, TextEditingDeltaInsertion);
|
||||
expect(deletionDelta.runtimeType, TextEditingDeltaDeletion);
|
||||
expect(replacementDelta.runtimeType, TextEditingDeltaReplacement);
|
||||
expect(nonTextUpdateDelta.runtimeType, TextEditingDeltaNonTextUpdate);
|
||||
|
||||
state.updateEditingValueWithDeltas(<TextEditingDelta>[insertionDelta, deletionDelta, replacementDelta, nonTextUpdateDelta]);
|
||||
await tester.pump();
|
||||
expect(controller.text, 'let there b light');
|
||||
expect(controller.selection, nonTextUpdateDelta.selection);
|
||||
expect(state.currentTextEditingValue.composing, nonTextUpdateDelta.composing);
|
||||
});
|
||||
});
|
||||
|
||||
group('TextEditingController', () {
|
||||
testWidgets('TextEditingController.text set to empty string clears field', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user