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
diff --git a/packages/flutter/lib/src/services/text_editing_delta.dart b/packages/flutter/lib/src/services/text_editing_delta.dart
index 5f6ab98..60e1ff1 100644
--- a/packages/flutter/lib/src/services/text_editing_delta.dart
+++ b/packages/flutter/lib/src/services/text_editing_delta.dart
@@ -35,10 +35,10 @@
/// * [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 @@
/// {@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,
diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart
index 4177892e..8b2cbf0 100644
--- a/packages/flutter/lib/src/services/text_input.dart
+++ b/packages/flutter/lib/src/services/text_input.dart
@@ -647,19 +647,24 @@
/// 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 @@
/// 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 @@
/// 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 @@
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 @@
_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 @@
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));
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index 51fa2fb..80aaefa 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -1796,15 +1796,6 @@
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
// input plugin. The [EditableText] may not have the focus or an open input
diff --git a/packages/flutter/test/services/autofill_test.dart b/packages/flutter/test/services/autofill_test.dart
index 8609409..11fdcd9 100644
--- a/packages/flutter/test/services/autofill_test.dart
+++ b/packages/flutter/test/services/autofill_test.dart
@@ -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();
@@ -107,19 +107,6 @@
}
@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;
String latestMethodCall = '';
@@ -169,62 +156,3 @@
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.');
- }
- }
-}
diff --git a/packages/flutter/test/services/delta_text_input_test.dart b/packages/flutter/test/services/delta_text_input_test.dart
new file mode 100644
index 0000000..e87a665
--- /dev/null
+++ b/packages/flutter/test/services/delta_text_input_test.dart
@@ -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);
+}
diff --git a/packages/flutter/test/services/text_input_test.dart b/packages/flutter/test/services/text_input_test.dart
index 9bd27e5..e6efaaf 100644
--- a/packages/flutter/test/services/text_input_test.dart
+++ b/packages/flutter/test/services/text_input_test.dart
@@ -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();
@@ -452,11 +453,6 @@
}
@override
- void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
- latestMethodCall = 'updateEditingValueWithDeltas';
- }
-
- @override
void updateFloatingCursor(RawFloatingCursorPoint point) {
latestMethodCall = 'updateFloatingCursor';
}
@@ -473,60 +469,3 @@
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.');
- }
- }
-}
diff --git a/packages/flutter/test/services/text_input_utils.dart b/packages/flutter/test/services/text_input_utils.dart
new file mode 100644
index 0000000..fd58e40
--- /dev/null
+++ b/packages/flutter/test/services/text_input_utils.dart
@@ -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.');
+ }
+ }
+}
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 8bf1c86..060d4f2 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -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 @@
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();