From c008f3369cf532a07243aea54959d2dc5d5a591e Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 12:13:02 +0800 Subject: [PATCH 1/3] test: implement backspace key test for styled text --- .../example/test/widget_test.dart | 24 +---- .../delete_text_handler.dart | 4 +- .../flowy_editor/test/infra/test_editor.dart | 8 +- .../test/infra/test_raw_key_event.dart | 15 ++- .../delete_text_handler_test.dart | 97 +++++++++++++++++++ 5 files changed, 115 insertions(+), 33 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart b/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart index 092d222f7e..2a2b819285 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart @@ -5,26 +5,4 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} +void main() {} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index 92f5e9afb9..afef57cceb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -72,7 +72,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { _deleteNodes(transactionBuilder, textNodes, selection); } - transactionBuilder.commit(); + if (transactionBuilder.operations.isNotEmpty) { + transactionBuilder.commit(); + } return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index 3f780412c4..b6aebeaa41 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -73,13 +73,7 @@ class EditorWidgetTester { } Future pressLogicKey(LogicalKeyboardKey key) async { - late RawKeyEvent testRawKeyEventData; - if (key == LogicalKeyboardKey.enter) { - testRawKeyEventData = const TestRawKeyEventData( - logicalKey: LogicalKeyboardKey.enter, - physicalKey: PhysicalKeyboardKey.enter, - ).toKeyEvent; - } + final testRawKeyEventData = TestRawKeyEventData(logicalKey: key).toKeyEvent; _editorState.service.keyboardService!.onKey(testRawKeyEventData); await tester.pumpAndSettle(); } diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index 2cb3fa0fd9..aa98781d7d 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -7,7 +7,6 @@ class TestRawKeyEvent extends RawKeyDownEvent { class TestRawKeyEventData extends RawKeyEventData { const TestRawKeyEventData({ required this.logicalKey, - required this.physicalKey, this.isControlPressed = false, this.isShiftPressed = false, this.isAltPressed = false, @@ -30,7 +29,7 @@ class TestRawKeyEventData extends RawKeyEventData { final LogicalKeyboardKey logicalKey; @override - final PhysicalKeyboardKey physicalKey; + PhysicalKeyboardKey get physicalKey => logicalKey.toPhysicalKey; @override KeyboardSide? getModifierSide(ModifierKey key) { @@ -50,3 +49,15 @@ class TestRawKeyEventData extends RawKeyEventData { return TestRawKeyEvent(data: this); } } + +extension on LogicalKeyboardKey { + PhysicalKeyboardKey get toPhysicalKey { + if (this == LogicalKeyboardKey.enter) { + return PhysicalKeyboardKey.enter; + } + if (this == LogicalKeyboardKey.backspace) { + return PhysicalKeyboardKey.backspace; + } + throw UnimplementedError(); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart new file mode 100644 index 0000000000..4a42ac2c47 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart @@ -0,0 +1,97 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.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('delete_text_handler.dart', () { + testWidgets('Presses backspace key in empty document', (tester) async { + // Before + // + // [Empty Line] + // + // After + // + // [Empty Line] + // + final editor = tester.editor..insertEmptyTextNode(); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + // Pressing the backspace key continuously. + for (int i = 1; i <= 1; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + ); + expect(editor.documentLength, 1); + expect(editor.documentSelection, + Selection.single(path: [0], startOffset: 0)); + } + }); + }); + + // Before + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁Welcome to Appflowy 😁 + // + testWidgets('Presses backspace key in styled text', (tester) async { + await _deleteStyledText(tester, StyleKey.checkbox); + }); +} + +Future _deleteStyledText(WidgetTester tester, String style) async { + const text = 'Welcome to Appflowy 😁'; + Attributes attributes = { + StyleKey.subtype: style, + }; + if (style == StyleKey.checkbox) { + attributes[StyleKey.checkbox] = true; + } else if (style == StyleKey.numberList) { + attributes[StyleKey.number] = 1; + } + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text, attributes: attributes) + ..insertTextNode(text, attributes: attributes); + + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [2], startOffset: 0), + ); + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + ); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); + + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + ); + expect(editor.documentLength, 2); + expect(editor.documentSelection, + Selection.single(path: [1], startOffset: text.length)); + expect(editor.nodeAtPath([1])?.subtype, style); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text * 2); + + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0), + ); + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + ); + expect(editor.documentLength, 2); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); + expect(editor.nodeAtPath([1])?.subtype, null); +} From c66e1e4df84e95d70b98278ce1346b7c9cd05d0d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 14:05:18 +0800 Subject: [PATCH 2/3] test: implement delete key test for styled text --- .../packages/flowy_editor/coverage/lcov.info | 0 .../delete_text_handler.dart | 6 +- .../flowy_editor/test/infra/test_editor.dart | 9 + .../test/infra/test_raw_key_event.dart | 3 + .../delete_text_handler_test.dart | 260 +++++++++++++++++- 5 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/coverage/lcov.info diff --git a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index afef57cceb..ccddff27be 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -80,11 +80,13 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { - final selection = editorState.service.selectionService.currentSelection.value; + var selection = editorState.service.selectionService.currentSelection.value; if (selection == null) { return KeyEventResult.ignored; } - final nodes = editorState.service.selectionService.currentSelectedNodes; + var nodes = editorState.service.selectionService.currentSelectedNodes; + nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); + selection = selection.isBackward ? selection : selection.reversed; // make sure all nodes is [TextNode]. final textNodes = nodes.whereType().toList(); if (textNodes.length != nodes.length) { diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index b6aebeaa41..ddbe4d5b2c 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -95,6 +95,15 @@ class EditorWidgetTester { } } +extension TestString on String { + String safeSubString([int start = 0, int? end]) { + end ??= length - 1; + end = end.clamp(start, length - 1); + final sRunes = runes; + return String.fromCharCodes(sRunes, start, end); + } +} + extension TestEditorExtension on WidgetTester { EditorWidgetTester get editor => EditorWidgetTester(tester: this)..initialize(); diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index aa98781d7d..48c4ab3e67 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -58,6 +58,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.backspace) { return PhysicalKeyboardKey.backspace; } + if (this == LogicalKeyboardKey.delete) { + return PhysicalKeyboardKey.delete; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart index 4a42ac2c47..15af27e7a4 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart @@ -36,6 +36,87 @@ void main() async { }); }); + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome Appflowy 😁 + // + // Then + // Welcome to Appflowy 😁 + // + testWidgets( + 'Presses backspace key in non-empty document and selection is backward', + (tester) async { + await _deleteTextByBackspace(tester, true); + }); + testWidgets( + 'Presses backspace key in non-empty document and selection is forward', + (tester) async { + await _deleteTextByBackspace(tester, false); + }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome Appflowy 😁 + // + // Then + // Welcome to Appflowy 😁 + // + testWidgets( + 'Presses delete key in non-empty document and selection is backward', + (tester) async { + await _deleteTextByDelete(tester, true); + }); + testWidgets( + 'Presses delete key in non-empty document and selection is forward', + (tester) async { + await _deleteTextByDelete(tester, false); + }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁Welcome Appflowy 😁 + testWidgets( + 'Presses delete key in non-empty document and selection is at the end of the text', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [0], startOffset: text.length), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + + expect(editor.documentLength, 1); + expect(editor.documentSelection, + Selection.single(path: [0], startOffset: text.length)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), text * 2); + }); + // Before // // Welcome to Appflowy 😁 @@ -47,12 +128,49 @@ void main() async { // Welcome to Appflowy 😁 // [Style] Welcome to Appflowy 😁Welcome to Appflowy 😁 // - testWidgets('Presses backspace key in styled text', (tester) async { - await _deleteStyledText(tester, StyleKey.checkbox); + testWidgets('Presses backspace key in styled text (checkbox)', + (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.checkbox); + }); + testWidgets('Presses backspace key in styled text (bulletedList)', + (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.bulletedList); + }); + testWidgets('Presses backspace key in styled text (heading)', (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.heading); + }); + testWidgets('Presses backspace key in styled text (quote)', (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.quote); + }); + + // Before + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + testWidgets('Presses delete key in styled text (checkbox)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.checkbox); + }); + testWidgets('Presses delete key in styled text (bulletedList)', + (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.bulletedList); + }); + testWidgets('Presses delete key in styled text (heading)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.heading); + }); + testWidgets('Presses delete key in styled text (quote)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.quote); }); } -Future _deleteStyledText(WidgetTester tester, String style) async { +Future _deleteStyledTextByBackspace( + WidgetTester tester, String style) async { const text = 'Welcome to Appflowy 😁'; Attributes attributes = { StyleKey.subtype: style, @@ -61,6 +179,8 @@ Future _deleteStyledText(WidgetTester tester, String style) async { attributes[StyleKey.checkbox] = true; } else if (style == StyleKey.numberList) { attributes[StyleKey.number] = 1; + } else if (style == StyleKey.heading) { + attributes[StyleKey.heading] = StyleKey.h1; } final editor = tester.editor ..insertTextNode(text) @@ -95,3 +215,137 @@ Future _deleteStyledText(WidgetTester tester, String style) async { expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); expect(editor.nodeAtPath([1])?.subtype, null); } + +Future _deleteStyledTextByDelete( + WidgetTester tester, String style) async { + const text = 'Welcome to Appflowy 😁'; + Attributes attributes = { + StyleKey.subtype: style, + }; + if (style == StyleKey.checkbox) { + attributes[StyleKey.checkbox] = true; + } else if (style == StyleKey.numberList) { + attributes[StyleKey.number] = 1; + } else if (style == StyleKey.heading) { + attributes[StyleKey.heading] = StyleKey.h1; + } + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text, attributes: attributes) + ..insertTextNode(text, attributes: attributes); + + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0), + ); + for (var i = 1; i < text.length; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.delete, + ); + expect( + editor.documentSelection, Selection.single(path: [1], startOffset: 0)); + expect(editor.nodeAtPath([1])?.subtype, style); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + text.safeSubString(i)); + } + + await editor.pressLogicKey( + LogicalKeyboardKey.delete, + ); + expect(editor.documentLength, 2); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); + expect(editor.nodeAtPath([1])?.subtype, style); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); +} + +Future _deleteTextByBackspace( + WidgetTester tester, bool isBackwardSelection) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [1], startOffset: 10), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + 'Welcome t Appflowy 😁'); + + // delete 'to ' + await editor.updateSelection( + Selection.single(path: [2], startOffset: 8, endOffset: 11), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); + expect((editor.nodeAtPath([2]) as TextNode).toRawString(), + 'Welcome Appflowy 😁'); + + // delete 'Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome ' + final start = Position(path: [0], offset: 11); + final end = Position(path: [2], offset: 8); + await editor.updateSelection(Selection( + start: isBackwardSelection ? start : end, + end: isBackwardSelection ? end : start)); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 1); + expect( + editor.documentSelection, Selection.single(path: [0], startOffset: 11)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), + 'Welcome to Appflowy 😁'); +} + +Future _deleteTextByDelete( + WidgetTester tester, bool isBackwardSelection) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [1], startOffset: 9), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + 'Welcome t Appflowy 😁'); + + // delete 'to ' + await editor.updateSelection( + Selection.single(path: [2], startOffset: 8, endOffset: 11), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); + expect((editor.nodeAtPath([2]) as TextNode).toRawString(), + 'Welcome Appflowy 😁'); + + // delete 'Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome ' + final start = Position(path: [0], offset: 11); + final end = Position(path: [2], offset: 8); + await editor.updateSelection(Selection( + start: isBackwardSelection ? start : end, + end: isBackwardSelection ? end : start)); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + expect(editor.documentLength, 1); + expect( + editor.documentSelection, Selection.single(path: [0], startOffset: 11)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), + 'Welcome to Appflowy 😁'); +} From 6d46f40fae341035e8b1328e1fddcabf0c8f0224 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 14:06:56 +0800 Subject: [PATCH 3/3] chore: delete icov.info --- frontend/app_flowy/packages/flowy_editor/coverage/lcov.info | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/coverage/lcov.info diff --git a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info deleted file mode 100644 index e69de29bb2..0000000000