Procházet zdrojové kódy

Merge pull request #857 from LucasXu0/test/internal_key_event_handlers

 implement white_space_handler & update_text_style_by_command_X test
Nathan.fooo před 2 roky
rodič
revize
b3acb4f9e7

+ 24 - 0
frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart

@@ -41,6 +41,30 @@ extension TextNodeExtension on TextNode {
     }
     return true;
   }
+
+  bool allNotSatisfyInSelection(String styleKey, Selection selection) {
+    final ops = delta.whereType<TextInsert>();
+    final startOffset =
+        selection.isBackward ? selection.start.offset : selection.end.offset;
+    final endOffset =
+        selection.isBackward ? selection.end.offset : selection.start.offset;
+    var start = 0;
+    for (final op in ops) {
+      if (start >= endOffset) {
+        break;
+      }
+      final length = op.length;
+      if (start < endOffset && start + length > startOffset) {
+        if (op.attributes != null &&
+            op.attributes!.containsKey(styleKey) &&
+            op.attributes![styleKey] == true) {
+          return false;
+        }
+      }
+      start += length;
+    }
+    return true;
+  }
 }
 
 extension TextNodesExtension on List<TextNode> {

+ 6 - 2
frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart

@@ -72,8 +72,8 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
               GestureDetector(
+                key: iconKey,
                 child: FlowySvg(
-                  key: iconKey,
                   size: Size.square(_iconSize),
                   padding: EdgeInsets.only(
                       top: topPadding, right: _iconRightPadding),
@@ -149,7 +149,11 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
               style: widget.textNode.attributes.check
                   ? span.style?.copyWith(
                       color: Colors.grey,
-                      decoration: TextDecoration.lineThrough,
+                      decoration: TextDecoration.combine([
+                        TextDecoration.lineThrough,
+                        if (span.style?.decoration != null)
+                          span.style!.decoration!
+                      ]),
                     )
                   : span.style,
               recognizer: span.recognizer,

+ 7 - 0
frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart

@@ -9,6 +9,13 @@ import 'package:flowy_editor/src/operation/transaction_builder.dart';
 import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:flowy_editor/src/service/keyboard_service.dart';
 
+@visibleForTesting
+List<String> get checkboxListSymbols => _checkboxListSymbols;
+@visibleForTesting
+List<String> get unCheckboxListSymbols => _unCheckboxListSymbols;
+@visibleForTesting
+List<String> get bulletedListSymbols => _bulletedListSymbols;
+
 const _bulletedListSymbols = ['*', '-'];
 const _checkboxListSymbols = ['[x]', '-[x]'];
 const _unCheckboxListSymbols = ['[]', '-[]'];

+ 8 - 4
frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart

@@ -1,7 +1,4 @@
-import 'package:flowy_editor/src/service/keyboard_service.dart';
-import 'package:flowy_editor/src/service/render_plugin_service.dart';
-import 'package:flowy_editor/src/service/scroll_service.dart';
-import 'package:flowy_editor/src/service/selection_service.dart';
+import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flowy_editor/src/service/toolbar_service.dart';
 import 'package:flutter/material.dart';
 
@@ -26,6 +23,13 @@ class FlowyService {
 
   // input service
   final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service');
+  FlowyInputService? get inputService {
+    if (inputServiceKey.currentState != null &&
+        inputServiceKey.currentState is FlowyInputService) {
+      return inputServiceKey.currentState! as FlowyInputService;
+    }
+    return null;
+  }
 
   // render plugin service
   late FlowyRenderPlugin renderPluginService;

+ 27 - 4
frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart

@@ -47,13 +47,11 @@ class EditorWidgetTester {
     insert(TextNode.empty());
   }
 
-  void insertTextNode(String? text, {Attributes? attributes}) {
+  void insertTextNode(String? text, {Attributes? attributes, Delta? delta}) {
     insert(
       TextNode(
         type: 'text',
-        delta: Delta(
-          [TextInsert(text ?? 'Test')],
-        ),
+        delta: delta ?? Delta([TextInsert(text ?? 'Test')]),
         attributes: attributes,
       ),
     );
@@ -70,6 +68,31 @@ class EditorWidgetTester {
       _editorState.service.selectionService.updateSelection(selection);
     }
     await tester.pumpAndSettle();
+
+    expect(_editorState.service.selectionService.currentSelection.value,
+        selection);
+  }
+
+  Future<void> insertText(TextNode textNode, String text, int offset,
+      {Selection? selection}) async {
+    await apply([
+      TextEditingDeltaInsertion(
+        oldText: textNode.toRawString(),
+        textInserted: text,
+        insertionOffset: offset,
+        selection: selection != null
+            ? TextSelection(
+                baseOffset: selection.start.offset,
+                extentOffset: selection.end.offset)
+            : TextSelection.collapsed(offset: offset),
+        composing: TextRange.empty,
+      )
+    ]);
+  }
+
+  Future<void> apply(List<TextEditingDelta> deltas) async {
+    _editorState.service.inputService?.apply(deltas);
+    await tester.pumpAndSettle();
   }
 
   Future<void> pressLogicKey(

+ 3 - 0
frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart

@@ -79,6 +79,9 @@ extension on LogicalKeyboardKey {
     if (this == LogicalKeyboardKey.enter) {
       return PhysicalKeyboardKey.enter;
     }
+    if (this == LogicalKeyboardKey.space) {
+      return PhysicalKeyboardKey.space;
+    }
     if (this == LogicalKeyboardKey.backspace) {
       return PhysicalKeyboardKey.backspace;
     }

+ 73 - 0
frontend/app_flowy/packages/flowy_editor/test/render/rich_text/checkbox_text_test.dart

@@ -0,0 +1,73 @@
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/src/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/src/extensions/text_node_extensions.dart';
+import 'package:flutter_test/flutter_test.dart';
+import '../../infra/test_editor.dart';
+
+void main() async {
+  setUpAll(() {
+    TestWidgetsFlutterBinding.ensureInitialized();
+  });
+
+  group('delete_text_handler.dart', () {
+    testWidgets('Presses backspace key in empty document', (tester) async {
+      // Before
+      //
+      // [BIUS]Welcome to Appflowy 😁[BIUS]
+      //
+      // After
+      //
+      // [checkbox]Welcome to Appflowy 😁
+      //
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor
+        ..insertTextNode(
+          '',
+          attributes: {
+            StyleKey.subtype: StyleKey.checkbox,
+            StyleKey.checkbox: false,
+          },
+          delta: Delta([
+            TextInsert(text, {
+              StyleKey.bold: true,
+              StyleKey.italic: true,
+              StyleKey.underline: true,
+              StyleKey.strikethrough: true,
+            }),
+          ]),
+        );
+      await editor.startTesting();
+      await editor.updateSelection(
+        Selection.single(path: [0], startOffset: 0),
+      );
+
+      final selection =
+          Selection.single(path: [0], startOffset: 0, endOffset: text.length);
+      var node = editor.nodeAtPath([0]) as TextNode;
+      var state = node.key?.currentState as DefaultSelectable;
+      var checkboxWidget = find.byKey(state.iconKey!);
+      await tester.tap(checkboxWidget);
+      await tester.pumpAndSettle();
+
+      expect(node.attributes.check, true);
+
+      expect(node.allSatisfyBoldInSelection(selection), true);
+      expect(node.allSatisfyItalicInSelection(selection), true);
+      expect(node.allSatisfyUnderlineInSelection(selection), true);
+      expect(node.allSatisfyStrikethroughInSelection(selection), true);
+
+      node = editor.nodeAtPath([0]) as TextNode;
+      state = node.key?.currentState as DefaultSelectable;
+      await tester.ensureVisible(find.byKey(state.iconKey!));
+      await tester.tap(find.byKey(state.iconKey!));
+      await tester.pump();
+
+      expect(node.attributes.check, false);
+      expect(node.allSatisfyBoldInSelection(selection), true);
+      expect(node.allSatisfyItalicInSelection(selection), true);
+      expect(node.allSatisfyUnderlineInSelection(selection), true);
+      expect(node.allSatisfyStrikethroughInSelection(selection), true);
+    });
+  });
+}

+ 45 - 4
frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart

@@ -18,7 +18,6 @@ void main() async {
         LogicalKeyboardKey.keyB,
       );
     });
-
     testWidgets('Presses Command + I to update text style', (tester) async {
       await _testUpdateTextStyleByCommandX(
         tester,
@@ -26,7 +25,6 @@ void main() async {
         LogicalKeyboardKey.keyI,
       );
     });
-
     testWidgets('Presses Command + U to update text style', (tester) async {
       await _testUpdateTextStyleByCommandX(
         tester,
@@ -34,7 +32,6 @@ void main() async {
         LogicalKeyboardKey.keyU,
       );
     });
-
     testWidgets('Presses Command + S to update text style', (tester) async {
       await _testUpdateTextStyleByCommandX(
         tester,
@@ -83,5 +80,49 @@ Future<void> _testUpdateTextStyleByCommandX(
     isMetaPressed: true,
   );
   textNode = editor.nodeAtPath([1]) as TextNode;
-  expect(textNode.allSatisfyInSelection(matchStyle, selection), false);
+  expect(textNode.allNotSatisfyInSelection(matchStyle, selection), true);
+
+  selection = Selection(
+    start: Position(path: [0], offset: 0),
+    end: Position(path: [2], offset: text.length),
+  );
+  await editor.updateSelection(selection);
+  await editor.pressLogicKey(
+    key,
+    isShiftPressed: key == LogicalKeyboardKey.keyS,
+    isMetaPressed: true,
+  );
+  var nodes = editor.editorState.service.selectionService.currentSelectedNodes
+      .whereType<TextNode>();
+  expect(nodes.length, 3);
+  for (final node in nodes) {
+    expect(
+      node.allSatisfyInSelection(
+        matchStyle,
+        Selection.single(
+            path: node.path, startOffset: 0, endOffset: text.length),
+      ),
+      true,
+    );
+  }
+
+  await editor.updateSelection(selection);
+  await editor.pressLogicKey(
+    key,
+    isShiftPressed: key == LogicalKeyboardKey.keyS,
+    isMetaPressed: true,
+  );
+  nodes = editor.editorState.service.selectionService.currentSelectedNodes
+      .whereType<TextNode>();
+  expect(nodes.length, 3);
+  for (final node in nodes) {
+    expect(
+      node.allNotSatisfyInSelection(
+        matchStyle,
+        Selection.single(
+            path: node.path, startOffset: 0, endOffset: text.length),
+      ),
+      true,
+    );
+  }
 }

+ 178 - 0
frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart

@@ -0,0 +1,178 @@
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import '../../infra/test_editor.dart';
+
+void main() async {
+  setUpAll(() {
+    TestWidgetsFlutterBinding.ensureInitialized();
+  });
+
+  group('white_space_handler.dart', () {
+    // Before
+    //
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    //
+    // After
+    // [h1]Welcome to Appflowy 😁
+    // [h2]Welcome to Appflowy 😁
+    // [h3]Welcome to Appflowy 😁
+    // [h4]Welcome to Appflowy 😁
+    // [h5]Welcome to Appflowy 😁
+    // [h6]Welcome to Appflowy 😁
+    //
+    testWidgets('Presses whitespace key after #*', (tester) async {
+      const maxSignCount = 6;
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor;
+      for (var i = 1; i <= maxSignCount; i++) {
+        editor.insertTextNode('${'#' * i}$text');
+      }
+      await editor.startTesting();
+
+      for (var i = 1; i <= maxSignCount; i++) {
+        await editor.updateSelection(
+          Selection.single(path: [i - 1], startOffset: i),
+        );
+        await editor.pressLogicKey(LogicalKeyboardKey.space);
+
+        final textNode = (editor.nodeAtPath([i - 1]) as TextNode);
+
+        expect(textNode.subtype, StyleKey.heading);
+        // StyleKey.h1 ~ StyleKey.h6
+        expect(textNode.attributes.heading, 'h$i');
+      }
+    });
+
+    // Before
+    //
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    //
+    // After
+    // [h1]##Welcome to Appflowy 😁
+    // [h2]##Welcome to Appflowy 😁
+    // [h3]##Welcome to Appflowy 😁
+    // [h4]##Welcome to Appflowy 😁
+    // [h5]##Welcome to Appflowy 😁
+    // [h6]##Welcome to Appflowy 😁
+    //
+    testWidgets('Presses whitespace key inside #*', (tester) async {
+      const maxSignCount = 6;
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor;
+      for (var i = 1; i <= maxSignCount; i++) {
+        editor.insertTextNode('${'###' * i}$text');
+      }
+      await editor.startTesting();
+
+      for (var i = 1; i <= maxSignCount; i++) {
+        await editor.updateSelection(
+          Selection.single(path: [i - 1], startOffset: i),
+        );
+        await editor.pressLogicKey(LogicalKeyboardKey.space);
+
+        final textNode = (editor.nodeAtPath([i - 1]) as TextNode);
+
+        expect(textNode.subtype, StyleKey.heading);
+        // StyleKey.h1 ~ StyleKey.h6
+        expect(textNode.attributes.heading, 'h$i');
+        expect(textNode.toRawString().startsWith('##'), true);
+      }
+    });
+
+    // Before
+    //
+    // Welcome to Appflowy 😁
+    //
+    // After
+    // [h1 ~ h6]##Welcome to Appflowy 😁
+    //
+    testWidgets('Presses whitespace key in heading styled text',
+        (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor..insertTextNode(text);
+
+      await editor.startTesting();
+
+      const maxSignCount = 6;
+      for (var i = 1; i <= maxSignCount; i++) {
+        await editor.updateSelection(
+          Selection.single(path: [0], startOffset: 0),
+        );
+
+        final textNode = (editor.nodeAtPath([0]) as TextNode);
+
+        await editor.insertText(textNode, '#' * i, 0);
+        await editor.pressLogicKey(LogicalKeyboardKey.space);
+
+        expect(textNode.subtype, StyleKey.heading);
+        // StyleKey.h2 ~ StyleKey.h6
+        expect(textNode.attributes.heading, 'h$i');
+      }
+    });
+
+    testWidgets('Presses whitespace key after (un)checkbox symbols',
+        (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor..insertTextNode(text);
+      await editor.startTesting();
+
+      final textNode = editor.nodeAtPath([0]) as TextNode;
+      for (final symbol in unCheckboxListSymbols) {
+        await editor.updateSelection(
+          Selection.single(path: [0], startOffset: 0),
+        );
+        await editor.insertText(textNode, symbol, 0);
+        await editor.pressLogicKey(LogicalKeyboardKey.space);
+        expect(textNode.subtype, StyleKey.checkbox);
+        expect(textNode.attributes.check, false);
+      }
+    });
+
+    testWidgets('Presses whitespace key after checkbox symbols',
+        (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor..insertTextNode(text);
+      await editor.startTesting();
+
+      final textNode = editor.nodeAtPath([0]) as TextNode;
+      for (final symbol in checkboxListSymbols) {
+        await editor.updateSelection(
+          Selection.single(path: [0], startOffset: 0),
+        );
+        await editor.insertText(textNode, symbol, 0);
+        await editor.pressLogicKey(LogicalKeyboardKey.space);
+        expect(textNode.subtype, StyleKey.checkbox);
+        expect(textNode.attributes.check, true);
+      }
+    });
+
+    testWidgets('Presses whitespace key after bulleted list', (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor..insertTextNode(text);
+      await editor.startTesting();
+
+      final textNode = editor.nodeAtPath([0]) as TextNode;
+      for (final symbol in bulletedListSymbols) {
+        await editor.updateSelection(
+          Selection.single(path: [0], startOffset: 0),
+        );
+        await editor.insertText(textNode, symbol, 0);
+        await editor.pressLogicKey(LogicalKeyboardKey.space);
+        expect(textNode.subtype, StyleKey.bulletedList);
+      }
+    });
+  });
+}