appflowy-editor/test/new/infra/testable_editor.dart

316 lines
8.2 KiB
Dart
Raw Normal View History

2023-04-28 10:54:21 +08:00
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
2023-05-01 14:37:25 +08:00
import 'package:flutter/services.dart';
2023-04-28 10:54:21 +08:00
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util/util.dart';
class TestableEditor {
TestableEditor({
required this.tester,
});
final WidgetTester tester;
EditorState get editorState => _editorState;
late EditorState _editorState;
Document get document => _editorState.document;
int get documentRootLen => document.root.children.length;
2023-04-28 11:12:49 +08:00
Selection? get selection => _editorState.selection;
2023-05-02 14:54:06 +08:00
MockIMEInput? _ime;
MockIMEInput get ime {
return _ime ??= MockIMEInput(
editorState: editorState,
tester: tester,
);
}
2023-04-28 10:54:21 +08:00
Future<TestableEditor> startTesting({
Locale locale = const Locale('en'),
}) async {
final editor = AppFlowyEditor(
editorState: editorState,
blockComponentBuilders: {
'document': DocumentComponentBuilder(),
'paragraph': TextBlockComponentBuilder(),
'todo_list': TodoListBlockComponentBuilder(),
'bulleted_list': BulletedListBlockComponentBuilder(),
'numbered_list': NumberedListBlockComponentBuilder(),
'quote': QuoteBlockComponentBuilder(),
'heading': HeadingBlockComponentBuilder(),
2023-04-28 10:54:21 +08:00
},
characterShortcutEvents: [
2023-05-02 14:54:06 +08:00
// '\n'
insertNewLineAfterBulletedList,
insertNewLineAfterTodoList,
insertNewLineAfterNumberedList,
2023-04-28 10:54:21 +08:00
insertNewLine,
2023-05-02 14:54:06 +08:00
// bulleted list
2023-04-28 10:54:21 +08:00
formatAsteriskToBulletedList,
formatMinusToBulletedList,
2023-05-02 14:54:06 +08:00
// numbered list
2023-04-28 10:54:21 +08:00
formatNumberToNumberedList,
2023-05-02 14:54:06 +08:00
// quote
2023-04-28 10:54:21 +08:00
formatGreaterToQuote,
2023-05-02 14:54:06 +08:00
// heading
formatSignToHeading,
2023-05-02 14:54:06 +08:00
// checkbox
// format unchecked box, [] or -[]
formatEmptyBracketsToUncheckedBox,
formatHyphenEmptyBracketsToUncheckedBox,
// format checked box, [x] or -[x]
formatFilledBracketsToCheckedBox,
formatHyphenFilledBracketsToCheckedBox,
// slash
2023-04-28 10:54:21 +08:00
slashCommand,
2023-05-02 14:54:06 +08:00
// markdown syntax
...markdownSyntaxShortcutEvents,
2023-04-28 10:54:21 +08:00
],
commandShortcutEvents: [
2023-05-02 14:54:06 +08:00
// backspace
convertToParagraphCommand,
2023-04-28 10:54:21 +08:00
backspaceCommand,
deleteLeftWordCommand,
deleteLeftSentenceCommand,
2023-05-02 14:54:06 +08:00
// arrow keys
2023-05-01 14:37:25 +08:00
...arrowLeftKeys,
...arrowRightKeys,
2023-05-01 15:24:51 +08:00
...arrowUpKeys,
2023-05-01 15:32:39 +08:00
...arrowDownKeys,
2023-05-02 14:54:06 +08:00
//
homeCommand,
endCommand,
toggleTodoListCommand,
...toggleMarkdownCommands,
2023-05-02 17:14:26 +08:00
//
indentCommand,
outdentCommand,
2023-05-02 17:19:38 +08:00
exitEditingCommand,
2023-04-28 10:54:21 +08:00
],
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
AppFlowyEditorLocalizations.delegate,
],
supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales,
locale: locale,
home: Scaffold(
body: editor,
),
),
);
await tester.pump();
return this;
}
void initialize() {
_editorState = EditorState(
document: Document.blank(),
);
}
Future<void> dispose() async {
2023-05-02 14:54:06 +08:00
_ime = null;
2023-04-28 10:54:21 +08:00
// Workaround: to wait all the debounce calls expire.
// https://github.com/flutter/flutter/issues/11181#issuecomment-568737491
await tester.pumpAndSettle(const Duration(seconds: 1));
}
2023-05-01 15:13:55 +08:00
void addNode(Node node) {
_editorState.document.root.insert(node);
}
2023-04-28 10:54:21 +08:00
void addParagraph({
TextBuilder? builder,
String? initialText,
NodeDecorator? decorator,
}) {
2023-04-28 11:12:49 +08:00
addParagraphs(
1,
builder: builder,
initialText: initialText,
decorator: decorator,
);
}
void addParagraphs(
int count, {
TextBuilder? builder,
String? initialText,
NodeDecorator? decorator,
}) {
_editorState.document.addParagraphs(
count,
2023-04-28 10:54:21 +08:00
builder: builder,
initialText: initialText,
decorator: decorator,
);
}
2023-05-01 15:13:55 +08:00
void addEmptyParagraph() {
2023-04-28 10:54:21 +08:00
_editorState.document.addParagraph(initialText: '');
}
Future<void> updateSelection(Selection? selection) async {
2023-04-28 10:54:21 +08:00
_editorState.selection = selection;
await tester.pumpAndSettle();
2023-04-28 10:54:21 +08:00
}
Node? nodeAtPath(Path path) {
return _editorState.getNodeAtPath(path);
}
2023-05-01 14:37:25 +08:00
2023-05-02 14:54:06 +08:00
final keyToCharacterMap = {
LogicalKeyboardKey.space: ' ',
2023-05-02 17:14:26 +08:00
LogicalKeyboardKey.enter: '\n',
LogicalKeyboardKey.backquote: '`',
LogicalKeyboardKey.tilde: '~',
LogicalKeyboardKey.asterisk: '*',
LogicalKeyboardKey.underscore: '_',
2023-05-02 14:54:06 +08:00
};
2023-05-01 14:37:25 +08:00
Future<void> pressLogicKey({
String? character,
LogicalKeyboardKey? key,
bool isControlPressed = false,
bool isShiftPressed = false,
bool isAltPressed = false,
bool isMetaPressed = false,
}) async {
if (key != null) {
if (isControlPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.control);
}
if (isShiftPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.shift);
}
if (isAltPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.alt);
}
if (isMetaPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.meta);
}
2023-05-02 14:54:06 +08:00
if (keyToCharacterMap.containsKey(key)) {
final character = keyToCharacterMap[key]!;
2023-05-02 17:14:26 +08:00
await ime.typeText(character);
2023-05-02 14:54:06 +08:00
} else {
await simulateKeyDownEvent(key);
await simulateKeyUpEvent(key);
}
2023-05-01 14:37:25 +08:00
if (isControlPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.control);
}
if (isShiftPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
}
if (isAltPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.alt);
}
if (isMetaPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.meta);
}
}
await tester.pumpAndSettle();
}
2023-04-28 10:54:21 +08:00
}
extension TestableEditorExtension on WidgetTester {
TestableEditor get editor => TestableEditor(tester: this)..initialize();
EditorState get editorState => editor.editorState;
}
2023-05-02 14:54:06 +08:00
class MockIMEInput {
MockIMEInput({
required this.editorState,
required this.tester,
});
final EditorState editorState;
final WidgetTester tester;
TextInputService get imeInput {
final keyboardService = tester.state(find.byType(KeyboardServiceWidget))
as KeyboardServiceWidgetState;
return keyboardService.textInputService;
}
2023-05-02 17:14:26 +08:00
Future<void> typeText(String text) async {
final selection = editorState.selection;
if (selection == null) {
return;
}
// if the selection is collapsed, do insertion.
// else if the selection is not collapsed, do replacement.
if (selection.isCollapsed) {
return insertText(text);
} else {
return replaceText(text);
}
}
2023-05-02 14:54:06 +08:00
Future<void> insertText(String text) async {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final node = editorState.getNodeAtPath(selection.end.path);
final delta = node?.delta;
if (delta == null) {
return;
}
return imeInput.apply([
TextEditingDeltaInsertion(
2023-05-02 17:14:26 +08:00
oldText: ' ${delta.toPlainText()}', // TODO: fix this workaround
2023-05-02 14:54:06 +08:00
textInserted: text,
2023-05-02 17:14:26 +08:00
insertionOffset: selection.startIndex + 1,
selection: TextSelection.collapsed(
offset: selection.startIndex + 1 + text.length,
),
composing: TextRange.empty,
)
]);
}
Future<void> replaceText(String text) async {
final selection = editorState.selection?.normalized;
if (selection == null || selection.isCollapsed) {
return;
}
final texts = editorState.getTextInSelection(selection).join('\n');
return imeInput.apply([
TextEditingDeltaReplacement(
oldText: ' $texts',
replacementText: text,
replacedRange: TextSelection(
baseOffset: selection.startIndex + 1,
extentOffset: selection.endIndex + 1,
),
selection: TextSelection.collapsed(
offset: selection.startIndex + 1 + text.length,
),
2023-05-02 14:54:06 +08:00
composing: TextRange.empty,
)
]);
}
}