From 91bb74a2837a7aee24478d5a5cd9b4b3f062a279 Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Fri, 2 Sep 2022 00:30:01 +0430 Subject: [PATCH 01/12] Updated RawEditorStateTextInputClientMixin to implement DeltaTextInputClient --- .../widgets/editor_input_client_mixin.dart | 71 +++++++------------ 1 file changed, 24 insertions(+), 47 deletions(-) diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index 0834fc59..b8444212 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -1,16 +1,16 @@ import 'dart:ui' as ui; -import 'package:fleather/util.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:quill_delta/quill_delta.dart'; import '../rendering/editor.dart'; import 'editor.dart'; mixin RawEditorStateTextInputClientMixin on EditorState - implements TextInputClient { + implements DeltaTextInputClient { TextInputConnection? _textInputConnection; TextEditingValue? _lastKnownRemoteTextEditingValue; @@ -29,12 +29,6 @@ mixin RawEditorStateTextInputClientMixin on EditorState /// - Changing the selection using a physical keyboard. bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; - void _remoteValueChanged( - int start, String deleted, String inserted, TextSelection selection) { - widget.controller - .replaceText(start, deleted.length, inserted, selection: selection); - } - /// Returns `true` if there is open input connection. bool get hasConnection => _textInputConnection != null && _textInputConnection!.attached; @@ -64,6 +58,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState readOnly: widget.readOnly, obscureText: false, autocorrect: false, + enableDeltaModel: true, inputAction: TextInputAction.newline, keyboardAppearance: widget.keyboardAppearance, textCapitalization: widget.textCapitalization, @@ -120,51 +115,33 @@ mixin RawEditorStateTextInputClientMixin on EditorState AutofillScope? get currentAutofillScope => null; @override - void updateEditingValue(TextEditingValue value) { + void updateEditingValueWithDeltas(List textEditingDeltas) { if (!shouldCreateInputConnection) { return; } - if (_lastKnownRemoteTextEditingValue == value) { - // There is no difference between this value and the last known value. - return; - } - - // Check if only composing range changed. - if (_lastKnownRemoteTextEditingValue!.text == value.text && - _lastKnownRemoteTextEditingValue!.selection == value.selection) { - // This update only modifies composing range. Since we don't keep track - // of composing range in Zefyr we just need to update last known value - // here. - // This check fixes an issue on Android when it sends - // composing updates separately from regular changes for text and - // selection. - _lastKnownRemoteTextEditingValue = value; - return; + for (final textEditingDelta in textEditingDeltas) { + final delta = Delta(); + if (textEditingDelta is TextEditingDeltaInsertion) { + delta.retain(textEditingDelta.insertionOffset); + delta.insert(textEditingDelta.textInserted); + } else if (textEditingDelta is TextEditingDeltaDeletion) { + delta.retain(textEditingDelta.deletedRange.start); + delta.delete(textEditingDelta.deletedRange.end - + textEditingDelta.deletedRange.start); + } else if (textEditingDelta is TextEditingDeltaReplacement) { + delta.retain(textEditingDelta.replacedRange.start); + delta.insert(textEditingDelta.replacementText); + delta.delete(textEditingDelta.replacedRange.end - + textEditingDelta.replacedRange.start); + } + widget.controller.compose(delta, selection: textEditingDelta.selection); } + } - // Note Flutter (unintentionally?) silences errors occurred during - // text input update, so we have to report it ourselves. - // For more details see https://github.com/flutter/flutter/issues/19191 - // TODO: remove try-catch when/if Flutter stops silencing these errors. - try { - final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; - _lastKnownRemoteTextEditingValue = value; - final oldText = effectiveLastKnownValue.text; - final text = value.text; - final cursorPosition = value.selection.extentOffset; - final diff = fastDiff(oldText, text, cursorPosition); - _remoteValueChanged( - diff.start, diff.deleted, diff.inserted, value.selection); - } catch (e, trace) { - FlutterError.reportError(FlutterErrorDetails( - exception: e, - stack: trace, - library: 'Fleather', - context: ErrorSummary('while updating editing value'), - )); - rethrow; - } + @override + void updateEditingValue(TextEditingValue value) { + // no-op } @override From 39b5cca6634deddee610ac46f4c9c6e6fc290f03 Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Fri, 2 Sep 2022 00:51:18 +0430 Subject: [PATCH 02/12] Updating _lastKnownRemoteTextEditingValue with deltas --- .../src/widgets/editor_input_client_mixin.dart | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index b8444212..6ad66780 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -120,6 +120,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } + var editingValue = _lastKnownRemoteTextEditingValue!; + for (final textEditingDelta in textEditingDeltas) { final delta = Delta(); if (textEditingDelta is TextEditingDeltaInsertion) { @@ -127,16 +129,17 @@ mixin RawEditorStateTextInputClientMixin on EditorState delta.insert(textEditingDelta.textInserted); } else if (textEditingDelta is TextEditingDeltaDeletion) { delta.retain(textEditingDelta.deletedRange.start); - delta.delete(textEditingDelta.deletedRange.end - - textEditingDelta.deletedRange.start); + delta.delete(textEditingDelta.deletedRange.length); } else if (textEditingDelta is TextEditingDeltaReplacement) { delta.retain(textEditingDelta.replacedRange.start); delta.insert(textEditingDelta.replacementText); - delta.delete(textEditingDelta.replacedRange.end - - textEditingDelta.replacedRange.start); + delta.delete(textEditingDelta.replacedRange.length); } widget.controller.compose(delta, selection: textEditingDelta.selection); + editingValue = textEditingDelta.apply(editingValue); } + + _lastKnownRemoteTextEditingValue = editingValue; } @override @@ -295,3 +298,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState } } } + +extension on TextRange { + int get length => end - start; +} From 22d4757f1900c4ca709ca0deda18f2ed74f6837c Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Fri, 2 Sep 2022 01:21:44 +0430 Subject: [PATCH 03/12] Updating _lastKnownRemoteTextEditingValue before document --- .../fleather/lib/src/widgets/editor_input_client_mixin.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index 6ad66780..ca566c2e 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -120,8 +120,6 @@ mixin RawEditorStateTextInputClientMixin on EditorState return; } - var editingValue = _lastKnownRemoteTextEditingValue!; - for (final textEditingDelta in textEditingDeltas) { final delta = Delta(); if (textEditingDelta is TextEditingDeltaInsertion) { @@ -135,11 +133,9 @@ mixin RawEditorStateTextInputClientMixin on EditorState delta.insert(textEditingDelta.replacementText); delta.delete(textEditingDelta.replacedRange.length); } + _lastKnownRemoteTextEditingValue = textEditingDelta.apply(_lastKnownRemoteTextEditingValue!); widget.controller.compose(delta, selection: textEditingDelta.selection); - editingValue = textEditingDelta.apply(editingValue); } - - _lastKnownRemoteTextEditingValue = editingValue; } @override From a3cfc414b93d0fd6d6a6b15591c4f9db98117a8e Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Fri, 2 Sep 2022 02:05:26 +0430 Subject: [PATCH 04/12] Fixed styling issue --- .../widgets/editor_input_client_mixin.dart | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index ca566c2e..7cfa83bc 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:quill_delta/quill_delta.dart'; import '../rendering/editor.dart'; import 'editor.dart'; @@ -121,20 +120,26 @@ mixin RawEditorStateTextInputClientMixin on EditorState } for (final textEditingDelta in textEditingDeltas) { - final delta = Delta(); - if (textEditingDelta is TextEditingDeltaInsertion) { - delta.retain(textEditingDelta.insertionOffset); - delta.insert(textEditingDelta.textInserted); - } else if (textEditingDelta is TextEditingDeltaDeletion) { - delta.retain(textEditingDelta.deletedRange.start); - delta.delete(textEditingDelta.deletedRange.length); - } else if (textEditingDelta is TextEditingDeltaReplacement) { - delta.retain(textEditingDelta.replacedRange.start); - delta.insert(textEditingDelta.replacementText); - delta.delete(textEditingDelta.replacedRange.length); + _lastKnownRemoteTextEditingValue = + textEditingDelta.apply(_lastKnownRemoteTextEditingValue!); + if (textEditingDelta is TextEditingDeltaNonTextUpdate) { + widget.controller.updateSelection(textEditingDelta.selection); + } else { + int start = 0, length = 0; + String data = ''; + if (textEditingDelta is TextEditingDeltaInsertion) { + start = textEditingDelta.insertionOffset; + data = textEditingDelta.textInserted; + } else if (textEditingDelta is TextEditingDeltaDeletion) { + start = textEditingDelta.deletedRange.start; + length = textEditingDelta.deletedRange.length; + } else if (textEditingDelta is TextEditingDeltaReplacement) { + start = textEditingDelta.replacedRange.start; + length = textEditingDelta.replacedRange.length; + data = textEditingDelta.replacementText; + } + widget.controller.replaceText(start, length, data); } - _lastKnownRemoteTextEditingValue = textEditingDelta.apply(_lastKnownRemoteTextEditingValue!); - widget.controller.compose(delta, selection: textEditingDelta.selection); } } From 8d6355ea9896152a857313dbedc4b23c33705a4a Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Fri, 2 Sep 2022 12:03:32 +0430 Subject: [PATCH 05/12] Fixed selection issue --- .../widgets/editor_input_client_mixin.dart | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index 7cfa83bc..8b3dcaf9 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -120,26 +120,23 @@ mixin RawEditorStateTextInputClientMixin on EditorState } for (final textEditingDelta in textEditingDeltas) { + int start = 0, length = 0; + String data = ''; + if (textEditingDelta is TextEditingDeltaInsertion) { + start = textEditingDelta.insertionOffset; + data = textEditingDelta.textInserted; + } else if (textEditingDelta is TextEditingDeltaDeletion) { + start = textEditingDelta.deletedRange.start; + length = textEditingDelta.deletedRange.length; + } else if (textEditingDelta is TextEditingDeltaReplacement) { + start = textEditingDelta.replacedRange.start; + length = textEditingDelta.replacedRange.length; + data = textEditingDelta.replacementText; + } _lastKnownRemoteTextEditingValue = textEditingDelta.apply(_lastKnownRemoteTextEditingValue!); - if (textEditingDelta is TextEditingDeltaNonTextUpdate) { - widget.controller.updateSelection(textEditingDelta.selection); - } else { - int start = 0, length = 0; - String data = ''; - if (textEditingDelta is TextEditingDeltaInsertion) { - start = textEditingDelta.insertionOffset; - data = textEditingDelta.textInserted; - } else if (textEditingDelta is TextEditingDeltaDeletion) { - start = textEditingDelta.deletedRange.start; - length = textEditingDelta.deletedRange.length; - } else if (textEditingDelta is TextEditingDeltaReplacement) { - start = textEditingDelta.replacedRange.start; - length = textEditingDelta.replacedRange.length; - data = textEditingDelta.replacementText; - } - widget.controller.replaceText(start, length, data); - } + widget.controller.replaceText(start, length, data, + selection: textEditingDelta.selection); } } From 6c5fead1de33d9ae47f30cd50a5c6a286ac09e65 Mon Sep 17 00:00:00 2001 From: Alan Mantoux Date: Thu, 8 Sep 2022 12:36:58 +0200 Subject: [PATCH 06/12] Fix tests --- .../test/widgets/editable_text_test.dart | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/packages/fleather/test/widgets/editable_text_test.dart b/packages/fleather/test/widgets/editable_text_test.dart index 2c904ffd..74a7909d 100644 --- a/packages/fleather/test/widgets/editable_text_test.dart +++ b/packages/fleather/test/widgets/editable_text_test.dart @@ -1,6 +1,7 @@ // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,7 +13,7 @@ void main() { final editor = EditorSandBox(tester: tester); await editor.pumpAndTap(); final currentValue = editor.document.toPlainText(); - await enterText(tester, 'Added $currentValue'); + await enterText(tester, 'Added ', oldText: currentValue); expect(editor.document.toPlainText(), 'Added This House Is A Circus\n'); }); @@ -30,14 +31,59 @@ void main() { }); } -Future enterText(WidgetTester tester, String text) async { +Future enterText(WidgetTester tester, String textInserted, + {String oldText = '', int atOffset = 0}) async { return TestAsyncUtils.guard(() async { - tester.testTextInput.updateEditingValue( - TextEditingValue( - text: text, - selection: const TextSelection.collapsed(offset: 6), - ), - ); + updateDeltaEditingValue(TextEditingDeltaInsertion( + oldText: oldText, + textInserted: textInserted, + insertionOffset: atOffset, + selection: const TextSelection.collapsed(offset: 0), + composing: TextRange.empty)); await tester.idle(); }); } + +void updateDeltaEditingValue(TextEditingDelta delta, {int? client}) { + TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage( + SystemChannels.textInput.name, + SystemChannels.textInput.codec.encodeMethodCall( + MethodCall( + 'TextInputClient.updateEditingStateWithDeltas', + [ + client ?? -1, + { + 'deltas': [delta.toJSON()] + } + ], + ), + ), + (ByteData? data) { + /* ignored */ + }, + ); +} + +extension DeltaJson on TextEditingDelta { + Map toJSON() { + final json = {}; + json['composingBase'] = composing.start; + json['composingExtent'] = composing.end; + + json['selectionBase'] = selection.baseOffset; + json['selectionExtent'] = selection.extentOffset; + json['selectionAffinity'] = selection.affinity.name; + json['selectionIsDirectional'] = selection.isDirectional; + + json['oldText'] = oldText; + if (this is TextEditingDeltaInsertion) { + final insertion = this as TextEditingDeltaInsertion; + json['deltaStart'] = insertion.insertionOffset; + // Assumes no replacement, simply insertion here + json['deltaEnd'] = insertion.insertionOffset; + json['deltaText'] = insertion.textInserted; + } + return json; + } +} From a3f62927079b61111dbc04f8d56a54a04329437a Mon Sep 17 00:00:00 2001 From: Alan Mantoux Date: Thu, 8 Sep 2022 14:58:50 +0200 Subject: [PATCH 07/12] More tests --- .../test/widgets/editable_text_test.dart | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/packages/fleather/test/widgets/editable_text_test.dart b/packages/fleather/test/widgets/editable_text_test.dart index 74a7909d..478b2a7b 100644 --- a/packages/fleather/test/widgets/editable_text_test.dart +++ b/packages/fleather/test/widgets/editable_text_test.dart @@ -2,21 +2,39 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../testing.dart'; void main() { group('FleatherEditableText', () { - testWidgets('user input', (tester) async { + testWidgets('user input inserts text', (tester) async { final editor = EditorSandBox(tester: tester); await editor.pumpAndTap(); final currentValue = editor.document.toPlainText(); - await enterText(tester, 'Added ', oldText: currentValue); + await insertText(tester, 'Added ', inText: currentValue); expect(editor.document.toPlainText(), 'Added This House Is A Circus\n'); }); + testWidgets('user input deletes text', (tester) async { + final editor = EditorSandBox(tester: tester); + await editor.pumpAndTap(); + final currentValue = editor.document.toPlainText(); + await deleteText(tester, nbCharacters: 5, inText: currentValue); + expect(editor.document.toPlainText(), 'House Is A Circus\n'); + }); + + testWidgets('user input replaced text', (tester) async { + final editor = EditorSandBox(tester: tester); + await editor.pumpAndTap(); + final currentValue = editor.document.toPlainText(); + await replaceText(tester, + inText: currentValue, + range: const TextRange(start: 5, end: 5 + 'House'.length), + withText: 'Place'); + expect(editor.document.toPlainText(), 'This Place Is A Circus\n'); + }); + testWidgets('autofocus', (tester) async { final editor = EditorSandBox(tester: tester, autofocus: true); await editor.pump(); @@ -31,11 +49,11 @@ void main() { }); } -Future enterText(WidgetTester tester, String textInserted, - {String oldText = '', int atOffset = 0}) async { +Future insertText(WidgetTester tester, String textInserted, + {int atOffset = 0, String inText = ''}) async { return TestAsyncUtils.guard(() async { updateDeltaEditingValue(TextEditingDeltaInsertion( - oldText: oldText, + oldText: inText, textInserted: textInserted, insertionOffset: atOffset, selection: const TextSelection.collapsed(offset: 0), @@ -44,6 +62,33 @@ Future enterText(WidgetTester tester, String textInserted, }); } +Future deleteText(WidgetTester tester, + {required int nbCharacters, int at = 0, required String inText}) { + return TestAsyncUtils.guard(() async { + updateDeltaEditingValue(TextEditingDeltaDeletion( + oldText: inText, + deletedRange: TextRange(start: at, end: at + nbCharacters), + selection: const TextSelection.collapsed(offset: 0), + composing: TextRange.empty)); + await tester.idle(); + }); +} + +Future replaceText(WidgetTester tester, + {required TextRange range, + required String withText, + required String inText}) { + return TestAsyncUtils.guard(() async { + updateDeltaEditingValue(TextEditingDeltaReplacement( + oldText: inText, + replacedRange: range, + replacementText: withText, + selection: const TextSelection.collapsed(offset: 0), + composing: TextRange.empty)); + await tester.idle(); + }); +} + void updateDeltaEditingValue(TextEditingDelta delta, {int? client}) { TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger .handlePlatformMessage( @@ -77,6 +122,7 @@ extension DeltaJson on TextEditingDelta { json['selectionIsDirectional'] = selection.isDirectional; json['oldText'] = oldText; + if (this is TextEditingDeltaInsertion) { final insertion = this as TextEditingDeltaInsertion; json['deltaStart'] = insertion.insertionOffset; @@ -84,6 +130,22 @@ extension DeltaJson on TextEditingDelta { json['deltaEnd'] = insertion.insertionOffset; json['deltaText'] = insertion.textInserted; } + + if (this is TextEditingDeltaDeletion) { + final deletion = this as TextEditingDeltaDeletion; + json['deltaStart'] = deletion.deletedRange.start; + // Assumes no replacement, simply insertion here + json['deltaEnd'] = deletion.deletedRange.end; + json['deltaText'] = ''; + } + + if (this is TextEditingDeltaReplacement) { + final replacement = this as TextEditingDeltaReplacement; + json['deltaStart'] = replacement.replacedRange.start; + // Assumes no replacement, simply insertion here + json['deltaEnd'] = replacement.replacedRange.end; + json['deltaText'] = replacement.replacementText; + } return json; } } From 2cdbf1965c94fd21142a495ee87a57d76f27d2b6 Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Thu, 8 Sep 2022 21:51:31 +0430 Subject: [PATCH 08/12] Add tests for updateEditingValueWithDeltas --- .../widgets/editor_input_client_mixin.dart | 4 +- packages/fleather/pubspec.yaml | 1 + .../test/widgets/editable_text_test.dart | 3 - .../widgets/editor_input_client_mixin.dart | 150 ++++++++++++++++++ 4 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 packages/fleather/test/widgets/editor_input_client_mixin.dart diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index 8b3dcaf9..9a6c5a73 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -115,9 +115,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState @override void updateEditingValueWithDeltas(List textEditingDeltas) { - if (!shouldCreateInputConnection) { - return; - } + if (!shouldCreateInputConnection || textEditingDeltas.isEmpty) return; for (final textEditingDelta in textEditingDeltas) { int start = 0, length = 0; diff --git a/packages/fleather/pubspec.yaml b/packages/fleather/pubspec.yaml index f9c02ccd..83aa91a3 100644 --- a/packages/fleather/pubspec.yaml +++ b/packages/fleather/pubspec.yaml @@ -19,3 +19,4 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.1 + mocktail: ^0.3.0 diff --git a/packages/fleather/test/widgets/editable_text_test.dart b/packages/fleather/test/widgets/editable_text_test.dart index 478b2a7b..18c139e3 100644 --- a/packages/fleather/test/widgets/editable_text_test.dart +++ b/packages/fleather/test/widgets/editable_text_test.dart @@ -1,6 +1,3 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/fleather/test/widgets/editor_input_client_mixin.dart b/packages/fleather/test/widgets/editor_input_client_mixin.dart new file mode 100644 index 00000000..ff671e32 --- /dev/null +++ b/packages/fleather/test/widgets/editor_input_client_mixin.dart @@ -0,0 +1,150 @@ +import 'package:fleather/fleather.dart'; +import 'package:fleather/src/widgets/editor_input_client_mixin.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockRawEditor extends Mock implements RawEditor { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) => + super.toString(); +} + +class MockEditorState extends Mock implements EditorState { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) => + super.toString(); +} + +class MockRawEditorState extends MockEditorState + with RawEditorStateTextInputClientMixin {} + +class MockTextEditingDelta extends Mock implements TextEditingDelta {} + +class MockFleatherController extends Mock implements FleatherController {} + +class MockTextEditingValue extends Mock implements TextEditingValue { + @override + Map toJSON() => {}; +} + +void main() { + late MockRawEditorState editorState; + late RawEditor rawEditor; + late FleatherController controller; + final initialTextEditingValue = MockTextEditingValue(); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + editorState = MockRawEditorState(); + rawEditor = MockRawEditor(); + controller = MockFleatherController(); + when(() => editorState.widget).thenReturn(rawEditor); + when(() => editorState.textEditingValue) + .thenReturn(initialTextEditingValue); + when(() => rawEditor.controller).thenReturn(controller); + when(() => rawEditor.readOnly).thenReturn(false); + when(() => rawEditor.keyboardAppearance).thenReturn(Brightness.light); + when(() => rawEditor.textCapitalization) + .thenReturn(TextCapitalization.none); + when(() => controller.replaceText(any(), any(), any(), + selection: any(named: 'selection'))).thenReturn(null); + }); + + group('updateEditingValueWithDeltas', () { + setUp(() => editorState.openConnectionIfNeeded()); + + test('is readOnly or deltas are empty', () { + when(() => rawEditor.readOnly).thenReturn(true); + editorState.updateEditingValueWithDeltas([MockTextEditingDelta()]); + editorState.updateEditingValueWithDeltas([]); + verifyZeroInteractions(controller); + }); + + test('updated with TextEditingDeltaInsertion', () { + const selection = TextSelection.collapsed(offset: 7); + const delta = TextEditingDeltaInsertion( + oldText: 'Add ', + textInserted: 'Test', + insertionOffset: 4, + selection: selection, + composing: TextRange.empty, + ); + final updatedEditingValue = MockTextEditingValue(); + when(() => initialTextEditingValue.copyWith( + selection: any(named: 'selection'), + text: any(named: 'text'), + composing: any(named: 'composing'), + )).thenReturn(updatedEditingValue); + editorState.updateEditingValueWithDeltas([delta]); + verify(() => controller.replaceText( + delta.insertionOffset, + 0, + delta.textInserted, + selection: selection, + )); + expect(editorState.currentTextEditingValue, updatedEditingValue); + }); + + test('updated with TextEditingDeltaDeletion', () { + const selection = TextSelection.collapsed(offset: 3); + const delta = TextEditingDeltaDeletion( + oldText: 'Test', + deletedRange: TextRange(start: 3, end: 4), + selection: selection, + composing: TextRange.empty, + ); + final updatedEditingValue = MockTextEditingValue(); + when(() => initialTextEditingValue.copyWith( + selection: any(named: 'selection'), + text: any(named: 'text'), + composing: any(named: 'composing'), + )).thenReturn(updatedEditingValue); + editorState.updateEditingValueWithDeltas([delta]); + verify(() => controller.replaceText( + delta.deletedRange.start, + delta.deletedRange.end - delta.deletedRange.start, + '', + selection: selection, + )); + expect(editorState.currentTextEditingValue, updatedEditingValue); + }); + + test('updated with TextEditingDeltaReplacement', () { + const selection = TextSelection.collapsed(offset: 4); + const delta = TextEditingDeltaReplacement( + oldText: 'Test', + replacedRange: TextRange(start: 1, end: 3), + replacementText: 'rea', + selection: selection, + composing: TextRange.empty, + ); + final updatedEditingValue = MockTextEditingValue(); + when(() => initialTextEditingValue.copyWith( + selection: any(named: 'selection'), + text: any(named: 'text'), + composing: any(named: 'composing'), + )).thenReturn(updatedEditingValue); + editorState.updateEditingValueWithDeltas([delta]); + verify(() => controller.replaceText( + delta.replacedRange.start, + delta.replacedRange.end - delta.replacedRange.start, + delta.replacementText, + selection: selection, + )); + expect(editorState.currentTextEditingValue, updatedEditingValue); + }); + + test('updated with TextEditingDeltaNonTextUpdate', () { + const selection = TextSelection.collapsed(offset: 4); + const delta = TextEditingDeltaNonTextUpdate( + oldText: 'Test', selection: selection, composing: TextRange.empty); + final updatedEditingValue = delta.apply(initialTextEditingValue); + editorState.updateEditingValueWithDeltas([delta]); + verify(() => controller.replaceText(0, 0, '', selection: selection)); + expect(editorState.currentTextEditingValue, updatedEditingValue); + }); + }); +} From 5bb82833238de962250946e59393cb160f1b5e72 Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Wed, 25 Jan 2023 18:38:46 +0330 Subject: [PATCH 09/12] Update to support Flutter 3.7 --- .../lib/src/widgets/editor_input_client_mixin.dart | 11 +++++++++++ .../test/widgets/editor_input_client_mixin.dart | 8 ++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index 9a6c5a73..eb8914fa 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -293,6 +293,17 @@ mixin RawEditorStateTextInputClientMixin on EditorState }); } } + + + @override + void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) { + // no-op + } + + @override + void performSelector(String selectorName) { + // no-op + } } extension on TextRange { diff --git a/packages/fleather/test/widgets/editor_input_client_mixin.dart b/packages/fleather/test/widgets/editor_input_client_mixin.dart index ff671e32..1a4a8d6a 100644 --- a/packages/fleather/test/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/test/widgets/editor_input_client_mixin.dart @@ -1,6 +1,5 @@ import 'package:fleather/fleather.dart'; import 'package:fleather/src/widgets/editor_input_client_mixin.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -20,7 +19,12 @@ class MockEditorState extends Mock implements EditorState { class MockRawEditorState extends MockEditorState with RawEditorStateTextInputClientMixin {} -class MockTextEditingDelta extends Mock implements TextEditingDelta {} +class MockTextEditingDelta extends Mock implements TextEditingDelta { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} class MockFleatherController extends Mock implements FleatherController {} From 207269a29a4d66fa1e411ee07a5a513b731ab4bf Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Sat, 28 Jan 2023 16:57:30 +0330 Subject: [PATCH 10/12] Revert "Update to support Flutter 3.7" This reverts commit af51eca2cd6213d90c8b08bc48607511cbe4dd3a. --- .../lib/src/widgets/editor_input_client_mixin.dart | 11 ----------- .../test/widgets/editor_input_client_mixin.dart | 8 ++------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index eb8914fa..9a6c5a73 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -293,17 +293,6 @@ mixin RawEditorStateTextInputClientMixin on EditorState }); } } - - - @override - void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) { - // no-op - } - - @override - void performSelector(String selectorName) { - // no-op - } } extension on TextRange { diff --git a/packages/fleather/test/widgets/editor_input_client_mixin.dart b/packages/fleather/test/widgets/editor_input_client_mixin.dart index 1a4a8d6a..ff671e32 100644 --- a/packages/fleather/test/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/test/widgets/editor_input_client_mixin.dart @@ -1,5 +1,6 @@ import 'package:fleather/fleather.dart'; import 'package:fleather/src/widgets/editor_input_client_mixin.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -19,12 +20,7 @@ class MockEditorState extends Mock implements EditorState { class MockRawEditorState extends MockEditorState with RawEditorStateTextInputClientMixin {} -class MockTextEditingDelta extends Mock implements TextEditingDelta { - @override - String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return super.toString(); - } -} +class MockTextEditingDelta extends Mock implements TextEditingDelta {} class MockFleatherController extends Mock implements FleatherController {} From 9f5cb70f1d405d55e5c3261a40b65649374ab1bb Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Sat, 28 Jan 2023 17:53:17 +0330 Subject: [PATCH 11/12] Fix history tests --- packages/fleather/test/testing.dart | 5 ++++ .../fleather/test/widgets/history_test.dart | 28 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/fleather/test/testing.dart b/packages/fleather/test/testing.dart index ef792945..c3028e5c 100644 --- a/packages/fleather/test/testing.dart +++ b/packages/fleather/test/testing.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:fleather/fleather.dart'; +import 'package:fleather/src/widgets/editor_input_client_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:quill_delta/quill_delta.dart'; @@ -201,3 +202,7 @@ class TestUpdateWidgetState extends State { ], ); } + +RawEditorStateTextInputClientMixin getInputClient() => + (find.byType(RawEditor).evaluate().single as StatefulElement).state + as RawEditorStateTextInputClientMixin; diff --git a/packages/fleather/test/widgets/history_test.dart b/packages/fleather/test/widgets/history_test.dart index 3bffb4a0..087100de 100644 --- a/packages/fleather/test/widgets/history_test.dart +++ b/packages/fleather/test/widgets/history_test.dart @@ -1,7 +1,9 @@ import 'package:fleather/fleather.dart'; +import 'package:fleather/src/widgets/editor_input_client_mixin.dart'; import 'package:fleather/src/widgets/history.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:quill_delta/quill_delta.dart'; @@ -137,9 +139,17 @@ void main() { ..delete(5) ..insert('mmmmm,', {'i': true})); await editor.pump(); - await editor.enterText(const TextEditingValue( - text: 'Something in the way mmmmm,\n', - selection: TextSelection.collapsed(offset: 26))); + final inputClient = getInputClient(); + inputClient.openConnectionIfNeeded(); + inputClient.updateEditingValueWithDeltas([ + const TextEditingDeltaInsertion( + oldText: 'Something in the way mmmmm', + textInserted: ',', + insertionOffset: initialLength, + selection: TextSelection.collapsed(offset: 26), + composing: TextRange.collapsed(26)) + ]); + // Throttle time of 500ms in history await tester.pump(const Duration(milliseconds: 500)); await tester.pumpAndSettle(); @@ -204,6 +214,16 @@ void main() { Future enterText(TextEditingValue text) async { return TestAsyncUtils.guard(() async { await showKeyboard(); + final inputClient = getInputClient(); + inputClient.updateEditingValueWithDeltas([ + TextEditingDeltaInsertion( + oldText: inputClient.textEditingValue.text, + textInserted: text.text, + insertionOffset: 0, + selection: text.selection, + composing: text.composing, + ) + ]); tester.binding.testTextInput.updateEditingValue(text); await tester.idle(); await tester.pumpAndSettle(); @@ -224,7 +244,7 @@ void main() { await tester.tap(find.byType(TextButton)); await enterText(const TextEditingValue( - text: 'Something in the way mmmmm\n', + text: 'Something in the way mmmmm', selection: TextSelection.collapsed(offset: 26))); // Throttle time of 500ms in history await tester.pump(const Duration(milliseconds: 500)); From ac2799047c2ed521b314445d85be555a4babcfeb Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Sat, 28 Jan 2023 17:56:03 +0330 Subject: [PATCH 12/12] Apply analyzer fixes --- packages/fleather/test/widgets/editor_input_client_mixin.dart | 1 - packages/fleather/test/widgets/history_test.dart | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/fleather/test/widgets/editor_input_client_mixin.dart b/packages/fleather/test/widgets/editor_input_client_mixin.dart index ff671e32..42e6775a 100644 --- a/packages/fleather/test/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/test/widgets/editor_input_client_mixin.dart @@ -1,6 +1,5 @@ import 'package:fleather/fleather.dart'; import 'package:fleather/src/widgets/editor_input_client_mixin.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/packages/fleather/test/widgets/history_test.dart b/packages/fleather/test/widgets/history_test.dart index 087100de..77ecb972 100644 --- a/packages/fleather/test/widgets/history_test.dart +++ b/packages/fleather/test/widgets/history_test.dart @@ -1,9 +1,7 @@ import 'package:fleather/fleather.dart'; -import 'package:fleather/src/widgets/editor_input_client_mixin.dart'; import 'package:fleather/src/widgets/history.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:quill_delta/quill_delta.dart';