Browse Source

Merge pull request #853 from LucasXu0/test/page_up_down

Implement more tests.
Nathan.fooo 2 năm trước cách đây
mục cha
commit
dcbf657d84
13 tập tin đã thay đổi với 457 bổ sung71 xóa
  1. 11 23
      frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart
  2. 2 11
      frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart
  3. 19 11
      frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart
  4. 15 21
      frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart
  5. 28 0
      frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart
  6. 13 1
      frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart
  7. 14 2
      frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart
  8. 56 2
      frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart
  9. 75 0
      frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart
  10. 60 0
      frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart
  11. 38 0
      frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart
  12. 39 0
      frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart
  13. 87 0
      frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart

+ 11 - 23
frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart

@@ -28,10 +28,12 @@ List<String> defaultListToolbarEventNames = [
   'H1',
   'H2',
   'H3',
-  // 'B-List',
-  // 'N-List',
 ];
 
+mixin ToolbarMixin<T extends StatefulWidget> on State<T> {
+  void hide();
+}
+
 class ToolbarWidget extends StatefulWidget {
   const ToolbarWidget({
     Key? key,
@@ -50,7 +52,7 @@ class ToolbarWidget extends StatefulWidget {
   State<ToolbarWidget> createState() => _ToolbarWidgetState();
 }
 
-class _ToolbarWidgetState extends State<ToolbarWidget> {
+class _ToolbarWidgetState extends State<ToolbarWidget> with ToolbarMixin {
   final GlobalKey _listToolbarKey = GlobalKey();
 
   final toolbarHeight = 32.0;
@@ -63,21 +65,6 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
 
   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<ToolbarWidget> {
     );
   }
 
+  @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<ToolbarWidget> {
     }
     assert(false, 'Could not find the event handler for $eventName');
   }
-
-  void _onSelectionChange() {
-    _listToolbarOverlay?.remove();
-    _listToolbarOverlay = null;
-  }
 }

+ 2 - 11
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);

+ 19 - 11
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<PopupListItem> get popupListItems => _popupListItems;
+
 final List<PopupListItem> _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;

+ 15 - 21
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;

+ 28 - 0
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<FlowyScroll>
   @override
   double get dy => _scrollController.position.pixels;
 
+  @override
+  double? get onePageHeight {
+    final renderBox = context.findRenderObject()?.unwrapOrNull<RenderBox>();
+    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(

+ 13 - 1
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<FlowyToolbar>
     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<FlowyToolbar>
 
   @override
   void hide() {
+    _toolbarWidgetKey.currentState?.unwrapOrNull<ToolbarMixin>()?.hide();
     _toolbarOverlay?.remove();
     _toolbarOverlay = null;
   }
@@ -55,4 +60,11 @@ class _FlowyToolbarState extends State<FlowyToolbar>
       child: widget.child,
     );
   }
+
+  @override
+  void dispose() {
+    hide();
+
+    super.dispose();
+  }
 }

+ 14 - 2
frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart

@@ -72,8 +72,20 @@ class EditorWidgetTester {
     await tester.pumpAndSettle();
   }
 
-  Future<void> pressLogicKey(LogicalKeyboardKey key) async {
-    final testRawKeyEventData = TestRawKeyEventData(logicalKey: key).toKeyEvent;
+  Future<void> 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();
   }

+ 56 - 2
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();
   }
 }

+ 75 - 0
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);
+      }
+    });
+  });
+}

+ 60 - 0
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<void> _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);
+}

+ 38 - 0
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<void> _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),
+    ),
+  );
+}

+ 39 - 0
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);
+    });
+  });
+}

+ 87 - 0
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<void> _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);
+}