Browse Source

fix: undo redo for the transforming block will raise an error (#2869)

* fix: undo redo for the transforming block will raise an error

* test: add golden for editing document

* test: add undo redo test
Lucas.Xu 1 năm trước cách đây
mục cha
commit
9bd629aaef

+ 2 - 0
frontend/appflowy_flutter/.gitignore

@@ -72,3 +72,5 @@ windows/flutter/dart_ffi/
 *.env
 
 coverage/
+
+**/failures/*.png

+ 1 - 1
frontend/appflowy_flutter/integration_test/cover_image_test.dart → frontend/appflowy_flutter/integration_test/document/cover_image_test.dart

@@ -1,7 +1,7 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';
 
-import 'util/util.dart';
+import '../util/util.dart';
 
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();

+ 1 - 1
frontend/appflowy_flutter/integration_test/document_test.dart → frontend/appflowy_flutter/integration_test/document/document_test.dart

@@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';
 
-import 'util/util.dart';
+import '../util/util.dart';
 
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();

+ 1 - 1
frontend/appflowy_flutter/integration_test/document_with_database_test.dart → frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart

@@ -7,7 +7,7 @@ import 'package:flowy_infra/uuid.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';
 
-import 'util/util.dart';
+import '../util/util.dart';
 
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();

+ 141 - 0
frontend/appflowy_flutter/integration_test/document/edit_document_test.dart

@@ -0,0 +1,141 @@
+import 'dart:io';
+
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import '../util/ime.dart';
+import '../util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('edit document', () {
+    const location = 'appflowy';
+
+    setUp(() async {
+      await TestFolder.cleanTestLocation(location);
+      await TestFolder.setTestLocation(location);
+    });
+
+    tearDown(() async {
+      await TestFolder.cleanTestLocation(null);
+    });
+
+    testWidgets('redo & undo', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // create a new document called Sample
+      const pageName = 'Sample';
+      await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
+
+      // focus on the editor
+      await tester.editor.tapLineOfEditorAt(0);
+
+      // insert 1. to trigger it to be a numbered list
+      await tester.ime.insertText('1. ');
+      expect(find.text('1.', findRichText: true), findsOneWidget);
+      expect(
+        tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
+        NumberedListBlockKeys.type,
+      );
+
+      // undo
+      // numbered list will be reverted to paragraph
+      await tester.simulateKeyEvent(
+        LogicalKeyboardKey.keyZ,
+        isControlPressed: Platform.isWindows || Platform.isLinux,
+        isMetaPressed: Platform.isMacOS,
+      );
+      expect(
+        tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
+        ParagraphBlockKeys.type,
+      );
+
+      // redo
+      await tester.simulateKeyEvent(
+        LogicalKeyboardKey.keyZ,
+        isControlPressed: Platform.isWindows || Platform.isLinux,
+        isMetaPressed: Platform.isMacOS,
+        isShiftPressed: true,
+      );
+      expect(
+        tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
+        NumberedListBlockKeys.type,
+      );
+
+      // switch to other page and switch back
+      await tester.openPage(readme);
+      await tester.openPage(pageName);
+
+      // the numbered list should be kept
+      expect(
+        tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
+        NumberedListBlockKeys.type,
+      );
+    });
+
+    testWidgets('write a readme document', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // create a new document called Sample
+      const pageName = 'Sample';
+      await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
+
+      // focus on the editor
+      await tester.editor.tapLineOfEditorAt(0);
+
+      // mock inputting the sample
+      final lines = _sample.split('\n');
+      for (final line in lines) {
+        await tester.ime.insertText(line);
+        await tester.ime.insertCharacter('\n');
+      }
+
+      // switch to other page and switch back
+      await tester.openPage(readme);
+      await tester.openPage(pageName);
+
+      // this screenshots are different on different platform, so comment it out temporarily.
+      // check the document
+      // await expectLater(
+      //   find.byType(AppFlowyEditor),
+      //   matchesGoldenFile('document/edit_document_test.png'),
+      // );
+    });
+  });
+}
+
+// TODO(Lucas.Xu): there're no shorctcuts for underline, format code yet.
+const _sample = r'''
+# Heading 1
+## Heading 2
+### Heading 3
+---
+[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
+
+[] Type / followed by /bullet or /num to create a list.
+
+[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
+
+[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
+---
+* bulleted list 1
+
+* bulleted list 2
+
+* bulleted list 3
+bulleted list 4
+---
+1. numbered list 1
+
+2. numbered list 2
+
+3. numbered list 3
+numbered list 4
+---
+" quote''';

BIN
frontend/appflowy_flutter/integration_test/document/edit_document_test.png


+ 10 - 5
frontend/appflowy_flutter/integration_test/runner.dart

@@ -1,11 +1,13 @@
 import 'package:integration_test/integration_test.dart';
 
 import 'switch_folder_test.dart' as switch_folder_test;
-import 'document_test.dart' as document_test;
-import 'cover_image_test.dart' as cover_image_test;
+import 'document/document_test.dart' as document_test;
+import 'document/cover_image_test.dart' as cover_image_test;
 import 'share_markdown_test.dart' as share_markdown_test;
 import 'import_files_test.dart' as import_files_test;
-import 'document_with_database_test.dart' as document_with_database_test;
+import 'document/document_with_database_test.dart'
+    as document_with_database_test;
+import 'document/edit_document_test.dart' as edit_document_test;
 import 'database_cell_test.dart' as database_cell_test;
 import 'database_field_test.dart' as database_field_test;
 import 'database_share_test.dart' as database_share_test;
@@ -27,11 +29,14 @@ import 'database_sort_test.dart' as database_sort_test;
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
   switch_folder_test.main();
-  cover_image_test.main();
-  document_test.main();
   share_markdown_test.main();
   import_files_test.main();
+
+  // Document integration tests
+  cover_image_test.main();
+  document_test.main();
   document_with_database_test.main();
+  edit_document_test.main();
 
   // Database integration tests
   database_cell_test.main();

+ 37 - 0
frontend/appflowy_flutter/integration_test/util/common_operations.dart

@@ -11,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 import 'util.dart';
@@ -244,6 +245,42 @@ extension CommonOperations on WidgetTester {
     );
     await pumpAndSettle();
   }
+
+  Future<void> simulateKeyEvent(
+    LogicalKeyboardKey key, {
+    bool isControlPressed = false,
+    bool isShiftPressed = false,
+    bool isAltPressed = false,
+    bool isMetaPressed = false,
+  }) async {
+    if (isControlPressed) {
+      await simulateKeyDownEvent(LogicalKeyboardKey.control);
+    }
+    if (isShiftPressed) {
+      await simulateKeyDownEvent(LogicalKeyboardKey.shift);
+    }
+    if (isAltPressed) {
+      await simulateKeyDownEvent(LogicalKeyboardKey.alt);
+    }
+    if (isMetaPressed) {
+      await simulateKeyDownEvent(LogicalKeyboardKey.meta);
+    }
+    await simulateKeyDownEvent(key);
+    await simulateKeyUpEvent(key);
+    if (isControlPressed) {
+      await simulateKeyUpEvent(LogicalKeyboardKey.control);
+    }
+    if (isShiftPressed) {
+      await simulateKeyUpEvent(LogicalKeyboardKey.shift);
+    }
+    if (isAltPressed) {
+      await simulateKeyUpEvent(LogicalKeyboardKey.alt);
+    }
+    if (isMetaPressed) {
+      await simulateKeyUpEvent(LogicalKeyboardKey.meta);
+    }
+    await pumpAndSettle();
+  }
 }
 
 extension ViewLayoutPBTest on ViewLayoutPB {

+ 6 - 0
frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart

@@ -13,6 +13,12 @@ class EditorOperations {
 
   final WidgetTester tester;
 
+  EditorState getCurrentEditorState() {
+    return tester
+        .widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
+        .editorState;
+  }
+
   /// Tap the line of editor at [index]
   Future<void> tapLineOfEditorAt(int index) async {
     final textBlocks = find.byType(TextBlockComponentWidget);

+ 7 - 3
frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart

@@ -9,7 +9,7 @@ import 'package:appflowy/plugins/document/application/doc_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
 import 'package:appflowy_editor/appflowy_editor.dart'
-    show EditorState, LogLevel;
+    show EditorState, LogLevel, TransactionTime;
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:flutter/foundation.dart';
@@ -149,8 +149,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
     this.editorState = editorState;
 
     // subscribe to the document change from the editor
-    _subscription = editorState.transactionStream.listen((transaction) async {
-      await _transactionAdapter.apply(transaction, editorState);
+    _subscription = editorState.transactionStream.listen((event) async {
+      final time = event.$1;
+      if (time != TransactionTime.before) {
+        return;
+      }
+      await _transactionAdapter.apply(event.$2, editorState);
     });
 
     // output the log from the editor when debug mode

+ 2 - 1
frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart

@@ -117,12 +117,13 @@ extension NodeToBlock on Node {
   BlockPB toBlock({
     String? parentId,
     String? childrenId,
+    Attributes? attributes,
   }) {
     assert(id.isNotEmpty);
     final block = BlockPB.create()
       ..id = id
       ..ty = type
-      ..data = _dataAdapter(type, attributes);
+      ..data = _dataAdapter(type, attributes ?? this.attributes);
     if (childrenId != null && childrenId.isNotEmpty) {
       block.childrenId = childrenId;
     }

+ 11 - 4
frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart

@@ -10,7 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
         UpdateOperation,
         DeleteOperation,
         PathExtensions,
-        Node;
+        Node,
+        composeAttributes;
 import 'package:collection/collection.dart';
 import 'dart:async';
 
@@ -34,7 +35,8 @@ class TransactionAdapter {
     final actions = transaction.operations
         .map((op) => op.toBlockAction(editorState))
         .whereNotNull()
-        .expand((element) => element);
+        .expand((element) => element)
+        .toList(growable: false); // avoid lazy evaluation
     // Log.debug('actions => $actions');
     await documentService.applyAction(
       documentId: documentId,
@@ -114,7 +116,10 @@ extension on UpdateOperation {
         node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
     assert(parentId.isNotEmpty);
     final payload = BlockActionPayloadPB()
-      ..block = node.toBlock()
+      ..block = node.toBlock(
+        parentId: parentId,
+        attributes: composeAttributes(oldAttributes, attributes),
+      )
       ..parentId = parentId;
     actions.add(
       BlockActionPB()
@@ -132,7 +137,9 @@ extension on DeleteOperation {
       final parentId =
           node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
       final payload = BlockActionPayloadPB()
-        ..block = node.toBlock()
+        ..block = node.toBlock(
+          parentId: parentId,
+        )
         ..parentId = parentId;
       assert(parentId.isNotEmpty);
       actions.add(

+ 0 - 1
frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart

@@ -50,7 +50,6 @@ class AppFlowyPopover extends StatelessWidget {
       offset: offset,
       popupBuilder: (context) {
         final child = popupBuilder(context);
-        debugPrint("Show popover: $child");
         return _PopoverContainer(
           constraints: constraints,
           margin: margin,

+ 5 - 4
frontend/appflowy_flutter/pubspec.lock

@@ -52,10 +52,11 @@ packages:
   appflowy_editor:
     dependency: "direct main"
     description:
-      name: appflowy_editor
-      sha256: "3ab567d8993ca06ea114c35bc38c07d2f0d7a5b00857f52d71fbe6a7f9d2ba7b"
-      url: "https://pub.dev"
-    source: hosted
+      path: "."
+      ref: "4f83b6f"
+      resolved-ref: "4f83b6feb92963f104f3f1f77a473a06aa4870f5"
+      url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
+    source: git
     version: "1.0.4"
   appflowy_popover:
     dependency: "direct main"

+ 5 - 5
frontend/appflowy_flutter/pubspec.yaml

@@ -42,11 +42,11 @@ dependencies:
     git:
       url: https://github.com/AppFlowy-IO/appflowy-board.git
       ref: a183c57
-  appflowy_editor: ^1.0.4
-  # appflowy_editor:
-  #   git:
-  #     url: https://github.com/AppFlowy-IO/appflowy-editor.git
-  #     ref: d2460c9
+  # appflowy_editor: ^1.0.4
+  appflowy_editor:
+    git:
+      url: https://github.com/AppFlowy-IO/appflowy-editor.git
+      ref: 4f83b6f
   appflowy_popover:
     path: packages/appflowy_popover