diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart index 4f768feef3..59daccca8b 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart @@ -59,7 +59,7 @@ void main() { expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty); }); - testWidgets('turn into', (tester) async { + testWidgets('turn into - single line', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -97,5 +97,51 @@ void main() { ); } }); + + testWidgets('turn into - multi lines', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + const name = 'Test Document'; + await tester.createNewPageWithNameUnderParent(name: name); + await tester.openPage(name); + + await tester.editor.tapLineOfEditorAt(0); + await tester.ime.insertText('turn into 1'); + await tester.ime.insertCharacter('\n'); + await tester.ime.insertText('turn into 2'); + + // click the block option button to convert it to another blocks + final values = { + LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type, + LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type, + LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type, + LocaleKeys.document_slashMenu_name_bulletedList.tr(): + BulletedListBlockKeys.type, + LocaleKeys.document_slashMenu_name_numberedList.tr(): + NumberedListBlockKeys.type, + LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, + LocaleKeys.document_slashMenu_name_todoList.tr(): + TodoListBlockKeys.type, + LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type, + LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type, + }; + + for (final value in values.entries) { + final editorState = tester.editor.getCurrentEditorState(); + editorState.selection = Selection( + start: Position(path: [0]), + end: Position(path: [1], offset: 2), + ); + final menuText = value.key; + final afterType = value.value; + await turnIntoBlock( + tester, + [0], + menuText: menuText, + afterType: afterType, + ); + } + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 0b5626767f..1bbb6bd846 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -132,6 +132,11 @@ void _customBlockOptionActions( if (UniversalPlatform.isDesktop) { builder.showActions = (node) => node.parent?.type != TableCellBlockKeys.type; + builder.configuration = builder.configuration.copyWith( + blockSelectionAreaMargin: (_) => const EdgeInsets.symmetric( + vertical: 1, + ), + ); builder.actionBuilder = (context, state) { final top = builder.configuration.padding(context.node).top; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart index cddeb7f2d0..a3065fb153 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart @@ -209,44 +209,60 @@ class BlockActionOptionCubit extends Cubit { Node node, { int? level, }) async { - final toType = type; - - Log.info( - 'Turn into block: from ${node.type} to $type', - ); - - if (type == node.type && type != HeadingBlockKeys.type) { - Log.info('Block type is the same'); + final selection = editorState.selection; + if (selection == null) { return false; } - Node afterNode = node.copyWith( - type: type, - attributes: { - if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level, - if (toType == TodoListBlockKeys.type) TodoListBlockKeys.checked: false, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: (node.delta ?? Delta()).toJson(), - }, - ); - final insertedNode = []; - // heading block and callout block should not have children - if ([HeadingBlockKeys.type, CalloutBlockKeys.type].contains(toType)) { - afterNode = afterNode.copyWith( - children: [], + final toType = type; + + // only handle the node in the same depth + final selectedNodes = editorState + .getNodesInSelection(selection.normalized) + .where((e) => e.path.length == node.path.length) + .toList(); + Log.info('turnIntoBlock selectedNodes $selectedNodes'); + + final insertedNode = []; + + for (final node in selectedNodes) { + Log.info( + 'Turn into block: from ${node.type} to $type', ); - insertedNode.addAll(node.children.map((e) => e.copyWith())); + + Node afterNode = node.copyWith( + type: type, + attributes: { + if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level, + if (toType == TodoListBlockKeys.type) + TodoListBlockKeys.checked: false, + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + blockComponentTextDirection: + node.attributes[blockComponentTextDirection], + blockComponentDelta: (node.delta ?? Delta()).toJson(), + }, + ); + + // heading block and callout block should not have children + if ([HeadingBlockKeys.type, CalloutBlockKeys.type, QuoteBlockKeys.type] + .contains(toType)) { + afterNode = afterNode.copyWith( + children: [], + ); + insertedNode.add(afterNode); + insertedNode.addAll(node.children.map((e) => e.copyWith())); + } else { + insertedNode.add(afterNode); + } } final transaction = editorState.transaction; - transaction.insertNodes(node.path, [ - afterNode, - ...insertedNode, - ]); - transaction.deleteNode(node); + transaction.insertNodes( + node.path, + insertedNode, + ); + transaction.deleteNodes(selectedNodes); await editorState.apply(transaction); return true; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart index fa40fd3d56..b395d47dbf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; @@ -16,6 +14,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; // this flag is used to disable the tooltip of the block when it is dragged @@ -248,7 +247,7 @@ class _OptionButtonFeedbackState extends State<_OptionButtonFeedback> { } } -class _OptionButton extends StatelessWidget { +class _OptionButton extends StatefulWidget { const _OptionButton({ required this.controller, required this.editorState, @@ -261,10 +260,45 @@ class _OptionButton extends StatelessWidget { final BlockComponentContext blockComponentContext; final ValueNotifier isDragging; + @override + State<_OptionButton> createState() => _OptionButtonState(); +} + +const _interceptorKey = 'document_option_button_interceptor'; + +class _OptionButtonState extends State<_OptionButton> { + late final gestureInterceptor = SelectionGestureInterceptor( + key: _interceptorKey, + canTap: (details) => !_isTapInBounds(details.globalPosition), + ); + + // the selection will be cleared when tap the option button + // so we need to restore the selection after tap the option button + Selection? beforeSelection; + RenderBox? get renderBox => context.findRenderObject() as RenderBox?; + + @override + void initState() { + super.initState(); + + widget.editorState.service.selectionService.registerGestureInterceptor( + gestureInterceptor, + ); + } + + @override + void dispose() { + widget.editorState.service.selectionService.unregisterGestureInterceptor( + _interceptorKey, + ); + + super.dispose(); + } + @override Widget build(BuildContext context) { return ValueListenableBuilder( - valueListenable: isDragging, + valueListenable: widget.isDragging, builder: (context, isDragging, child) { return BlockActionButton( svg: FlowySvgs.drag_element_s, @@ -291,7 +325,11 @@ class _OptionButton extends StatelessWidget { ], ), onTap: () { - controller.show(); + if (widget.editorState.selection != null) { + beforeSelection = widget.editorState.selection; + } + + widget.controller.show(); // update selection _updateBlockSelection(); @@ -302,23 +340,35 @@ class _OptionButton extends StatelessWidget { } void _updateBlockSelection() { - final startNode = blockComponentContext.node; - var endNode = startNode; - while (endNode.children.isNotEmpty) { - endNode = endNode.children.last; + if (beforeSelection == null) { + final path = widget.blockComponentContext.node.path; + final selection = Selection.collapsed( + Position(path: path), + ); + widget.editorState.updateSelectionWithReason( + selection, + customSelectionType: SelectionType.block, + ); + } else { + widget.editorState.updateSelectionWithReason( + beforeSelection!, + customSelectionType: SelectionType.block, + ); + } + } + + bool _isTapInBounds(Offset offset) { + if (renderBox == null) { + return false; } - final start = Position(path: startNode.path); - final end = endNode.selectable?.end() ?? - Position( - path: endNode.path, - offset: endNode.delta?.length ?? 0, - ); - - editorState.selectionType = SelectionType.block; - editorState.selection = Selection( - start: start, - end: end, - ); + final localPosition = renderBox!.globalToLocal(offset); + final result = renderBox!.paintBounds.contains(localPosition); + if (result) { + beforeSelection = widget.editorState.selection; + } else { + beforeSelection = null; + } + return result; } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index e090c4eb05..32eef446d5 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,8 +53,8 @@ packages: dependency: "direct main" description: path: "." - ref: a0b3c72 - resolved-ref: a0b3c7289b8c4073b47793f665e70a511324f9b9 + ref: bcd1208 + resolved-ref: bcd12082fea75cdbbea8b090bf938aae7dc9f4ad url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" @@ -1535,10 +1535,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: "direct dev" description: @@ -1933,10 +1933,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" string_validator: dependency: "direct main" description: @@ -2238,10 +2238,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index e079aebdd0..13a84596f2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -43,7 +43,7 @@ dependencies: # Desktop Drop uses Cross File (XFile) data type desktop_drop: ^0.4.4 - device_info_plus: ^10.1.0 + device_info_plus: dotted_border: ^2.0.0+3 easy_localization: ^3.0.2 envied: ^0.5.2 @@ -161,6 +161,7 @@ dev_dependencies: dependency_overrides: http: ^1.0.0 + device_info_plus: ^10.1.0 url_protocol: git: @@ -170,7 +171,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "a0b3c72" + ref: "bcd1208" appflowy_editor_plugins: git: diff --git a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart index f944a62470..ef79e9e64b 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart @@ -2,6 +2,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/bl import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -16,6 +17,7 @@ void main() { Document document, String originalType, String originalText, { + Selection? selection, String? toType, void Function(EditorState editorState, Node node)? afterTurnInto, }) async { @@ -33,6 +35,12 @@ void main() { continue; } + editorState.selectionType = SelectionType.block; + editorState.selection = selection ?? + Selection.collapsed( + Position(path: [0]), + ); + final node = editorState.getNodeAtPath([0])!; expect(node.type, originalType); final result = await cubit.turnIntoBlock( @@ -49,6 +57,11 @@ void main() { ); // turn it back the originalType for the next test + editorState.selectionType = SelectionType.block; + editorState.selection = selection ?? + Selection.collapsed( + Position(path: [0]), + ); await cubit.turnIntoBlock( originalType, newNode, @@ -165,122 +178,358 @@ void main() { ); }); - test('from nested list to heading', () async { - const text = 'bulleted list'; - const nestedText1 = 'nested bulleted list 1'; - const nestedText2 = 'nested bulleted list 2'; - const nestedText3 = 'nested bulleted list 3'; - final document = createDocument([ - bulletedListNode( - text: text, - children: [ - bulletedListNode( - text: nestedText1, - ), - bulletedListNode( - text: nestedText2, - ), - bulletedListNode( - text: nestedText3, - ), - ], - ), - ]); - await checkTurnInto( - document, - BulletedListBlockKeys.type, - text, - toType: HeadingBlockKeys.type, - afterTurnInto: (editorState, node) { - expect(node.type, HeadingBlockKeys.type); - expect(node.children.length, 0); - expect(node.delta!.toPlainText(), text); + for (final type in [ + HeadingBlockKeys.type, + QuoteBlockKeys.type, + CalloutBlockKeys.type, + ]) { + test('from nested bulleted list to $type', () async { + const text = 'bulleted list'; + const nestedText1 = 'nested bulleted list 1'; + const nestedText2 = 'nested bulleted list 2'; + const nestedText3 = 'nested bulleted list 3'; + final document = createDocument([ + bulletedListNode( + text: text, + children: [ + bulletedListNode( + text: nestedText1, + ), + bulletedListNode( + text: nestedText2, + ), + bulletedListNode( + text: nestedText3, + ), + ], + ), + ]); + await checkTurnInto( + document, + BulletedListBlockKeys.type, + text, + toType: type, + afterTurnInto: (editorState, node) { + expect(node.type, type); + expect(node.children.length, 0); + expect(node.delta!.toPlainText(), text); - expect(editorState.document.root.children.length, 4); - expect( - editorState.document.root.children[1].type, - BulletedListBlockKeys.type, - ); - expect( - editorState.document.root.children[1].delta!.toPlainText(), - nestedText1, - ); - expect( - editorState.document.root.children[2].type, - BulletedListBlockKeys.type, - ); - expect( - editorState.document.root.children[2].delta!.toPlainText(), - nestedText2, - ); - expect( - editorState.document.root.children[3].type, - BulletedListBlockKeys.type, - ); - expect( - editorState.document.root.children[3].delta!.toPlainText(), - nestedText3, - ); - }, - ); - }); + expect(editorState.document.root.children.length, 4); + expect( + editorState.document.root.children[1].type, + BulletedListBlockKeys.type, + ); + expect( + editorState.document.root.children[1].delta!.toPlainText(), + nestedText1, + ); + expect( + editorState.document.root.children[2].type, + BulletedListBlockKeys.type, + ); + expect( + editorState.document.root.children[2].delta!.toPlainText(), + nestedText2, + ); + expect( + editorState.document.root.children[3].type, + BulletedListBlockKeys.type, + ); + expect( + editorState.document.root.children[3].delta!.toPlainText(), + nestedText3, + ); + }, + ); + }); + } - test('from numbered list to heading', () async { - const text = 'numbered list'; - const nestedText1 = 'nested numbered list 1'; - const nestedText2 = 'nested numbered list 2'; - const nestedText3 = 'nested numbered list 3'; + for (final type in [ + HeadingBlockKeys.type, + QuoteBlockKeys.type, + CalloutBlockKeys.type, + ]) { + test('from nested numbered list to $type', () async { + const text = 'numbered list'; + const nestedText1 = 'nested numbered list 1'; + const nestedText2 = 'nested numbered list 2'; + const nestedText3 = 'nested numbered list 3'; + final document = createDocument([ + numberedListNode( + delta: Delta()..insert(text), + children: [ + numberedListNode( + delta: Delta()..insert(nestedText1), + ), + numberedListNode( + delta: Delta()..insert(nestedText2), + ), + numberedListNode( + delta: Delta()..insert(nestedText3), + ), + ], + ), + ]); + await checkTurnInto( + document, + NumberedListBlockKeys.type, + text, + toType: type, + afterTurnInto: (editorState, node) { + expect(node.type, type); + expect(node.children.length, 0); + expect(node.delta!.toPlainText(), text); + + expect(editorState.document.root.children.length, 4); + expect( + editorState.document.root.children[1].type, + NumberedListBlockKeys.type, + ); + expect( + editorState.document.root.children[1].delta!.toPlainText(), + nestedText1, + ); + expect( + editorState.document.root.children[2].type, + NumberedListBlockKeys.type, + ); + expect( + editorState.document.root.children[2].delta!.toPlainText(), + nestedText2, + ); + expect( + editorState.document.root.children[3].type, + NumberedListBlockKeys.type, + ); + expect( + editorState.document.root.children[3].delta!.toPlainText(), + nestedText3, + ); + }, + ); + }); + } + + for (final type in [ + HeadingBlockKeys.type, + QuoteBlockKeys.type, + CalloutBlockKeys.type, + ]) { + // numbered list, bulleted list, todo list + // before + // - numbered list 1 + // - nested list 1 + // - bulleted list 2 + // - nested list 2 + // - todo list 3 + // - nested list 3 + // after + // - heading 1 + // - nested list 1 + // - heading 2 + // - nested list 2 + // - heading 3 + // - nested list 3 + test('from nested mixed list to $type', () async { + const text1 = 'numbered list 1'; + const text2 = 'bulleted list 2'; + const text3 = 'todo list 3'; + const nestedText1 = 'nested list 1'; + const nestedText2 = 'nested list 2'; + const nestedText3 = 'nested list 3'; + final document = createDocument([ + numberedListNode( + delta: Delta()..insert(text1), + children: [ + numberedListNode( + delta: Delta()..insert(nestedText1), + ), + ], + ), + bulletedListNode( + delta: Delta()..insert(text2), + children: [ + bulletedListNode( + delta: Delta()..insert(nestedText2), + ), + ], + ), + todoListNode( + checked: false, + text: text3, + children: [ + todoListNode( + checked: false, + text: nestedText3, + ), + ], + ), + ]); + await checkTurnInto( + document, + NumberedListBlockKeys.type, + text1, + toType: type, + selection: Selection( + start: Position(path: [0]), + end: Position(path: [2]), + ), + afterTurnInto: (editorState, node) { + final nodes = editorState.document.root.children; + expect(nodes.length, 6); + final texts = [ + text1, + nestedText1, + text2, + nestedText2, + text3, + nestedText3, + ]; + final types = [ + type, + NumberedListBlockKeys.type, + type, + BulletedListBlockKeys.type, + type, + TodoListBlockKeys.type, + ]; + for (var i = 0; i < 6; i++) { + expect(nodes[i].type, types[i]); + expect(nodes[i].children.length, 0); + expect(nodes[i].delta!.toPlainText(), texts[i]); + } + }, + ); + }); + } + + for (final type in [ + ParagraphBlockKeys.type, + BulletedListBlockKeys.type, + NumberedListBlockKeys.type, + TodoListBlockKeys.type, + ]) { + // numbered list, bulleted list, todo list + // before + // - numbered list 1 + // - nested list 1 + // - bulleted list 2 + // - nested list 2 + // - todo list 3 + // - nested list 3 + // after + // - new_list_type + // - nested list 1 + // - new_list_type + // - nested list 2 + // - new_list_type + // - nested list 3 + test('from nested mixed list to $type', () async { + const text1 = 'numbered list 1'; + const text2 = 'bulleted list 2'; + const text3 = 'todo list 3'; + const nestedText1 = 'nested list 1'; + const nestedText2 = 'nested list 2'; + const nestedText3 = 'nested list 3'; + final document = createDocument([ + numberedListNode( + delta: Delta()..insert(text1), + children: [ + numberedListNode( + delta: Delta()..insert(nestedText1), + ), + ], + ), + bulletedListNode( + delta: Delta()..insert(text2), + children: [ + bulletedListNode( + delta: Delta()..insert(nestedText2), + ), + ], + ), + todoListNode( + checked: false, + text: text3, + children: [ + todoListNode( + checked: false, + text: nestedText3, + ), + ], + ), + ]); + await checkTurnInto( + document, + NumberedListBlockKeys.type, + text1, + toType: type, + selection: Selection( + start: Position(path: [0]), + end: Position(path: [2]), + ), + afterTurnInto: (editorState, node) { + final nodes = editorState.document.root.children; + expect(nodes.length, 3); + final texts = [ + text1, + text2, + text3, + ]; + final nestedTexts = [ + nestedText1, + nestedText2, + nestedText3, + ]; + final types = [ + NumberedListBlockKeys.type, + BulletedListBlockKeys.type, + TodoListBlockKeys.type, + ]; + for (var i = 0; i < 3; i++) { + expect(nodes[i].type, type); + expect(nodes[i].children.length, 1); + expect(nodes[i].delta!.toPlainText(), texts[i]); + expect(nodes[i].children[0].type, types[i]); + expect(nodes[i].children[0].delta!.toPlainText(), nestedTexts[i]); + } + }, + ); + }); + } + + test('undo, redo', () async { + const text1 = 'numbered list 1'; + const nestedText1 = 'nested list 1'; final document = createDocument([ numberedListNode( - delta: Delta()..insert(text), + delta: Delta()..insert(text1), children: [ numberedListNode( delta: Delta()..insert(nestedText1), ), - numberedListNode( - delta: Delta()..insert(nestedText2), - ), - numberedListNode( - delta: Delta()..insert(nestedText3), - ), ], ), ]); await checkTurnInto( document, NumberedListBlockKeys.type, - text, + text1, toType: HeadingBlockKeys.type, afterTurnInto: (editorState, node) { - expect(node.type, HeadingBlockKeys.type); - expect(node.children.length, 0); - expect(node.delta!.toPlainText(), text); - - expect(editorState.document.root.children.length, 4); - expect( - editorState.document.root.children[1].type, - NumberedListBlockKeys.type, + expect(editorState.document.root.children.length, 2); + editorState.selection = Selection.collapsed( + Position(path: [0]), ); - expect( - editorState.document.root.children[1].delta!.toPlainText(), - nestedText1, - ); - expect( - editorState.document.root.children[2].type, - NumberedListBlockKeys.type, - ); - expect( - editorState.document.root.children[2].delta!.toPlainText(), - nestedText2, - ); - expect( - editorState.document.root.children[3].type, - NumberedListBlockKeys.type, - ); - expect( - editorState.document.root.children[3].delta!.toPlainText(), - nestedText3, + KeyEventResult result = undoCommand.execute(editorState); + expect(result, KeyEventResult.handled); + expect(editorState.document.root.children.length, 1); + editorState.selection = Selection.collapsed( + Position(path: [0]), ); + result = redoCommand.execute(editorState); + expect(result, KeyEventResult.handled); + expect(editorState.document.root.children.length, 2); }, ); });