feat: turn into multiple lines (#6558)

* feat: select multiple lines with block selection style

* feat: multiple nodes conversion

* fix: exclude children for the block can't contain children

* chore: update editor version

* fix: unit test

* test: convert nested list to heading/quote/callout

* test: transform nodes at the same level into another block type

* test: add undo redo for turn into

* test: add multi lines integration test

* chore: remove debug logs

* fix: integration test
This commit is contained in:
Lucas 2024-10-16 22:19:18 +08:00 committed by GitHub
parent b1682e4f54
commit a8bcab7770
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 528 additions and 161 deletions

View File

@ -59,7 +59,7 @@ void main() {
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty); expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
}); });
testWidgets('turn into', (tester) async { testWidgets('turn into - single line', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton(); 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,
);
}
});
}); });
} }

View File

@ -132,6 +132,11 @@ void _customBlockOptionActions(
if (UniversalPlatform.isDesktop) { if (UniversalPlatform.isDesktop) {
builder.showActions = builder.showActions =
(node) => node.parent?.type != TableCellBlockKeys.type; (node) => node.parent?.type != TableCellBlockKeys.type;
builder.configuration = builder.configuration.copyWith(
blockSelectionAreaMargin: (_) => const EdgeInsets.symmetric(
vertical: 1,
),
);
builder.actionBuilder = (context, state) { builder.actionBuilder = (context, state) {
final top = builder.configuration.padding(context.node).top; final top = builder.configuration.padding(context.node).top;

View File

@ -209,44 +209,60 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
Node node, { Node node, {
int? level, int? level,
}) async { }) async {
final toType = type; final selection = editorState.selection;
if (selection == null) {
Log.info(
'Turn into block: from ${node.type} to $type',
);
if (type == node.type && type != HeadingBlockKeys.type) {
Log.info('Block type is the same');
return false; return false;
} }
Node afterNode = node.copyWith( final toType = type;
type: type,
attributes: { // only handle the node in the same depth
if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level, final selectedNodes = editorState
if (toType == TodoListBlockKeys.type) TodoListBlockKeys.checked: false, .getNodesInSelection(selection.normalized)
blockComponentBackgroundColor: .where((e) => e.path.length == node.path.length)
node.attributes[blockComponentBackgroundColor], .toList();
blockComponentTextDirection: Log.info('turnIntoBlock selectedNodes $selectedNodes');
node.attributes[blockComponentTextDirection],
blockComponentDelta: (node.delta ?? Delta()).toJson(), final insertedNode = <Node>[];
},
); for (final node in selectedNodes) {
final insertedNode = []; Log.info(
// heading block and callout block should not have children 'Turn into block: from ${node.type} to $type',
if ([HeadingBlockKeys.type, CalloutBlockKeys.type].contains(toType)) {
afterNode = afterNode.copyWith(
children: [],
); );
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; final transaction = editorState.transaction;
transaction.insertNodes(node.path, [ transaction.insertNodes(
afterNode, node.path,
...insertedNode, insertedNode,
]); );
transaction.deleteNode(node); transaction.deleteNodes(selectedNodes);
await editorState.apply(transaction); await editorState.apply(transaction);
return true; return true;

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.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:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
// this flag is used to disable the tooltip of the block when it is dragged // 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({ const _OptionButton({
required this.controller, required this.controller,
required this.editorState, required this.editorState,
@ -261,10 +260,45 @@ class _OptionButton extends StatelessWidget {
final BlockComponentContext blockComponentContext; final BlockComponentContext blockComponentContext;
final ValueNotifier<bool> isDragging; final ValueNotifier<bool> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: isDragging, valueListenable: widget.isDragging,
builder: (context, isDragging, child) { builder: (context, isDragging, child) {
return BlockActionButton( return BlockActionButton(
svg: FlowySvgs.drag_element_s, svg: FlowySvgs.drag_element_s,
@ -291,7 +325,11 @@ class _OptionButton extends StatelessWidget {
], ],
), ),
onTap: () { onTap: () {
controller.show(); if (widget.editorState.selection != null) {
beforeSelection = widget.editorState.selection;
}
widget.controller.show();
// update selection // update selection
_updateBlockSelection(); _updateBlockSelection();
@ -302,23 +340,35 @@ class _OptionButton extends StatelessWidget {
} }
void _updateBlockSelection() { void _updateBlockSelection() {
final startNode = blockComponentContext.node; if (beforeSelection == null) {
var endNode = startNode; final path = widget.blockComponentContext.node.path;
while (endNode.children.isNotEmpty) { final selection = Selection.collapsed(
endNode = endNode.children.last; 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 localPosition = renderBox!.globalToLocal(offset);
final end = endNode.selectable?.end() ?? final result = renderBox!.paintBounds.contains(localPosition);
Position( if (result) {
path: endNode.path, beforeSelection = widget.editorState.selection;
offset: endNode.delta?.length ?? 0, } else {
); beforeSelection = null;
}
editorState.selectionType = SelectionType.block; return result;
editorState.selection = Selection(
start: start,
end: end,
);
} }
} }

View File

@ -53,8 +53,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: a0b3c72 ref: bcd1208
resolved-ref: a0b3c7289b8c4073b47793f665e70a511324f9b9 resolved-ref: bcd12082fea75cdbbea8b090bf938aae7dc9f4ad
url: "https://github.com/AppFlowy-IO/appflowy-editor.git" url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git source: git
version: "4.0.0" version: "4.0.0"
@ -1535,10 +1535,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.4" version: "3.1.5"
plugin_platform_interface: plugin_platform_interface:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1933,10 +1933,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
string_validator: string_validator:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2238,10 +2238,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.1" version: "14.2.5"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -43,7 +43,7 @@ dependencies:
# Desktop Drop uses Cross File (XFile) data type # Desktop Drop uses Cross File (XFile) data type
desktop_drop: ^0.4.4 desktop_drop: ^0.4.4
device_info_plus: ^10.1.0 device_info_plus:
dotted_border: ^2.0.0+3 dotted_border: ^2.0.0+3
easy_localization: ^3.0.2 easy_localization: ^3.0.2
envied: ^0.5.2 envied: ^0.5.2
@ -161,6 +161,7 @@ dev_dependencies:
dependency_overrides: dependency_overrides:
http: ^1.0.0 http: ^1.0.0
device_info_plus: ^10.1.0
url_protocol: url_protocol:
git: git:
@ -170,7 +171,7 @@ dependency_overrides:
appflowy_editor: appflowy_editor:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "a0b3c72" ref: "bcd1208"
appflowy_editor_plugins: appflowy_editor_plugins:
git: git:

View File

@ -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/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
@ -16,6 +17,7 @@ void main() {
Document document, Document document,
String originalType, String originalType,
String originalText, { String originalText, {
Selection? selection,
String? toType, String? toType,
void Function(EditorState editorState, Node node)? afterTurnInto, void Function(EditorState editorState, Node node)? afterTurnInto,
}) async { }) async {
@ -33,6 +35,12 @@ void main() {
continue; continue;
} }
editorState.selectionType = SelectionType.block;
editorState.selection = selection ??
Selection.collapsed(
Position(path: [0]),
);
final node = editorState.getNodeAtPath([0])!; final node = editorState.getNodeAtPath([0])!;
expect(node.type, originalType); expect(node.type, originalType);
final result = await cubit.turnIntoBlock( final result = await cubit.turnIntoBlock(
@ -49,6 +57,11 @@ void main() {
); );
// turn it back the originalType for the next test // turn it back the originalType for the next test
editorState.selectionType = SelectionType.block;
editorState.selection = selection ??
Selection.collapsed(
Position(path: [0]),
);
await cubit.turnIntoBlock( await cubit.turnIntoBlock(
originalType, originalType,
newNode, newNode,
@ -165,122 +178,358 @@ void main() {
); );
}); });
test('from nested list to heading', () async { for (final type in [
const text = 'bulleted list'; HeadingBlockKeys.type,
const nestedText1 = 'nested bulleted list 1'; QuoteBlockKeys.type,
const nestedText2 = 'nested bulleted list 2'; CalloutBlockKeys.type,
const nestedText3 = 'nested bulleted list 3'; ]) {
final document = createDocument([ test('from nested bulleted list to $type', () async {
bulletedListNode( const text = 'bulleted list';
text: text, const nestedText1 = 'nested bulleted list 1';
children: [ const nestedText2 = 'nested bulleted list 2';
bulletedListNode( const nestedText3 = 'nested bulleted list 3';
text: nestedText1, final document = createDocument([
), bulletedListNode(
bulletedListNode( text: text,
text: nestedText2, children: [
), bulletedListNode(
bulletedListNode( text: nestedText1,
text: nestedText3, ),
), bulletedListNode(
], text: nestedText2,
), ),
]); bulletedListNode(
await checkTurnInto( text: nestedText3,
document, ),
BulletedListBlockKeys.type, ],
text, ),
toType: HeadingBlockKeys.type, ]);
afterTurnInto: (editorState, node) { await checkTurnInto(
expect(node.type, HeadingBlockKeys.type); document,
expect(node.children.length, 0); BulletedListBlockKeys.type,
expect(node.delta!.toPlainText(), text); 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.length, 4);
expect( expect(
editorState.document.root.children[1].type, editorState.document.root.children[1].type,
BulletedListBlockKeys.type, BulletedListBlockKeys.type,
); );
expect( expect(
editorState.document.root.children[1].delta!.toPlainText(), editorState.document.root.children[1].delta!.toPlainText(),
nestedText1, nestedText1,
); );
expect( expect(
editorState.document.root.children[2].type, editorState.document.root.children[2].type,
BulletedListBlockKeys.type, BulletedListBlockKeys.type,
); );
expect( expect(
editorState.document.root.children[2].delta!.toPlainText(), editorState.document.root.children[2].delta!.toPlainText(),
nestedText2, nestedText2,
); );
expect( expect(
editorState.document.root.children[3].type, editorState.document.root.children[3].type,
BulletedListBlockKeys.type, BulletedListBlockKeys.type,
); );
expect( expect(
editorState.document.root.children[3].delta!.toPlainText(), editorState.document.root.children[3].delta!.toPlainText(),
nestedText3, nestedText3,
); );
}, },
); );
}); });
}
test('from numbered list to heading', () async { for (final type in [
const text = 'numbered list'; HeadingBlockKeys.type,
const nestedText1 = 'nested numbered list 1'; QuoteBlockKeys.type,
const nestedText2 = 'nested numbered list 2'; CalloutBlockKeys.type,
const nestedText3 = 'nested numbered list 3'; ]) {
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([ final document = createDocument([
numberedListNode( numberedListNode(
delta: Delta()..insert(text), delta: Delta()..insert(text1),
children: [ children: [
numberedListNode( numberedListNode(
delta: Delta()..insert(nestedText1), delta: Delta()..insert(nestedText1),
), ),
numberedListNode(
delta: Delta()..insert(nestedText2),
),
numberedListNode(
delta: Delta()..insert(nestedText3),
),
], ],
), ),
]); ]);
await checkTurnInto( await checkTurnInto(
document, document,
NumberedListBlockKeys.type, NumberedListBlockKeys.type,
text, text1,
toType: HeadingBlockKeys.type, toType: HeadingBlockKeys.type,
afterTurnInto: (editorState, node) { afterTurnInto: (editorState, node) {
expect(node.type, HeadingBlockKeys.type); expect(editorState.document.root.children.length, 2);
expect(node.children.length, 0); editorState.selection = Selection.collapsed(
expect(node.delta!.toPlainText(), text); Position(path: [0]),
expect(editorState.document.root.children.length, 4);
expect(
editorState.document.root.children[1].type,
NumberedListBlockKeys.type,
); );
expect( KeyEventResult result = undoCommand.execute(editorState);
editorState.document.root.children[1].delta!.toPlainText(), expect(result, KeyEventResult.handled);
nestedText1, expect(editorState.document.root.children.length, 1);
); editorState.selection = Selection.collapsed(
expect( Position(path: [0]),
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,
); );
result = redoCommand.execute(editorState);
expect(result, KeyEventResult.handled);
expect(editorState.document.root.children.length, 2);
}, },
); );
}); });