mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
d302f6b3fb
commit
9bd629aaef
2
frontend/appflowy_flutter/.gitignore
vendored
2
frontend/appflowy_flutter/.gitignore
vendored
@ -72,3 +72,5 @@ windows/flutter/dart_ffi/
|
|||||||
*.env
|
*.env
|
||||||
|
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
**/failures/*.png
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'util/util.dart';
|
import '../util/util.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'util/util.dart';
|
import '../util/util.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
@ -7,7 +7,7 @@ import 'package:flowy_infra/uuid.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'util/util.dart';
|
import '../util/util.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
@ -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 |
@ -1,11 +1,13 @@
|
|||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'switch_folder_test.dart' as switch_folder_test;
|
import 'switch_folder_test.dart' as switch_folder_test;
|
||||||
import 'document_test.dart' as document_test;
|
import 'document/document_test.dart' as document_test;
|
||||||
import 'cover_image_test.dart' as cover_image_test;
|
import 'document/cover_image_test.dart' as cover_image_test;
|
||||||
import 'share_markdown_test.dart' as share_markdown_test;
|
import 'share_markdown_test.dart' as share_markdown_test;
|
||||||
import 'import_files_test.dart' as import_files_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_cell_test.dart' as database_cell_test;
|
||||||
import 'database_field_test.dart' as database_field_test;
|
import 'database_field_test.dart' as database_field_test;
|
||||||
import 'database_share_test.dart' as database_share_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() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
switch_folder_test.main();
|
switch_folder_test.main();
|
||||||
cover_image_test.main();
|
|
||||||
document_test.main();
|
|
||||||
share_markdown_test.main();
|
share_markdown_test.main();
|
||||||
import_files_test.main();
|
import_files_test.main();
|
||||||
|
|
||||||
|
// Document integration tests
|
||||||
|
cover_image_test.main();
|
||||||
|
document_test.main();
|
||||||
document_with_database_test.main();
|
document_with_database_test.main();
|
||||||
|
edit_document_test.main();
|
||||||
|
|
||||||
// Database integration tests
|
// Database integration tests
|
||||||
database_cell_test.main();
|
database_cell_test.main();
|
||||||
|
@ -11,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'util.dart';
|
import 'util.dart';
|
||||||
@ -244,6 +245,42 @@ extension CommonOperations on WidgetTester {
|
|||||||
);
|
);
|
||||||
await pumpAndSettle();
|
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 {
|
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||||
|
@ -13,6 +13,12 @@ class EditorOperations {
|
|||||||
|
|
||||||
final WidgetTester tester;
|
final WidgetTester tester;
|
||||||
|
|
||||||
|
EditorState getCurrentEditorState() {
|
||||||
|
return tester
|
||||||
|
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
|
||||||
|
.editorState;
|
||||||
|
}
|
||||||
|
|
||||||
/// Tap the line of editor at [index]
|
/// Tap the line of editor at [index]
|
||||||
Future<void> tapLineOfEditorAt(int index) async {
|
Future<void> tapLineOfEditorAt(int index) async {
|
||||||
final textBlocks = find.byType(TextBlockComponentWidget);
|
final textBlocks = find.byType(TextBlockComponentWidget);
|
||||||
|
@ -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-document2/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -149,8 +149,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
this.editorState = editorState;
|
this.editorState = editorState;
|
||||||
|
|
||||||
// subscribe to the document change from the editor
|
// subscribe to the document change from the editor
|
||||||
_subscription = editorState.transactionStream.listen((transaction) async {
|
_subscription = editorState.transactionStream.listen((event) async {
|
||||||
await _transactionAdapter.apply(transaction, editorState);
|
final time = event.$1;
|
||||||
|
if (time != TransactionTime.before) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _transactionAdapter.apply(event.$2, editorState);
|
||||||
});
|
});
|
||||||
|
|
||||||
// output the log from the editor when debug mode
|
// output the log from the editor when debug mode
|
||||||
|
@ -117,12 +117,13 @@ extension NodeToBlock on Node {
|
|||||||
BlockPB toBlock({
|
BlockPB toBlock({
|
||||||
String? parentId,
|
String? parentId,
|
||||||
String? childrenId,
|
String? childrenId,
|
||||||
|
Attributes? attributes,
|
||||||
}) {
|
}) {
|
||||||
assert(id.isNotEmpty);
|
assert(id.isNotEmpty);
|
||||||
final block = BlockPB.create()
|
final block = BlockPB.create()
|
||||||
..id = id
|
..id = id
|
||||||
..ty = type
|
..ty = type
|
||||||
..data = _dataAdapter(type, attributes);
|
..data = _dataAdapter(type, attributes ?? this.attributes);
|
||||||
if (childrenId != null && childrenId.isNotEmpty) {
|
if (childrenId != null && childrenId.isNotEmpty) {
|
||||||
block.childrenId = childrenId;
|
block.childrenId = childrenId;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
|||||||
UpdateOperation,
|
UpdateOperation,
|
||||||
DeleteOperation,
|
DeleteOperation,
|
||||||
PathExtensions,
|
PathExtensions,
|
||||||
Node;
|
Node,
|
||||||
|
composeAttributes;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@ -34,7 +35,8 @@ class TransactionAdapter {
|
|||||||
final actions = transaction.operations
|
final actions = transaction.operations
|
||||||
.map((op) => op.toBlockAction(editorState))
|
.map((op) => op.toBlockAction(editorState))
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
.expand((element) => element);
|
.expand((element) => element)
|
||||||
|
.toList(growable: false); // avoid lazy evaluation
|
||||||
// Log.debug('actions => $actions');
|
// Log.debug('actions => $actions');
|
||||||
await documentService.applyAction(
|
await documentService.applyAction(
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
@ -114,7 +116,10 @@ extension on UpdateOperation {
|
|||||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||||
assert(parentId.isNotEmpty);
|
assert(parentId.isNotEmpty);
|
||||||
final payload = BlockActionPayloadPB()
|
final payload = BlockActionPayloadPB()
|
||||||
..block = node.toBlock()
|
..block = node.toBlock(
|
||||||
|
parentId: parentId,
|
||||||
|
attributes: composeAttributes(oldAttributes, attributes),
|
||||||
|
)
|
||||||
..parentId = parentId;
|
..parentId = parentId;
|
||||||
actions.add(
|
actions.add(
|
||||||
BlockActionPB()
|
BlockActionPB()
|
||||||
@ -132,7 +137,9 @@ extension on DeleteOperation {
|
|||||||
final parentId =
|
final parentId =
|
||||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||||
final payload = BlockActionPayloadPB()
|
final payload = BlockActionPayloadPB()
|
||||||
..block = node.toBlock()
|
..block = node.toBlock(
|
||||||
|
parentId: parentId,
|
||||||
|
)
|
||||||
..parentId = parentId;
|
..parentId = parentId;
|
||||||
assert(parentId.isNotEmpty);
|
assert(parentId.isNotEmpty);
|
||||||
actions.add(
|
actions.add(
|
||||||
|
@ -50,7 +50,6 @@ class AppFlowyPopover extends StatelessWidget {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
popupBuilder: (context) {
|
popupBuilder: (context) {
|
||||||
final child = popupBuilder(context);
|
final child = popupBuilder(context);
|
||||||
debugPrint("Show popover: $child");
|
|
||||||
return _PopoverContainer(
|
return _PopoverContainer(
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
margin: margin,
|
margin: margin,
|
||||||
|
@ -52,10 +52,11 @@ packages:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: appflowy_editor
|
path: "."
|
||||||
sha256: "3ab567d8993ca06ea114c35bc38c07d2f0d7a5b00857f52d71fbe6a7f9d2ba7b"
|
ref: "4f83b6f"
|
||||||
url: "https://pub.dev"
|
resolved-ref: "4f83b6feb92963f104f3f1f77a473a06aa4870f5"
|
||||||
source: hosted
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
|
source: git
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
|
@ -42,11 +42,11 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||||
ref: a183c57
|
ref: a183c57
|
||||||
appflowy_editor: ^1.0.4
|
# appflowy_editor: ^1.0.4
|
||||||
# appflowy_editor:
|
appflowy_editor:
|
||||||
# git:
|
git:
|
||||||
# url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
# ref: d2460c9
|
ref: 4f83b6f
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user