diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart index 68d78f484f..2c9fb9ad93 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart @@ -28,10 +28,12 @@ List defaultListToolbarEventNames = [ 'H1', 'H2', 'H3', - // 'B-List', - // 'N-List', ]; +mixin ToolbarMixin on State { + void hide(); +} + class ToolbarWidget extends StatefulWidget { const ToolbarWidget({ Key? key, @@ -50,7 +52,7 @@ class ToolbarWidget extends StatefulWidget { State createState() => _ToolbarWidgetState(); } -class _ToolbarWidgetState extends State { +class _ToolbarWidgetState extends State with ToolbarMixin { final GlobalKey _listToolbarKey = GlobalKey(); final toolbarHeight = 32.0; @@ -63,21 +65,6 @@ class _ToolbarWidgetState extends State { OverlayEntry? _listToolbarOverlay; - @override - void initState() { - super.initState(); - - widget.editorState.service.selectionService.currentSelection - .addListener(_onSelectionChange); - } - - @override - void dispose() { - widget.editorState.service.selectionService.currentSelection - .removeListener(_onSelectionChange); - super.dispose(); - } - @override Widget build(BuildContext context) { return Positioned( @@ -92,6 +79,12 @@ class _ToolbarWidgetState extends State { ); } + @override + void hide() { + _listToolbarOverlay?.remove(); + _listToolbarOverlay = null; + } + Widget _buildToolbar(BuildContext context) { return Material( borderRadius: BorderRadius.circular(cornerRadius), @@ -212,9 +205,4 @@ class _ToolbarWidgetState extends State { } assert(false, 'Could not find the event handler for $eventName'); } - - void _onSelectionChange() { - _listToolbarOverlay?.remove(); - _listToolbarOverlay = null; - } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart index 5acdcdcb0d..18ab2d23ee 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart @@ -2,25 +2,16 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -double? getEditorHeight(EditorState editorState) { - final renderObj = - editorState.service.scrollServiceKey.currentContext?.findRenderObject(); - if (renderObj is RenderBox) { - return renderObj.size.height; - } - return null; -} - FlowyKeyEventHandler pageUpDownHandler = (editorState, event) { if (event.logicalKey == LogicalKeyboardKey.pageUp) { - final scrollHeight = getEditorHeight(editorState); + final scrollHeight = editorState.service.scrollService?.onePageHeight; final scrollService = editorState.service.scrollService; if (scrollHeight != null && scrollService != null) { scrollService.scrollTo(scrollService.dy - scrollHeight); } return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.pageDown) { - final scrollHeight = getEditorHeight(editorState); + final scrollHeight = editorState.service.scrollService?.onePageHeight; final scrollService = editorState.service.scrollService; if (scrollHeight != null && scrollService != null) { scrollService.scrollTo(scrollService.dy + scrollHeight); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 83f1b9e13a..093318813d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -11,6 +11,9 @@ import 'package:flowy_editor/src/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +@visibleForTesting +List get popupListItems => _popupListItems; + final List _popupListItems = [ PopupListItem( text: 'Text', @@ -94,6 +97,14 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { _editorState = editorState; WidgetsBinding.instance.addPostFrameCallback((_) { _selectionChangeBySlash = false; + + editorState.service.selectionService.currentSelection + .removeListener(clearPopupList); + editorState.service.selectionService.currentSelection + .addListener(clearPopupList); + + editorState.service.scrollService?.disable(); + showPopupList(context, editorState, selectionRects.first.bottomRight); }); @@ -115,23 +126,20 @@ void showPopupList( ); Overlay.of(context)?.insert(_popupListOverlay!); - - editorState.service.selectionService.currentSelection - .removeListener(clearPopupList); - editorState.service.selectionService.currentSelection - .addListener(clearPopupList); - - editorState.service.scrollService?.disable(); } void clearPopupList() { if (_popupListOverlay == null || _editorState == null) { return; } - final selection = - _editorState?.service.selectionService.currentSelection.value; - if (selection == null) { - return; + final isSelectionDisposed = + _editorState?.service.selectionServiceKey.currentState != null; + if (isSelectionDisposed) { + final selection = + _editorState?.service.selectionService.currentSelection.value; + if (selection == null) { + return; + } } if (_selectionChangeBySlash) { _selectionChangeBySlash = false; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 4bd9382e3a..4dab96e54a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/src/document/node.dart'; import 'package:flowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; import 'package:flowy_editor/src/service/keyboard_service.dart'; +import 'package:flutter/services.dart'; FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { - if (!event.isMetaPressed || event.character == null) { + if (!event.isMetaPressed) { return KeyEventResult.ignored; } @@ -17,26 +18,19 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { return KeyEventResult.ignored; } - switch (event.character!) { - // bold - case 'B': - case 'b': - formatBold(editorState); - return KeyEventResult.handled; - case 'I': - case 'i': - formatItalic(editorState); - return KeyEventResult.handled; - case 'U': - case 'u': - formatUnderline(editorState); - return KeyEventResult.handled; - case 'S': - case 's': - formatStrikethrough(editorState); - return KeyEventResult.handled; - default: - break; + if (event.logicalKey == LogicalKeyboardKey.keyB) { + formatBold(editorState); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyI) { + formatItalic(editorState); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyU) { + formatUnderline(editorState); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyS && + event.isShiftPressed) { + formatStrikethrough(editorState); + return KeyEventResult.handled; } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart index 5201a18942..199249b715 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart @@ -1,8 +1,15 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_editor/src/extensions/object_extensions.dart'; abstract class FlowyScrollService { double get dy; + double? get onePageHeight; + + int? get page; + + double get maxScrollExtent; + double get minScrollExtent; void scrollTo(double dy); @@ -32,6 +39,27 @@ class _FlowyScrollState extends State @override double get dy => _scrollController.position.pixels; + @override + double? get onePageHeight { + final renderBox = context.findRenderObject()?.unwrapOrNull(); + return renderBox?.size.height; + } + + @override + double get maxScrollExtent => _scrollController.position.maxScrollExtent; + + @override + double get minScrollExtent => _scrollController.position.minScrollExtent; + + @override + int? get page { + if (onePageHeight != null) { + final scrollExtent = maxScrollExtent - minScrollExtent; + return (scrollExtent / onePageHeight!).ceil(); + } + return null; + } + @override Widget build(BuildContext context) { return Listener( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart index aaf52bc20c..fad7437b63 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/src/render/selection/toolbar_widget.dart'; +import 'package:flowy_editor/src/extensions/object_extensions.dart'; abstract class FlowyToolbarService { /// Show the toolbar widget beside the offset. @@ -28,12 +29,15 @@ class FlowyToolbar extends StatefulWidget { class _FlowyToolbarState extends State implements FlowyToolbarService { OverlayEntry? _toolbarOverlay; + final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget'); @override void showInOffset(Offset offset, LayerLink layerLink) { - _toolbarOverlay?.remove(); + hide(); + _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( + key: _toolbarWidgetKey, editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), @@ -45,6 +49,7 @@ class _FlowyToolbarState extends State @override void hide() { + _toolbarWidgetKey.currentState?.unwrapOrNull()?.hide(); _toolbarOverlay?.remove(); _toolbarOverlay = null; } @@ -55,4 +60,11 @@ class _FlowyToolbarState extends State child: widget.child, ); } + + @override + void dispose() { + hide(); + + super.dispose(); + } } 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 ddbe4d5b2c..533cace586 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 @@ -72,8 +72,20 @@ class EditorWidgetTester { await tester.pumpAndSettle(); } - Future pressLogicKey(LogicalKeyboardKey key) async { - final testRawKeyEventData = TestRawKeyEventData(logicalKey: key).toKeyEvent; + Future pressLogicKey( + LogicalKeyboardKey key, { + bool isControlPressed = false, + bool isShiftPressed = false, + bool isAltPressed = false, + bool isMetaPressed = false, + }) async { + final testRawKeyEventData = TestRawKeyEventData( + logicalKey: key, + isControlPressed: isControlPressed, + isShiftPressed: isShiftPressed, + isAltPressed: isAltPressed, + isMetaPressed: isMetaPressed, + ).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 48c4ab3e67..e4eb99b60e 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 @@ -1,7 +1,25 @@ import 'package:flutter/services.dart'; class TestRawKeyEvent extends RawKeyDownEvent { - const TestRawKeyEvent({required super.data}); + const TestRawKeyEvent({ + required super.data, + this.isControlPressed = false, + this.isShiftPressed = false, + this.isAltPressed = false, + this.isMetaPressed = false, + }); + + @override + final bool isControlPressed; + + @override + final bool isShiftPressed; + + @override + final bool isAltPressed; + + @override + final bool isMetaPressed; } class TestRawKeyEventData extends RawKeyEventData { @@ -46,7 +64,13 @@ class TestRawKeyEventData extends RawKeyEventData { String get keyLabel => throw UnimplementedError(); RawKeyEvent get toKeyEvent { - return TestRawKeyEvent(data: this); + return TestRawKeyEvent( + data: this, + isAltPressed: isAltPressed, + isControlPressed: isControlPressed, + isMetaPressed: isMetaPressed, + isShiftPressed: isShiftPressed, + ); } } @@ -61,6 +85,36 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.delete) { return PhysicalKeyboardKey.delete; } + if (this == LogicalKeyboardKey.pageDown) { + return PhysicalKeyboardKey.pageDown; + } + if (this == LogicalKeyboardKey.pageUp) { + return PhysicalKeyboardKey.pageUp; + } + if (this == LogicalKeyboardKey.slash) { + return PhysicalKeyboardKey.slash; + } + if (this == LogicalKeyboardKey.arrowDown) { + return PhysicalKeyboardKey.arrowDown; + } + if (this == LogicalKeyboardKey.keyA) { + return PhysicalKeyboardKey.keyA; + } + if (this == LogicalKeyboardKey.keyB) { + return PhysicalKeyboardKey.keyB; + } + if (this == LogicalKeyboardKey.keyI) { + return PhysicalKeyboardKey.keyI; + } + if (this == LogicalKeyboardKey.keyS) { + return PhysicalKeyboardKey.keyS; + } + if (this == LogicalKeyboardKey.keyU) { + return PhysicalKeyboardKey.keyU; + } + if (this == LogicalKeyboardKey.keyZ) { + return PhysicalKeyboardKey.keyZ; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart new file mode 100644 index 0000000000..fe74e41efe --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart @@ -0,0 +1,75 @@ +import 'package:flowy_editor/flowy_editor.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('page_up_down_handler_test.dart', () { + testWidgets('Presses PageUp and pageDown key in large document', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < 1000; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + + final scrollService = editor.editorState.service.scrollService; + + expect(scrollService != null, true); + + if (scrollService == null) { + return; + } + + final page = scrollService.page; + final onePageHeight = scrollService.onePageHeight; + expect(page != null, true); + expect(onePageHeight != null, true); + + // Pressing the pageDown key continuously. + var currentOffsetY = 0.0; + for (int i = 1; i <= page!; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.pageDown, + ); + currentOffsetY += onePageHeight!; + final dy = scrollService.dy; + expect(dy, currentOffsetY); + } + + for (int i = 1; i <= 5; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.pageDown, + ); + final dy = scrollService.dy; + expect(dy == scrollService.maxScrollExtent, true); + } + + // Pressing the pageUp key continuously. + for (int i = page; i >= 1; i--) { + await editor.pressLogicKey( + LogicalKeyboardKey.pageUp, + ); + currentOffsetY -= onePageHeight!; + final dy = editor.editorState.service.scrollService?.dy; + expect(dy, currentOffsetY); + } + + for (int i = 1; i <= 5; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.pageUp, + ); + final dy = scrollService.dy; + expect(dy == scrollService.minScrollExtent, true); + } + }); + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart new file mode 100644 index 0000000000..3bd9a81c52 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart @@ -0,0 +1,60 @@ +import 'package:flowy_editor/flowy_editor.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('redo_undo_handler_test.dart', () { + // TODO: need to test more cases. + testWidgets('Redo, Undo for backspace key, and selection is downward', + (tester) async { + await _testBackspaceUndoRedo(tester, true); + }); + + testWidgets('Redo, Undo for backspace key, and selection is forward', + (tester) async { + await _testBackspaceUndoRedo(tester, false); + }); + }); +} + +Future _testBackspaceUndoRedo( + WidgetTester tester, bool isDownwardSelection) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + final start = Position(path: [0], offset: text.length); + final end = Position(path: [1], offset: text.length); + final selection = Selection( + start: isDownwardSelection ? start : end, + end: isDownwardSelection ? end : start, + ); + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 2); + + await editor.pressLogicKey( + LogicalKeyboardKey.keyZ, + isMetaPressed: true, + ); + + expect(editor.documentLength, 3); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); + expect(editor.documentSelection, selection); + + await editor.pressLogicKey( + LogicalKeyboardKey.keyZ, + isMetaPressed: true, + isShiftPressed: true, + ); + + expect(editor.documentLength, 2); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart new file mode 100644 index 0000000000..53c13b7119 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart @@ -0,0 +1,38 @@ +import 'package:flowy_editor/flowy_editor.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('select_all_handler_test.dart', () { + testWidgets('Presses Command + A in small document', (tester) async { + await _testSelectAllHandler(tester, 10); + }); + + testWidgets('Presses Command + A in small document', (tester) async { + await _testSelectAllHandler(tester, 1000); + }); + }); +} + +Future _testSelectAllHandler(WidgetTester tester, int lines) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); + + expect( + editor.documentSelection, + Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [lines - 1], offset: text.length), + ), + ); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart new file mode 100644 index 0000000000..a8d1cc36f1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart @@ -0,0 +1,39 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_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('slash_handler.dart', () { + testWidgets('Presses / to trigger popup list ', (tester) async { + const text = 'Welcome to Appflowy 😁'; + const lines = 3; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [1], startOffset: 0)); + await editor.pressLogicKey(LogicalKeyboardKey.slash); + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect(find.byType(PopupListWidget, skipOffstage: false), findsOneWidget); + + for (final item in popupListItems) { + expect(find.byWidget(item.icon), findsOneWidget); + } + + await editor.updateSelection(Selection.single(path: [1], startOffset: 0)); + + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect(find.byType(PopupListWidget, skipOffstage: false), findsNothing); + }); + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart new file mode 100644 index 0000000000..39c750a933 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -0,0 +1,87 @@ +import 'package:flowy_editor/flowy_editor.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/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('update_text_style_by_command_x_handler.dart', () { + testWidgets('Presses Command + B to update text style', (tester) async { + await _testUpdateTextStyleByCommandX( + tester, + StyleKey.bold, + LogicalKeyboardKey.keyB, + ); + }); + + testWidgets('Presses Command + I to update text style', (tester) async { + await _testUpdateTextStyleByCommandX( + tester, + StyleKey.italic, + LogicalKeyboardKey.keyI, + ); + }); + + testWidgets('Presses Command + U to update text style', (tester) async { + await _testUpdateTextStyleByCommandX( + tester, + StyleKey.underline, + LogicalKeyboardKey.keyU, + ); + }); + + testWidgets('Presses Command + S to update text style', (tester) async { + await _testUpdateTextStyleByCommandX( + tester, + StyleKey.strikethrough, + LogicalKeyboardKey.keyS, + ); + }); + }); +} + +Future _testUpdateTextStyleByCommandX( + WidgetTester tester, String matchStyle, LogicalKeyboardKey key) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + var selection = + Selection.single(path: [1], startOffset: 2, endOffset: text.length - 2); + await editor.updateSelection(selection); + await editor.pressLogicKey( + key, + isShiftPressed: key == LogicalKeyboardKey.keyS, + isMetaPressed: true, + ); + var textNode = editor.nodeAtPath([1]) as TextNode; + expect(textNode.allSatisfyInSelection(matchStyle, selection), true); + + selection = + Selection.single(path: [1], startOffset: 0, endOffset: text.length); + await editor.updateSelection(selection); + await editor.pressLogicKey( + key, + isShiftPressed: key == LogicalKeyboardKey.keyS, + isMetaPressed: true, + ); + textNode = editor.nodeAtPath([1]) as TextNode; + expect(textNode.allSatisfyInSelection(matchStyle, selection), true); + + await editor.updateSelection(selection); + await editor.pressLogicKey( + key, + isShiftPressed: key == LogicalKeyboardKey.keyS, + isMetaPressed: true, + ); + textNode = editor.nodeAtPath([1]) as TextNode; + expect(textNode.allSatisfyInSelection(matchStyle, selection), false); +}