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
This commit is contained in:
Lucas.Xu 2023-06-21 19:53:29 +08:00 committed by GitHub
parent d302f6b3fb
commit 9bd629aaef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 229 additions and 26 deletions

View File

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

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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''';

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -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();

View File

@ -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 {

View File

@ -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);

View File

@ -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

View File

@ -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;
}

View File

@ -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(

View File

@ -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,

View File

@ -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"

View File

@ -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