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

384 lines
10 KiB
Dart
Raw Normal View History

2023-04-28 10:54:21 +08:00
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/ime/text_input_service.dart';
2023-04-28 10:54:21 +08:00
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';
feat: find dialog 1760 (#106) * feat: make find menu widget * feat: service for find menu * feat: add find menu shortcut event * feat: create a search service * docs: explain search service * fix: unhighlight method takes searched word * fix: unhighlight before each search * feat: navigate between matches * feat: forget highlighting from undo stack * feat: replace logic and ui * feat: replace shortcut handler and widget * test: find functionality * test: replace menu tests * refactor: separate class for search algo * refactor: suggested changes * refactor: remove unhighlight method * feat: add find highlight color * refactor: name of the search algo class * test: unit tests for search algorithm * chore: simplify syntax * test: renamed test group * refactor: move to editor * refactor: add shortcut for find * refactor: use new api * refactor: add abstrac class search algo * fix: avoid multiple instances of find dialog * refactor: xazin's suggestions * chore: separately build input decor * test: search algorithm * chore: unhighlight properly * refactor: replace handler * refactor: move tests into new * test: find menu widget test * test: replace menu * fix: localizations + resolve scroll bug partially * fix: without update selection on highlight * fix: do not select words when highlighting * test: update expected selection * fix: matches and styling * feat: separate attribute for highlighting * feat: unique color for selected match * test: unique color for selected match * refactor: cleaning the code * fix: navigate to first match * fix: tests and replace logic --------- Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com> Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2023-08-10 16:38:45 +05:30
import '../find_replace_menu/find_replace_menu_utils.dart';
2023-04-28 10:54:21 +08:00
import '../util/util.dart';
2023-09-27 13:04:33 +05:30
final floatingToolbarItems = [
paragraphItem,
...headingItems,
...markdownFormatItems,
quoteItem,
bulletedListItem,
numberedListItem,
linkItem,
buildTextColorItem(),
buildHighlightColorItem(),
];
2023-04-28 10:54:21 +08:00
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'),
2023-05-02 23:01:48 +08:00
bool autoFocus = false,
bool editable = true,
bool shrinkWrap = false,
bool withFloatingToolbar = false,
2023-07-07 16:07:47 +08:00
bool inMobile = false,
ScrollController? scrollController,
Widget Function(Widget child)? wrapper,
TargetPlatform? platform,
String? defaultTextDirection,
2023-09-27 13:04:33 +05:30
TextDirection textDirection = TextDirection.ltr,
2023-04-28 10:54:21 +08:00
}) async {
await AppFlowyEditorLocalizations.load(locale);
if (withFloatingToolbar) {
scrollController ??= ScrollController();
}
2023-09-11 15:03:14 +08:00
final editorScrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: shrinkWrap,
scrollController: scrollController,
);
feat: find dialog 1760 (#106) * feat: make find menu widget * feat: service for find menu * feat: add find menu shortcut event * feat: create a search service * docs: explain search service * fix: unhighlight method takes searched word * fix: unhighlight before each search * feat: navigate between matches * feat: forget highlighting from undo stack * feat: replace logic and ui * feat: replace shortcut handler and widget * test: find functionality * test: replace menu tests * refactor: separate class for search algo * refactor: suggested changes * refactor: remove unhighlight method * feat: add find highlight color * refactor: name of the search algo class * test: unit tests for search algorithm * chore: simplify syntax * test: renamed test group * refactor: move to editor * refactor: add shortcut for find * refactor: use new api * refactor: add abstrac class search algo * fix: avoid multiple instances of find dialog * refactor: xazin's suggestions * chore: separately build input decor * test: search algorithm * chore: unhighlight properly * refactor: replace handler * refactor: move tests into new * test: find menu widget test * test: replace menu * fix: localizations + resolve scroll bug partially * fix: without update selection on highlight * fix: do not select words when highlighting * test: update expected selection * fix: matches and styling * feat: separate attribute for highlighting * feat: unique color for selected match * test: unique color for selected match * refactor: cleaning the code * fix: navigate to first match * fix: tests and replace logic --------- Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com> Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2023-08-10 16:38:45 +05:30
Widget editor = Builder(
builder: (context) {
return AppFlowyEditor(
editorState: editorState,
editable: editable,
autoFocus: autoFocus,
shrinkWrap: shrinkWrap,
2023-09-11 15:03:14 +08:00
editorScrollController: editorScrollController,
feat: find dialog 1760 (#106) * feat: make find menu widget * feat: service for find menu * feat: add find menu shortcut event * feat: create a search service * docs: explain search service * fix: unhighlight method takes searched word * fix: unhighlight before each search * feat: navigate between matches * feat: forget highlighting from undo stack * feat: replace logic and ui * feat: replace shortcut handler and widget * test: find functionality * test: replace menu tests * refactor: separate class for search algo * refactor: suggested changes * refactor: remove unhighlight method * feat: add find highlight color * refactor: name of the search algo class * test: unit tests for search algorithm * chore: simplify syntax * test: renamed test group * refactor: move to editor * refactor: add shortcut for find * refactor: use new api * refactor: add abstrac class search algo * fix: avoid multiple instances of find dialog * refactor: xazin's suggestions * chore: separately build input decor * test: search algorithm * chore: unhighlight properly * refactor: replace handler * refactor: move tests into new * test: find menu widget test * test: replace menu * fix: localizations + resolve scroll bug partially * fix: without update selection on highlight * fix: do not select words when highlighting * test: update expected selection * fix: matches and styling * feat: separate attribute for highlighting * feat: unique color for selected match * test: unique color for selected match * refactor: cleaning the code * fix: navigate to first match * fix: tests and replace logic --------- Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com> Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2023-08-10 16:38:45 +05:30
commandShortcutEvents: [
...standardCommandShortcutEvents,
...TestableFindAndReplaceCommands(context: context)
.testableFindAndReplaceCommands,
],
characterShortcutEvents: [
...standardCharacterShortcutEvents,
formatGreaterHyphen,
],
contextMenuBuilder: (context, position, editorState, onPressed) {
return ContextMenu(
position: position,
editorState: editorState,
items: standardContextMenuItems,
onPressed: onPressed,
);
},
feat: find dialog 1760 (#106) * feat: make find menu widget * feat: service for find menu * feat: add find menu shortcut event * feat: create a search service * docs: explain search service * fix: unhighlight method takes searched word * fix: unhighlight before each search * feat: navigate between matches * feat: forget highlighting from undo stack * feat: replace logic and ui * feat: replace shortcut handler and widget * test: find functionality * test: replace menu tests * refactor: separate class for search algo * refactor: suggested changes * refactor: remove unhighlight method * feat: add find highlight color * refactor: name of the search algo class * test: unit tests for search algorithm * chore: simplify syntax * test: renamed test group * refactor: move to editor * refactor: add shortcut for find * refactor: use new api * refactor: add abstrac class search algo * fix: avoid multiple instances of find dialog * refactor: xazin's suggestions * chore: separately build input decor * test: search algorithm * chore: unhighlight properly * refactor: replace handler * refactor: move tests into new * test: find menu widget test * test: replace menu * fix: localizations + resolve scroll bug partially * fix: without update selection on highlight * fix: do not select words when highlighting * test: update expected selection * fix: matches and styling * feat: separate attribute for highlighting * feat: unique color for selected match * test: unique color for selected match * refactor: cleaning the code * fix: navigate to first match * fix: tests and replace logic --------- Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com> Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2023-08-10 16:38:45 +05:30
editorStyle: inMobile
? EditorStyle.mobile(
defaultTextDirection: defaultTextDirection,
)
: EditorStyle.desktop(
defaultTextDirection: defaultTextDirection,
),
feat: find dialog 1760 (#106) * feat: make find menu widget * feat: service for find menu * feat: add find menu shortcut event * feat: create a search service * docs: explain search service * fix: unhighlight method takes searched word * fix: unhighlight before each search * feat: navigate between matches * feat: forget highlighting from undo stack * feat: replace logic and ui * feat: replace shortcut handler and widget * test: find functionality * test: replace menu tests * refactor: separate class for search algo * refactor: suggested changes * refactor: remove unhighlight method * feat: add find highlight color * refactor: name of the search algo class * test: unit tests for search algorithm * chore: simplify syntax * test: renamed test group * refactor: move to editor * refactor: add shortcut for find * refactor: use new api * refactor: add abstrac class search algo * fix: avoid multiple instances of find dialog * refactor: xazin's suggestions * chore: separately build input decor * test: search algorithm * chore: unhighlight properly * refactor: replace handler * refactor: move tests into new * test: find menu widget test * test: replace menu * fix: localizations + resolve scroll bug partially * fix: without update selection on highlight * fix: do not select words when highlighting * test: update expected selection * fix: matches and styling * feat: separate attribute for highlighting * feat: unique color for selected match * test: unique color for selected match * refactor: cleaning the code * fix: navigate to first match * fix: tests and replace logic --------- Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com> Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2023-08-10 16:38:45 +05:30
);
},
2023-04-28 10:54:21 +08:00
);
2023-09-11 15:03:14 +08:00
if (withFloatingToolbar) {
2023-07-07 16:07:47 +08:00
if (inMobile) {
final items = [
textDecorationMobileToolbarItem,
headingMobileToolbarItem,
todoListMobileToolbarItem,
listMobileToolbarItem,
linkMobileToolbarItem,
quoteMobileToolbarItem,
codeMobileToolbarItem,
];
editor = Column(
children: [
Expanded(
child: AppFlowyEditor(
editorStyle: const EditorStyle.mobile(),
editorState: editorState,
),
),
MobileToolbar(
editorState: editorState,
toolbarItems: items,
),
],
);
} else {
editor = FloatingToolbar(
2023-09-27 13:04:33 +05:30
items: floatingToolbarItems,
2023-07-07 16:07:47 +08:00
editorState: editorState,
2023-09-27 13:04:33 +05:30
textDirection: textDirection,
2023-09-11 15:03:14 +08:00
editorScrollController: editorScrollController,
2023-07-07 16:07:47 +08:00
child: editor,
);
}
}
2023-09-27 13:04:33 +05:30
editor = Directionality(
textDirection: textDirection,
child: editor,
);
2023-04-28 10:54:21 +08:00
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
2023-04-28 10:54:21 +08:00
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
AppFlowyEditorLocalizations.delegate,
],
supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales,
locale: locale,
home: Scaffold(
body: wrapper == null
? editor
: wrapper(
editor,
),
2023-04-28 10:54:21 +08:00
),
),
);
await tester.pump();
return this;
}
void initialize() {
_editorState = EditorState(
document: Document.blank(),
);
}
void initializeWithDocument(Document document) {
_editorState = EditorState(
document: document,
);
}
2023-04-28 10:54:21 +08:00
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 {
_editorState.updateSelectionWithReason(
selection,
reason: SelectionUpdateReason.uiEvent,
);
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: ' ',
LogicalKeyboardKey.backquote: '`',
LogicalKeyboardKey.tilde: '~',
LogicalKeyboardKey.asterisk: '*',
LogicalKeyboardKey.underscore: '_',
2023-05-02 14:54:06 +08:00
};
2023-05-03 11:10:17 +08:00
Future<void> pressKey({
2023-05-01 14:37:25 +08:00
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);
}
2023-05-03 11:10:17 +08:00
} else if (character != null) {
await ime.typeText(character);
2023-05-01 14:37:25 +08:00
}
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) {
await insertText(text);
2023-05-02 17:14:26 +08:00
} else {
await replaceText(text);
2023-05-02 17:14:26 +08:00
}
}
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;
}
await imeInput.apply([
2023-05-02 14:54:06 +08:00
TextEditingDeltaInsertion(
oldText: ' ${delta.toPlainText()}',
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,
2023-09-13 15:26:29 +08:00
),
2023-05-02 17:14:26 +08:00
]);
await tester.pumpAndSettle();
2023-05-02 17:14:26 +08:00
}
Future<void> replaceText(String text) async {
var selection = editorState.selection?.normalized;
if (selection == null) {
2023-05-02 17:14:26 +08:00
return;
}
if (!selection.isSingle || !selection.isCollapsed) {
await editorState.deleteSelection(selection);
}
selection = editorState.selection?.normalized;
if (selection == null) {
return;
}
final oldText =
editorState.getNodeAtPath(selection.start.path)!.delta!.toPlainText();
await imeInput.apply([
2023-05-02 17:14:26 +08:00
TextEditingDeltaReplacement(
oldText: ' $oldText',
2023-05-02 17:14:26 +08:00
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,
2023-09-13 15:26:29 +08:00
),
2023-05-02 14:54:06 +08:00
]);
await tester.pumpAndSettle();
2023-05-02 14:54:06 +08:00
}
}