diff --git a/packages/flutter/lib/src/services/text_editing_delta.dart b/packages/flutter/lib/src/services/text_editing_delta.dart index 5f6ab98bfed..60e1ff16193 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 @@ 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, diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index 4177892e90c..8b2cbf08550 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart @@ -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 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 textEditingDeltas) { + /// TextEditingValue newValue = _previousValue; + /// for (final TextEditingDelta delta in textEditingDeltas) { + /// newValue = delta.apply(newValue); + /// } + /// _localValue = newValue; + /// } + /// {@end-tool} + void updateEditingValueWithDeltas(List 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)); break; case 'TextInputClient.updateEditingStateWithDeltas': + assert(_currentConnection!._client is DeltaTextInputClient, 'You must be using a DeltaTextInputClient if TextInputConfiguration.enableDeltaModel is set to true'); final List deltas = []; final Map encoded = args[1] as Map; @@ -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)); diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 51fa2fb6cbd..80aaefaf47d 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1795,15 +1795,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien @override TextEditingValue get currentTextEditingValue => _value; - @override - void updateEditingValueWithDeltas(List 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 diff --git a/packages/flutter/test/services/autofill_test.dart b/packages/flutter/test/services/autofill_test.dart index 86094096003..11fdcd93eab 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(); @@ -106,19 +106,6 @@ class FakeAutofillClient implements TextInputClient, AutofillClient { latestMethodCall = 'updateEditingValue'; } - @override - void updateEditingValueWithDeltas(List 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 Function(MethodCall) outgoing; - Future Function(MethodCall)? incoming; - - List outgoingCalls = []; - - @override - BinaryMessenger get binaryMessenger => throw UnimplementedError(); - - @override - MethodCodec get codec => const JSONMethodCodec(); - - @override - Future> invokeListMethod(String method, [dynamic arguments]) => throw UnimplementedError(); - - @override - Future> invokeMapMethod(String method, [dynamic arguments]) => throw UnimplementedError(); - - @override - Future invokeMethod(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 Function(MethodCall call)? handler) { - incoming = handler; - } - - void validateOutgoingMethodCalls(List 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 00000000000..e87a66521f8 --- /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({ + 'args': [ + 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 data) { + latestMethodCall = 'performPrivateCommand'; + } + + @override + void updateEditingValue(TextEditingValue value) { + latestMethodCall = 'updateEditingValue'; + } + + @override + void updateEditingValueWithDeltas(List 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 9bd27e52897..e6efaaff9d4 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(); @@ -451,11 +452,6 @@ class FakeTextInputClient implements TextInputClient { latestMethodCall = 'updateEditingValue'; } - @override - void updateEditingValueWithDeltas(List 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 Function(MethodCall) outgoing; - Future Function(MethodCall)? incoming; - - List outgoingCalls = []; - - @override - BinaryMessenger get binaryMessenger => throw UnimplementedError(); - - @override - MethodCodec get codec => const JSONMethodCodec(); - - @override - Future> invokeListMethod(String method, [dynamic arguments]) => throw UnimplementedError(); - - @override - Future> invokeMapMethod(String method, [dynamic arguments]) => throw UnimplementedError(); - - @override - Future invokeMethod(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 Function(MethodCall call)? handler) => incoming = handler; - - void validateOutgoingMethodCalls(List 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 00000000000..fd58e40548c --- /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 Function(MethodCall) outgoing; + Future Function(MethodCall)? incoming; + + List outgoingCalls = []; + + @override + BinaryMessenger get binaryMessenger => throw UnimplementedError(); + + @override + MethodCodec get codec => const JSONMethodCodec(); + + @override + Future> invokeListMethod(String method, [dynamic arguments]) => throw UnimplementedError(); + + @override + Future> invokeMapMethod(String method, [dynamic arguments]) => throw UnimplementedError(); + + @override + Future invokeMethod(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 Function(MethodCall call)? handler) => incoming = handler; + + void validateOutgoingMethodCalls(List 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 8bf1c863fa4..060d4f20b39 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 @@ 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 test = jsonDecode(jsonDelta) as Map; - final TextEditingDelta delta = TextEditingDelta.fromJSON(test); - expect(delta.runtimeType, TextEditingDeltaInsertion); - - state.updateEditingValueWithDeltas([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 test = jsonDecode(jsonDelta) as Map; - - final TextEditingDelta delta = TextEditingDelta.fromJSON(test); - expect(delta.runtimeType, TextEditingDeltaDeletion); - - state.updateEditingValueWithDeltas([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 test = jsonDecode(jsonDelta) as Map; - - final TextEditingDelta delta = TextEditingDelta.fromJSON(test); - expect(delta.runtimeType, TextEditingDeltaReplacement); - - state.updateEditingValueWithDeltas([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 test = jsonDecode(jsonDelta) as Map; - - final TextEditingDelta delta = TextEditingDelta.fromJSON(test); - expect(delta.runtimeType, TextEditingDeltaNonTextUpdate); - - state.updateEditingValueWithDeltas([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); - final TextEditingDelta deletionDelta = TextEditingDelta.fromJSON(jsonDecode(jsonDeletionDelta) as Map); - final TextEditingDelta replacementDelta = TextEditingDelta.fromJSON(jsonDecode(jsonReplacementDelta) as Map); - final TextEditingDelta nonTextUpdateDelta = TextEditingDelta.fromJSON(jsonDecode(jsonNonTextUpdateDelta) as Map); - expect(insertionDelta.runtimeType, TextEditingDeltaInsertion); - expect(deletionDelta.runtimeType, TextEditingDeltaDeletion); - expect(replacementDelta.runtimeType, TextEditingDeltaReplacement); - expect(nonTextUpdateDelta.runtimeType, TextEditingDeltaNonTextUpdate); - - state.updateEditingValueWithDeltas([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();