refactor: migrate tests (#438)

* refactor: separate remove line command

* test: delete sentence

* chore: export delete left line command

* refactor: move from internal_key_handler to new

* refactor: move from command to new
This commit is contained in:
Mayur Mahajan 2023-09-11 11:54:50 +05:30 • committed by GitHub
commit ad55171c5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 627 additions and 649 deletions

View file

@ -14,35 +14,6 @@ final CommandShortcutEvent backspaceCommand = CommandShortcutEvent(
handler: _backspaceCommandHandler,
);
final CommandShortcutEvent deleteLeftSentenceCommand = CommandShortcutEvent(
key: 'delete the left word',
command: 'ctrl+alt+backspace',
macOSCommand: 'cmd+backspace',
handler: _deleteLeftSentenceCommandHandler,
);
CommandShortcutEventHandler _deleteLeftSentenceCommandHandler = (editorState) {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return KeyEventResult.ignored;
}
final node = editorState.getNodeAtPath(selection.end.path);
final delta = node?.delta;
if (node == null || delta == null) {
return KeyEventResult.ignored;
}
final transaction = editorState.transaction;
transaction.deleteText(
node,
0,
selection.endIndex,
);
editorState.apply(transaction);
return KeyEventResult.handled;
};
CommandShortcutEventHandler _backspaceCommandHandler = (editorState) {
final selection = editorState.selection;
if (selection == null) {

View file

@ -17,6 +17,7 @@ export 'page_down_command.dart';
export 'page_up_command.dart';
export 'paste_command.dart';
export 'remove_word_command.dart';
export 'remove_line_command.dart';
export 'select_all_command.dart';
export 'show_link_menu_command.dart';
export 'toggle_colors_command.dart';

View file

@ -0,0 +1,39 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
/// Remove Line to the Left.
///
/// - support
/// - desktop
/// - web
/// - mobile
///
final CommandShortcutEvent deleteLeftSentenceCommand = CommandShortcutEvent(
key: 'delete the left line',
command: 'ctrl+alt+backspace',
macOSCommand: 'cmd+backspace',
handler: _deleteLeftSentenceCommandHandler,
);
CommandShortcutEventHandler _deleteLeftSentenceCommandHandler = (editorState) {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return KeyEventResult.ignored;
}
final node = editorState.getNodeAtPath(selection.end.path);
final delta = node?.delta;
if (node == null || delta == null) {
return KeyEventResult.ignored;
}
final transaction = editorState.transaction;
transaction.deleteText(
node,
0,
selection.endIndex,
);
editorState.apply(transaction);
return KeyEventResult.handled;
};

View file

@ -1,270 +0,0 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/clipboard_test.dart';
import '../../new/infra/testable_editor.dart';
void main() async {
late MockClipboard mockClipboard;
setUp(() {
TestWidgetsFlutterBinding.ensureInitialized();
mockClipboard = const MockClipboard(html: null, text: null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (message) async {
switch (message.method) {
case "Clipboard.getData":
return mockClipboard.getData;
case "Clipboard.setData":
final args = message.arguments as Map<String, dynamic>;
mockClipboard = mockClipboard.copyWith(
text: args['text'],
);
}
return null;
});
});
group('paste_command_test.dart', () {
testWidgets(
'Copy link',
(tester) async {
final editor = tester.editor..addParagraph(initialText: '');
await editor.startTesting();
await editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 0)),
);
const link = 'https://appflowy.io/';
AppFlowyClipboard.mockSetData(
const AppFlowyClipboardData(text: link),
);
pasteCommand.execute(editor.editorState);
await tester.pumpAndSettle();
final delta = editor.nodeAtPath([0])!.delta!;
expect(delta.toPlainText(), link);
expect(
delta.everyAttributes(
(element) => element[AppFlowyRichTextKeys.href] == link,
),
true,
);
AppFlowyClipboard.mockSetData(null);
await editor.dispose();
},
);
testWidgets(
'Presses Command + A in small document and copy text and paste text',
(tester) async {
await _testHandleCopyPaste(tester, Document.fromJson(paragraphData));
});
testWidgets(
'Presses Command + A in small document and copy text and paste text multiple times',
(tester) async {
await _testHandleCopyMultiplePaste(
tester,
Document.fromJson(paragraphData),
);
});
});
}
Future<void> _testHandleCopyMultiplePaste(
WidgetTester tester,
Document document,
) async {
final editor = tester.editor..initializeWithDocument(document);
await editor.startTesting();
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.pressKey(
key: LogicalKeyboardKey.keyA,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
handleCopy(editor.editorState);
deleteSelectedContent(editor.editorState);
pasteHTML(
editor.editorState,
documentToHTML(Document.fromJson(paragraphData)),
);
expect(
editor.editorState.document.toJson(),
paragraphData,
);
await editor.updateSelection(Selection.single(path: [0], startOffset: 10));
pasteHTML(
editor.editorState,
documentToHTML(Document.fromJson(paragraphData)),
);
expect(
editor.document.toJson(),
secondParagraph,
);
pasteHTML(
editor.editorState,
documentToHTML(Document.fromJson(paragraphData)),
);
expect(
editor.document.toJson(),
thirdParagraph,
);
await editor.dispose();
}
Future<void> _testHandleCopyPaste(
WidgetTester tester,
Document document,
) async {
final editor = tester.editor..initializeWithDocument(document);
await editor.startTesting(platform: TargetPlatform.windows);
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.pressKey(
key: LogicalKeyboardKey.keyA,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
handleCopy(editor.editorState);
deleteSelectedContent(editor.editorState);
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.pressKey(
key: LogicalKeyboardKey.keyP,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
final clipBoardData = await AppFlowyClipboard.getData();
handlePastePlainText(editor.editorState, clipBoardData.text!);
expect(editor.document.toJson(), plainTextJson);
await editor.dispose();
}
const paragraphData = {
"document": {
"type": "page",
"children": [
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'AppFlowy Editor is a '},
{
'insert': 'highly customizable',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'rich-text editor',
'attributes': {'italic': true}
}
]
}
}
]
}
};
const secondParagraph = {
"document": {
"type": "page",
"children": [
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
},
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
}
]
}
}
]
}
};
const plainTextJson = {
"document": {
"type": "page",
"children": [
{
"type": "paragraph",
"data": {
"delta": [
{
"insert":
"AppFlowy Editor is a highly customizable rich-text editor"
}
]
}
}
]
}
};
const thirdParagraph = {
"document": {
"type": "page",
"children": [
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
},
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
},
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
}
]
}
}
]
}
};

View file

@ -0,0 +1,790 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
TestWidgetsFlutterBinding.ensureInitialized();
group('arrow_keys_handler.dart', () {
testWidgets('Presses arrow right key, move the cursor from left to right',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
for (var i = 0; i < text.length; i++) {
await editor.pressKey(key: LogicalKeyboardKey.arrowRight);
if (i == text.length - 1) {
// Wrap to next node if the cursor is at the end of the current node.
expect(
editor.selection,
Selection.single(
path: [1],
startOffset: 0,
),
);
} else {
final delta = editor.nodeAtPath([0])!.delta!;
expect(
editor.selection,
Selection.single(
path: [0],
startOffset: delta.nextRunePosition(i),
),
);
}
}
await editor.dispose();
});
});
testWidgets('Cursor up/down', (tester) async {
const text1 = 'Welcome';
const text2 = 'Welcome to AppFlowy';
final editor = tester.editor
..addParagraph(initialText: text1)
..addParagraph(initialText: text2);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [1], startOffset: text2.length),
);
await editor.pressKey(key: LogicalKeyboardKey.arrowUp);
expect(
editor.selection,
Selection.single(path: [0], startOffset: text1.length),
);
await editor.pressKey(key: LogicalKeyboardKey.arrowDown);
expect(
editor.selection,
Selection.single(path: [1], startOffset: text1.length),
);
await editor.dispose();
});
testWidgets('Cursor top/bottom select', (tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
Future<void> select(bool isTop) async {
return editor.pressKey(
key: isTop ? LogicalKeyboardKey.arrowUp : LogicalKeyboardKey.arrowDown,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
isShiftPressed: true,
);
}
final selection = Selection.collapsed(Position(path: [1], offset: 7));
// Welcome| to Appflowy
await editor.updateSelection(
selection,
);
await select(true);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: 0),
),
);
await select(false);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [2], offset: 19),
),
);
await editor.dispose();
});
testWidgets('''move the cursor one word right
MacOS: presses alt + arrow right key,
Windows & Linux: presses ctrl + arrow right key,
''', (tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
final selection = Selection.collapsed(Position(path: [0]));
await editor.updateSelection(
selection,
);
await _pressShortcutToNavigateAroundWord(editor);
expect(
editor.selection,
Selection.collapsed(Position(path: [0], offset: 'Welcome'.length)),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
);
await _pressShortcutToNavigateAroundWord(editor);
expect(
editor.selection,
Selection.collapsed(Position(path: [0], offset: 'Welcome to'.length)),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
);
await _pressShortcutToNavigateAroundWord(editor);
expect(
editor.selection,
Selection.collapsed(Position(path: [0], offset: text.length)),
);
await editor.dispose();
});
testWidgets('''move the cursor one word left
MacOS: presses alt + arrow left key,
Windows & Linux: presses ctrl + arrow left key,
''', (tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
final selection =
Selection.collapsed(Position(path: [0], offset: text.length));
await editor.updateSelection(
selection,
);
await _pressShortcutToNavigateAroundWord(editor, toRight: false);
expect(
editor.selection,
Selection.collapsed(Position(path: [0], offset: 11)),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
);
await _pressShortcutToNavigateAroundWord(editor, toRight: false);
expect(
editor.selection,
Selection.collapsed(Position(path: [0], offset: 8)),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
);
await _pressShortcutToNavigateAroundWord(editor, toRight: false);
expect(
editor.selection,
Selection.collapsed(Position(path: [0])),
);
await editor.dispose();
});
testWidgets(
'Presses arrow left/right key since selection is not collapsed and backward',
(tester) async {
await _testPressArrowKeyInNotCollapsedSelection(tester, true);
});
testWidgets(
'Presses arrow left/right key since selection is not collapsed and forward',
(tester) async {
await _testPressArrowKeyInNotCollapsedSelection(tester, false);
});
testWidgets('Presses arrow left/right + shift in collapsed selection',
(tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
const offset = 8;
final selection = Selection.single(path: [1], startOffset: offset);
await editor.updateSelection(selection);
for (var i = offset - 1; i >= 0; i--) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [1], offset: i),
),
);
}
for (var i = text.length; i >= 0; i--) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
for (var i = 1; i <= text.length; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
for (var i = 0; i < text.length; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [1], offset: i),
),
);
}
await editor.dispose();
});
testWidgets(
'Presses arrow left/right + shift in not collapsed and backward selection',
(tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
const start = 8;
const end = 12;
final selection = Selection.single(
path: [0],
startOffset: start,
endOffset: end,
);
await editor.updateSelection(selection);
for (var i = end + 1; i <= text.length; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
for (var i = text.length - 1; i >= 0; i--) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
await editor.dispose();
});
testWidgets(
'Presses arrow left/right + command in not collapsed and forward selection',
(tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
const start = 12;
const end = 8;
final selection = Selection.single(
path: [0],
startOffset: start,
endOffset: end,
);
await editor.updateSelection(selection);
for (var i = end - 1; i >= 0; i--) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
for (var i = 1; i <= text.length; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
);
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
await editor.dispose();
});
testWidgets('Presses arrow left/right/up/down + meta in collapsed selection',
(tester) async {
await _testPressArrowKeyWithMetaInSelection(tester, true, false);
});
testWidgets(
'Presses arrow left/right/up/down + meta in not collapsed and backward selection',
(tester) async {
await _testPressArrowKeyWithMetaInSelection(tester, false, true);
});
testWidgets(
'Presses arrow left/right/up/down + meta in not collapsed and forward selection',
(tester) async {
await _testPressArrowKeyWithMetaInSelection(tester, false, false);
});
testWidgets('Presses arrow up/down + shift in not collapsed selection',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..addParagraphs(2, initialText: text)
..addEmptyParagraph()
..addParagraph(initialText: text)
..addEmptyParagraph()
..addParagraphs(2, initialText: text);
await editor.startTesting();
final selection = Selection.single(path: [3], startOffset: 8);
await editor.updateSelection(selection);
for (int i = 0; i < 3; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowUp,
isShiftPressed: true,
);
}
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: 0),
),
);
for (int i = 0; i < 7; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowDown,
isShiftPressed: true,
);
}
expect(
editor.selection,
selection.copyWith(
end: Position(path: [6], offset: 0),
),
);
for (int i = 0; i < 3; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.arrowUp,
isShiftPressed: true,
);
}
expect(
editor.selection,
selection.copyWith(
end: Position(path: [3], offset: 0),
),
);
await editor.dispose();
});
//Before:
//Welcome |to Appflowy 😁
//Welcome to Appflowy 😁
//After On Mac
//Welcome |to Appflowy 😁
//Welcome to Appflowy 😁|
//expands the selection to include till end of line
//After On Windows
//Welcome |to Appflowy 😁
//Welcome to| Appflowy 😁
//expands the selection to include till end of next word
testWidgets('Presses shift + arrow down and meta/ctrl + shift + right',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
final selection = Selection.single(path: [0], startOffset: 8);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.arrowDown,
isShiftPressed: true,
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
if (Platform.isMacOS) {
expect(
editor.selection,
selection.copyWith(
end: Position(path: [1], offset: text.length),
),
);
} else {
expect(
editor.selection,
selection.copyWith(
end: Position(path: [1], offset: 'Welcome to'.length),
),
);
}
await editor.dispose();
});
//Before:
//Welcome to Appflowy 😁
//Welcome |to Appflowy 😁
//After On Mac
//|Welcome to Appflowy 😁
//Welcome |to Appflowy 😁
//expands the selection to include till end of line
//After On Windows
//Welcome| to Appflowy 😁
//Welcome |to Appflowy 😁
//expands the selection to include till end of next word
testWidgets('Presses shift + arrow up and meta/ctrl + shift + left',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
final selection = Selection.single(path: [1], startOffset: 8);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.arrowUp,
isShiftPressed: true,
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
if (Platform.isMacOS) {
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: 0),
),
);
} else {
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: 'Welcome'.length),
),
);
}
await editor.dispose();
});
// press alt on MacOS to select a word
// press ctrl on Windows and Linux to select a word
testWidgets('Presses shift + alt/ctrl + arrow left to select a word',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
final selection = Selection.single(path: [1], startOffset: 10);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
// <to>
expect(
editor.selection,
selection.copyWith(
end: Position(path: [1], offset: 8),
),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
// < to>
expect(
editor.selection,
selection.copyWith(
end: Position(path: [1], offset: 7),
),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
// <Welcome to>
expect(
editor.selection,
selection.copyWith(
end: Position(path: [1], offset: 0),
),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
// <😁>
// <Welcome to>
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: 22),
),
);
await editor.dispose();
});
// press alt on MacOS to select a word
// press ctrl on Windows and Linux to select a word
testWidgets('Presses shift + alt/ctrl + arrow right to select a word',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
final selection = Selection.single(path: [0], startOffset: 10);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
// < >
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: 11),
),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
// < Appflowy>
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: 19),
),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
// < Appflowy 😁>
expect(
editor.selection,
selection.copyWith(
end: Position(path: [0], offset: 22),
),
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
// < Appflowy 😁>
// <>
expect(
editor.selection,
selection.copyWith(
end: Position(path: [1], offset: 0),
),
);
await editor.dispose();
});
}
Future<void> _testPressArrowKeyWithMetaInSelection(
WidgetTester tester,
bool isSingle,
bool isBackward,
) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
final initialSelection = Selection.single(path: [0], startOffset: 8);
final selectionAtBeginning = Selection.single(path: [0], startOffset: 0);
final selectionAtEnd = Selection.single(path: [0], startOffset: text.length);
final selectionAtEndOfWelcome = Selection.single(path: [0], startOffset: 7);
Selection selection;
if (isSingle) {
selection = initialSelection;
} else {
if (isBackward) {
selection = Selection.single(
path: [0],
startOffset: 8,
endOffset: text.length,
);
} else {
selection = Selection.single(
path: [0],
startOffset: text.length,
endOffset: 8,
);
}
}
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.arrowLeft,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
if (Platform.isMacOS) {
expect(editor.selection, selectionAtBeginning);
} else if (isSingle) {
expect(editor.selection, selectionAtBeginning);
} else if (isBackward) {
expect(editor.selection, initialSelection);
} else {
expect(editor.selection, selectionAtEndOfWelcome);
}
await editor.updateSelection(selectionAtBeginning);
await editor.pressKey(
key: LogicalKeyboardKey.arrowRight,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
if (Platform.isMacOS) {
expect(editor.selection, selectionAtEnd);
} else {
expect(editor.selection, selectionAtEndOfWelcome);
}
await editor.updateSelection(selectionAtEnd);
await editor.pressKey(
key: LogicalKeyboardKey.arrowUp,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
expect(
editor.selection,
selectionAtBeginning,
);
await editor.pressKey(
key: LogicalKeyboardKey.arrowDown,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
expect(
editor.selection,
Selection.single(path: [1], startOffset: text.length),
);
await editor.dispose();
}
Future<void> _testPressArrowKeyInNotCollapsedSelection(
WidgetTester tester,
bool isBackward,
) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(2, initialText: text);
await editor.startTesting();
final start = Position(path: [0], offset: 5);
final end = Position(path: [1], offset: 10);
final selection = Selection(
start: isBackward ? start : end,
end: isBackward ? end : start,
);
await editor.updateSelection(selection);
await editor.pressKey(key: LogicalKeyboardKey.arrowLeft);
expect(editor.selection?.start, start);
await editor.updateSelection(selection);
await editor.pressKey(key: LogicalKeyboardKey.arrowRight);
expect(editor.selection?.end, end);
await editor.dispose();
}
Future<void> _pressShortcutToNavigateAroundWord(
TestableEditor editor, {
bool toRight = true,
}) async {
await editor.pressKey(
key: toRight ? LogicalKeyboardKey.arrowRight : LogicalKeyboardKey.arrowLeft,
isAltPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
}

View file

@ -0,0 +1,157 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('checkbox_event_handler_test.dart', () {
testWidgets('toggle checkbox with shortcut ctrl+enter', (tester) async {
const text = 'Checkbox1';
final editor = tester.editor
..addNode(
todoListNode(
checked: false,
delta: Delta()..insert(text),
),
);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: text.length),
);
var node = editor.nodeAtPath([0])!;
expect(node.type, 'todo_list');
expect(node.attributes[TodoListBlockKeys.checked], false);
await editor.pressKey(
key: LogicalKeyboardKey.enter,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
node = editor.nodeAtPath([0])!;
expect(node.attributes[TodoListBlockKeys.checked], true);
await editor.updateSelection(
Selection.single(path: [0], startOffset: text.length),
);
await editor.pressKey(
key: LogicalKeyboardKey.enter,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
node = editor.nodeAtPath([0])!;
expect(node.attributes[TodoListBlockKeys.checked], false);
await editor.dispose();
});
testWidgets(
'test if all checkboxes get unchecked after toggling them, if all of them were already checked',
(tester) async {
const text = 'Checkbox';
final editor = tester.editor
..addNode(
todoListNode(
checked: true,
attributes: {'delta': (Delta()..insert(text)).toJson()},
),
)
..addNode(
todoListNode(
checked: true,
attributes: {'delta': (Delta()..insert(text)).toJson()},
),
)
..addNode(
todoListNode(
checked: true,
attributes: {'delta': (Delta()..insert(text)).toJson()},
),
);
await editor.startTesting();
final selection =
Selection.collapsed(Position(path: [0], offset: text.length));
await editor.updateSelection(selection);
var nodes = editor.editorState.getNodesInSelection(selection);
for (final node in nodes) {
expect(node.type, 'todo_list');
expect(node.attributes[TodoListBlockKeys.checked], true);
}
await editor.pressKey(
key: LogicalKeyboardKey.enter,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
nodes = editor.editorState.getNodesInSelection(selection);
for (final node in nodes) {
expect(node.attributes[TodoListBlockKeys.checked], false);
}
await editor.dispose();
});
testWidgets(
'test if all checkboxes get checked after toggling them, if any one of them were already checked',
(tester) async {
const text = 'Checkbox';
final editor = tester.editor
..addNode(
todoListNode(
checked: false,
attributes: {'delta': (Delta()..insert(text)).toJson()},
),
)
..addNode(
todoListNode(
checked: true,
attributes: {'delta': (Delta()..insert(text)).toJson()},
),
)
..addNode(
todoListNode(
checked: false,
attributes: {'delta': (Delta()..insert(text)).toJson()},
),
);
await editor.startTesting();
final selection = Selection(
start: Position(path: [0], offset: 0),
end: Position(path: [2], offset: text.length),
);
await editor.updateSelection(
selection,
);
await editor.pressKey(
key: LogicalKeyboardKey.enter,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
final nodes = editor.editorState.getNodesInSelection(selection);
for (final node in nodes) {
expect(node.type, 'todo_list');
expect(node.attributes[TodoListBlockKeys.checked], true);
}
await editor.dispose();
});
});
}

View file

@ -0,0 +1,97 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../../infra/clipboard_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
late MockClipboard mockClipboard;
setUp(() {
TestWidgetsFlutterBinding.ensureInitialized();
mockClipboard = const MockClipboard(html: null, text: null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (message) async {
switch (message.method) {
case "Clipboard.getData":
return mockClipboard.getData;
case "Clipboard.setData":
final args = message.arguments as Map<String, dynamic>;
mockClipboard = mockClipboard.copyWith(
text: args['text'],
);
}
return null;
});
});
group('copy_command_test.dart', () {
testWidgets('Presses Command + A in small document and copy text',
(tester) async {
await _testHandleCopy(tester);
});
});
}
Future<void> _testHandleCopy(
WidgetTester tester,
) async {
final editor = tester.editor
..initializeWithDocument(Document.fromJson(paragraphdata));
await editor.startTesting(platform: TargetPlatform.windows);
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.pressKey(
key: LogicalKeyboardKey.keyA,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
await editor.pressKey(
key: LogicalKeyboardKey.keyC,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
deleteSelectedContent(editor.editorState);
expect(editor.document.root.children.length, 1);
expect(editor.document.root.children.first.delta!.isEmpty, true);
await editor.pressKey(
key: LogicalKeyboardKey.keyC,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
final clipBoardData = await AppFlowyClipboard.getData();
//this will be null because html content is not testable
expect(clipBoardData.html, null);
expect(clipBoardData.text, copiedText);
await editor.dispose();
}
const copiedText =
"AppFlowy Editor is a highly customizable rich-text editor";
const paragraphdata = {
"document": {
"type": "page",
"children": [
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'AppFlowy Editor is a '},
{
'insert': 'highly customizable',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'rich-text editor',
'attributes': {'italic': true}
}
]
}
}
]
}
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,337 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('cut_command_test.dart', () {
testWidgets('update selection and execute cut command', (tester) async {
await _testCutHandle(tester, Document.fromJson(paragraphdata));
});
});
}
Future<void> _testCutHandle(
WidgetTester tester,
Document document,
) async {
final editor = tester.editor..initializeWithDocument(document);
await editor.startTesting();
await editor.updateSelection(
Selection(
start: Position(path: [2], offset: 0),
end: Position(path: [2], offset: 69),
),
);
await editor.pressKey(
key: LogicalKeyboardKey.keyX,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
expect(
editor.editorState.document.toJson(),
Document.fromJson(afterCut).toJson(),
);
await editor.dispose();
}
const paragraphdata = {
"document": {
"type": "page",
"children": [
{
"type": "heading",
"data": {
"level": 2,
"delta": [
{"insert": "👋 "},
{
"insert": "Welcome to",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "AppFlowy Editor",
"attributes": {
"href": "appflowy.io",
"italic": true,
"bold": true
}
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "AppFlowy Editor is a"},
{"insert": " "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
},
{"insert": " for "},
{
"insert": "Flutter",
"attributes": {"underline": true}
}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Customizable"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Test-covered"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": false,
"delta": [
{"insert": "more to come!"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "quote",
"data": {
"delta": [
{"insert": "Here is an example you can give a try"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "You can also use "},
{
"insert": "AppFlowy Editor",
"attributes": {
"italic": true,
"bold": true,
"font_color": "0xffD70040",
"bg_color": "0x6000BCF0"
}
},
{"insert": " as a component to build your own app."}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{"insert": "Use / to insert blocks"}
]
}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{
"insert":
"Select text to trigger to the toolbar to format your notes."
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{
"insert":
"If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!"
}
]
}
}
]
}
};
const afterCut = {
"document": {
"type": "page",
"children": [
{
"type": "heading",
"data": {
"level": 2,
"delta": [
{"insert": "👋 "},
{
"insert": "Welcome to",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "AppFlowy Editor",
"attributes": {
"href": "appflowy.io",
"italic": true,
"bold": true
}
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Customizable"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Test-covered"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": false,
"delta": [
{"insert": "more to come!"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "quote",
"data": {
"delta": [
{"insert": "Here is an example you can give a try"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "You can also use "},
{
"insert": "AppFlowy Editor",
"attributes": {
"italic": true,
"bold": true,
"font_color": "0xffD70040",
"bg_color": "0x6000BCF0"
}
},
{"insert": " as a component to build your own app."}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{"insert": "Use / to insert blocks"}
]
}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{
"insert":
"Select text to trigger to the toolbar to format your notes."
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{
"insert":
"If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!"
}
]
}
}
]
}
};

View file

@ -0,0 +1,313 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
TestWidgetsFlutterBinding.ensureInitialized();
group('enter_without_shift_in_text_node_handler.dart', () {
testWidgets('Presses enter key in empty document', (tester) async {
// Before
//
// [Empty Line]
//
// After
//
// [Empty Line] * 10
//
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
// Pressing the enter key continuously.
for (int i = 1; i <= 10; i++) {
await editor.pressKey(
character: '\n',
);
expect(editor.documentRootLen, i + 1);
expect(
editor.selection,
Selection.single(path: [i], startOffset: 0),
);
}
await editor.dispose();
});
testWidgets('Presses enter key in non-empty document', (tester) async {
// Before
//
// Welcome to Appflowy 😁
// Welcome to Appflowy 😁
// Welcome to Appflowy 😁
//
// After
//
// Welcome to Appflowy 😁
// Welcome to Appflowy 😁
// [Empty Line]
// Welcome to Appflowy 😁
//
const text = 'Welcome to Appflowy 😁';
var lines = 3;
final editor = tester.editor;
editor.addParagraphs(lines, initialText: text);
await editor.startTesting();
expect(editor.documentRootLen, lines);
// Presses the enter key in last line.
await editor.updateSelection(
Selection.single(path: [lines - 1], startOffset: 0),
);
await editor.pressKey(
character: '\n',
);
lines += 1;
expect(editor.documentRootLen, lines);
expect(
editor.selection,
Selection.single(path: [lines - 1], startOffset: 0),
);
var lastNode = editor.nodeAtPath([lines - 1]);
expect(lastNode != null, true);
expect(lastNode?.type, 'paragraph');
expect(lastNode?.delta?.toPlainText(), text);
expect(lastNode?.previous?.delta?.toPlainText(), '');
expect(
lastNode?.previous?.previous?.delta?.toPlainText(),
text,
);
await editor.dispose();
});
// Before
//
// Welcome to Appflowy 😁
// [Style] Welcome to Appflowy 😁
// [Style] Welcome to Appflowy 😁
//
// After
//
// Welcome to Appflowy 😁
// [Empty Line]
// [Style] Welcome to Appflowy 😁
// [Style] Welcome to Appflowy 😁
// [Style]
testWidgets('Presses enter key in bulleted list', (tester) async {
await _testStyleNeedToBeCopy(tester, 'bulleted_list');
});
testWidgets('Presses enter key in numbered list', (tester) async {
await _testStyleNeedToBeCopy(tester, 'numbered_list');
});
testWidgets('Presses enter key in checkbox styled text', (tester) async {
await _testStyleNeedToBeCopy(tester, 'todo_list');
});
testWidgets('Presses enter key in checkbox list indented', (tester) async {
await _testListOutdent(tester, 'todo_list');
});
testWidgets('Presses enter key in bulleted list indented', (tester) async {
await _testListOutdent(tester, 'bulleted_list');
});
testWidgets('Presses enter key in multiple selection from top to bottom',
(tester) async {
_testMultipleSelection(tester, true);
});
testWidgets('Presses enter key in multiple selection from bottom to top',
(tester) async {
_testMultipleSelection(tester, false);
});
testWidgets('Presses enter key in the first line', (tester) async {
// Before
//
// Welcome to Appflowy 😁
//
// After
//
// [Empty Line]
// Welcome to Appflowy 😁
//
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraph(initialText: text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.pressKey(character: '\n');
expect(editor.documentRootLen, 2);
expect(editor.nodeAtPath([1])?.delta?.toPlainText(), text);
await editor.dispose();
});
});
}
Future<void> _testStyleNeedToBeCopy(WidgetTester tester, String style) async {
const text = 'Welcome to Appflowy 😁';
final attributes = {
'delta': (Delta()..insert(text)).toJson(),
};
Node? node;
if (style == 'todo_list') {
node = todoListNode(checked: true, attributes: attributes);
} else if (style == 'numbered_list') {
node = numberedListNode(attributes: attributes);
} else if (style == 'bulleted_list') {
node = bulletedListNode(attributes: attributes);
} else if (style == 'quote') {
node = quoteNode(attributes: attributes);
}
if (node == null) {
throw Exception('Invalid style: $style');
}
final editor = tester.editor
..addParagraph(initialText: text)
..addNode(node)
..addNode(node.copyWith());
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [1], startOffset: 0),
);
await editor.pressKey(
character: '\n',
);
expect(editor.selection, Selection.single(path: [2], startOffset: 0));
await editor.updateSelection(
Selection.single(path: [3], startOffset: text.length),
);
await editor.pressKey(
character: '\n',
);
expect(editor.selection, Selection.single(path: [4], startOffset: 0));
expect(editor.nodeAtPath([4])?.type, style);
await editor.pressKey(
character: '\n',
);
expect(
editor.selection,
Selection.single(path: [4], startOffset: 0),
);
expect(editor.nodeAtPath([4])?.type, 'paragraph');
await editor.dispose();
}
Future<void> _testListOutdent(WidgetTester tester, String style) async {
const text = 'Welcome to Appflowy 😁';
final attributes = {
'delta': (Delta()..insert(text)).toJson(),
};
Node? node;
if (style == 'todo_list') {
node = todoListNode(checked: true, attributes: attributes);
} else if (style == 'numbered_list') {
node = numberedListNode(attributes: attributes);
} else if (style == 'bulleted_list') {
node = bulletedListNode(attributes: attributes);
}
if (node == null) {
throw Exception('Invalid style: $style');
}
final editor = tester.editor
..addParagraph(initialText: text)
..addNode(node)
..addNode(
node.copyWith(
attributes: {
...node.attributes,
'delta': Delta().toJson(),
},
),
);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [2], startOffset: 0),
);
await editor.pressKey(
key: LogicalKeyboardKey.tab,
);
expect(
editor.selection,
Selection.single(path: [1, 0], startOffset: 0),
);
await editor.pressKey(
character: '\n',
);
// clear the style
expect(
editor.selection,
Selection.single(path: [1, 0], startOffset: 0),
);
expect(editor.nodeAtPath([1, 0])?.type, 'paragraph');
await editor.pressKey(
character: '\n',
);
expect(
editor.selection,
Selection.single(path: [1, 1], startOffset: 0),
);
expect(editor.nodeAtPath([1, 1])?.type, 'paragraph');
await editor.dispose();
}
Future<void> _testMultipleSelection(
WidgetTester tester,
bool isBackwardSelection,
) async {
// Before
//
// Welcome to Appflowy 😁
// Welcome to Appflowy 😁
// Welcome to Appflowy 😁
// Welcome to Appflowy 😁
//
// After
//
// Welcome
// to Appflowy 😁
//
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor;
var lines = 4;
editor.addParagraphs(lines, initialText: text);
await editor.startTesting();
final start = Position(path: [0], offset: 7);
final end = Position(path: [3], offset: 8);
await editor.updateSelection(
Selection(
start: isBackwardSelection ? start : end,
end: isBackwardSelection ? end : start,
),
);
await editor.pressKey(
character: '\n',
);
expect(editor.documentRootLen, 2);
expect(editor.nodeAtPath([0])?.delta?.toPlainText(), 'Welcome');
expect(editor.nodeAtPath([1])?.delta?.toPlainText(), 'to Appflowy 😁');
await editor.dispose();
}

View file

@ -0,0 +1,47 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('exit_editing_mode_handler.dart', () {
testWidgets('Exit editing mode', (tester) async {
const text = 'Welcome to Appflowy 😁';
const lines = 3;
final editor = tester.editor..addParagraphs(lines, initialText: text);
await editor.startTesting();
// collapsed selection
await _testSelection(editor, Selection.single(path: [1], startOffset: 0));
// single selection
await _testSelection(
editor,
Selection.single(path: [1], startOffset: 0, endOffset: text.length),
);
// multiple selection
await _testSelection(
editor,
Selection(
start: Position(path: [0], offset: 0),
end: Position(path: [2], offset: text.length),
),
);
});
});
}
Future<void> _testSelection(
TestableEditor editor,
Selection selection,
) async {
await editor.updateSelection(selection);
expect(editor.selection, selection);
await editor.pressKey(key: LogicalKeyboardKey.escape);
expect(editor.selection, null);
}

View file

@ -0,0 +1,216 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('format_style_handler.dart', () {
testWidgets('Presses Command + B to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
BuiltInAttributeKey.bold,
true,
LogicalKeyboardKey.keyB,
);
});
testWidgets('Presses Command + I to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
BuiltInAttributeKey.italic,
true,
LogicalKeyboardKey.keyI,
);
});
testWidgets('Presses Command + U to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
BuiltInAttributeKey.underline,
true,
LogicalKeyboardKey.keyU,
);
});
testWidgets('Presses Command + Shift + S to update text style',
(tester) async {
await _testUpdateTextStyleByCommandX(
tester,
BuiltInAttributeKey.strikethrough,
true,
LogicalKeyboardKey.keyS,
);
});
testWidgets('Presses Command + E to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
BuiltInAttributeKey.code,
true,
LogicalKeyboardKey.keyE,
);
});
});
}
Future<void> _testUpdateTextStyleByCommandX(
WidgetTester tester,
String matchStyle,
dynamic matchValue,
LogicalKeyboardKey key,
) async {
final isShiftPressed =
key == LogicalKeyboardKey.keyS || key == LogicalKeyboardKey.keyH;
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
var selection =
Selection.single(path: [1], startOffset: 2, endOffset: text.length - 2);
await editor.updateSelection(selection);
if (Platform.isWindows || Platform.isLinux) {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isControlPressed: true,
);
} else {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isMetaPressed: true,
);
}
Node? node = editor.nodeAtPath([1]);
expect(
node?.allSatisfyInSelection(selection, (delta) {
return delta
.everyAttributes((element) => element[matchStyle] == matchValue);
}),
true,
);
selection =
Selection.single(path: [1], startOffset: 0, endOffset: text.length);
await editor.updateSelection(selection);
if (Platform.isWindows || Platform.isLinux) {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isControlPressed: true,
);
} else {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isMetaPressed: true,
);
}
node = editor.nodeAtPath([1]);
expect(
node?.allSatisfyInSelection(selection, (delta) {
return delta
.everyAttributes((element) => element[matchStyle] == matchValue);
}),
true,
);
await editor.updateSelection(selection);
if (Platform.isWindows || Platform.isLinux) {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isControlPressed: true,
);
} else {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isMetaPressed: true,
);
}
node = editor.nodeAtPath([1]);
expect(
node?.allSatisfyInSelection(selection, (delta) {
return delta
.everyAttributes((element) => element[matchStyle] == matchValue);
}),
false,
);
selection = Selection(
start: Position(path: [0], offset: 0),
end: Position(path: [2], offset: text.length),
);
await editor.updateSelection(selection);
if (Platform.isWindows || Platform.isLinux) {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isControlPressed: true,
);
} else {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isMetaPressed: true,
);
}
var nodes = editor.editorState.service.selectionService.currentSelectedNodes;
expect(nodes.length, 3);
for (final node in nodes) {
expect(
node.allSatisfyInSelection(
Selection.single(
path: node.path,
startOffset: 0,
endOffset: text.length,
), (delta) {
return delta
.everyAttributes((element) => element[matchStyle] == matchValue);
}),
true,
);
}
await editor.updateSelection(selection);
if (Platform.isWindows || Platform.isLinux) {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isControlPressed: true,
);
} else {
await editor.pressKey(
key: key,
isShiftPressed: isShiftPressed,
isMetaPressed: true,
);
}
nodes = editor.editorState.service.selectionService.currentSelectedNodes;
expect(nodes.length, 3);
for (final node in nodes) {
expect(
node.allSatisfyInSelection(
Selection.single(
path: node.path,
startOffset: 0,
endOffset: text.length,
), (delta) {
return delta
.everyAttributes((element) => element[matchStyle] == matchValue);
}),
false,
);
}
await editor.dispose();
}

View file

@ -0,0 +1,780 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
bool allInSelection(
Node? node,
Selection selection,
String key,
) {
return node?.allSatisfyInSelection(
selection,
(delta) => delta.whereType<TextInsert>().every(
(element) => element.attributes?[key] == true,
),
) ??
false;
}
bool allCodeInSelection(Node? node, Selection selection) => allInSelection(
node,
selection,
'code',
);
bool allStrikethroughInSelection(Node? node, Selection selection) =>
allInSelection(
node,
selection,
'strikethrough',
);
bool allBoldInSelection(Node? node, Selection selection) => allInSelection(
node,
selection,
'bold',
);
bool allItalicInSelection(Node? node, Selection selection) => allInSelection(
node,
selection,
'bold',
);
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('markdown_syntax_to_styled_text.dart', () {
Future<void> insertBackquote(
TestableEditor editor, {
int repeat = 1,
}) async {
for (var i = 0; i < repeat; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.backquote,
);
}
}
group('convert single backquote to code', () {
testWidgets('`AppFlowy` to code AppFlowy', (tester) async {
const text = '`AppFlowy';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertBackquote(editor);
final node = editor.nodeAtPath([0]);
final allCode = allCodeInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length - 1,
),
);
expect(allCode, true);
expect(node?.delta?.toPlainText(), 'AppFlowy');
await editor.dispose();
});
testWidgets('App`Flowy` to code AppFlowy', (tester) async {
const text = 'App`Flowy';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertBackquote(editor);
final node = editor.nodeAtPath([0]);
final allCode = allCodeInSelection(
node,
Selection.single(
path: [0],
startOffset: 3,
endOffset: text.length - 1,
),
);
expect(allCode, true);
expect(node?.delta?.toPlainText(), 'AppFlowy');
await editor.dispose();
});
testWidgets('`` nothing changes', (tester) async {
const text = '`';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertBackquote(editor);
final node = editor.nodeAtPath([0]);
final allCode = allCodeInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(allCode, false);
expect(node.delta?.toPlainText(), '``');
await editor.dispose();
});
});
group('convert double backquote to code', () {
testWidgets('```` nothing changes', (tester) async {
const text = '```';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertBackquote(editor);
final node = editor.nodeAtPath([0]);
final allCode = allCodeInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(allCode, false);
expect(node.delta?.toPlainText(), '````');
await editor.dispose();
});
});
group('convert double tilde to strikethrough', () {
Future<void> insertTilde(
TestableEditor editor, {
int repeat = 1,
}) async {
for (var i = 0; i < repeat; i++) {
await editor.pressKey(key: LogicalKeyboardKey.tilde);
}
}
testWidgets('~~AppFlowy~~ to strikethrough AppFlowy', (tester) async {
const text = '~~AppFlowy~';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertTilde(editor);
final node = editor.nodeAtPath([0]);
final result = allStrikethroughInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, true);
expect(node.delta?.toPlainText(), 'AppFlowy');
await editor.dispose();
});
testWidgets('App~~Flowy~~ to strikethrough AppFlowy', (tester) async {
const text = 'App~~Flowy~';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertTilde(editor);
final node = editor.nodeAtPath([0]);
final result = allStrikethroughInSelection(
node,
Selection.single(
path: [0],
startOffset: 3,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, true);
expect(node.delta?.toPlainText(), 'AppFlowy');
await editor.dispose();
});
testWidgets('~~~~ nothing changes', (tester) async {
const text = '~~~';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertTilde(editor);
final node = editor.nodeAtPath([0]);
final result = allStrikethroughInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, false);
expect(node.delta?.toPlainText(), '~~~~');
await editor.dispose();
});
});
});
group('convert double asterisk to bold', () {
Future<void> insertAsterisk(
TestableEditor editor, {
int repeat = 1,
}) async {
for (var i = 0; i < repeat; i++) {
await editor.pressKey(key: LogicalKeyboardKey.asterisk);
}
}
testWidgets(
'**AppFlowy** to bold AppFlowy',
(tester) async {
const text = '**AppFlowy*';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertAsterisk(editor);
final node = editor.nodeAtPath([0]);
final result = allBoldInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, true);
expect(node.delta?.toPlainText(), 'AppFlowy');
await editor.dispose();
},
);
testWidgets(
'App**Flowy** to bold AppFlowy',
((tester) async {
const text = 'App**Flowy*';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertAsterisk(editor);
final node = editor.nodeAtPath([0]);
final result = allBoldInSelection(
node,
Selection.single(
path: [0],
startOffset: 3,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, true);
expect(node.delta?.toPlainText(), 'AppFlowy');
await editor.dispose();
}),
);
testWidgets(
'***AppFlowy** to bold *AppFlowy',
((tester) async {
const text = '***AppFlowy*';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertAsterisk(editor);
final node = editor.nodeAtPath([0]);
final result = allBoldInSelection(
node,
Selection.single(
path: [0],
startOffset: 1,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, true);
expect(node.delta?.toPlainText(), '*AppFlowy');
await editor.dispose();
}),
);
testWidgets(
'**** nothing changes',
((tester) async {
const text = '***';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertAsterisk(editor);
final node = editor.nodeAtPath([0]);
final result = allBoldInSelection(
node,
Selection.single(
path: [0],
startOffset: 1,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, false);
expect(node.delta?.toPlainText(), '****');
await editor.dispose();
}),
);
testWidgets(
'**bold and _nested_ italics**',
(tester) async {
const doubleAsterisks = '**';
const singleAsterisk = '*';
const firstBoldSegment = 'bold and ';
const underscore = '_';
const italicsSegment = 'nested';
const secondBoldSegment = ' italics';
const text = doubleAsterisks +
firstBoldSegment +
underscore +
italicsSegment +
underscore +
secondBoldSegment +
singleAsterisk;
final editor = tester.editor..addParagraph(initialText: '');
await editor.startTesting();
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
for (final c in text.characters) {
await editor.ime.insertText(c);
}
await insertAsterisk(editor);
final node = editor.nodeAtPath([0]);
final allTextBold = allBoldInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length,
),
);
final segmentItalic = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: firstBoldSegment.length,
endOffset: firstBoldSegment.length + italicsSegment.length - 1,
),
);
final lastSegmentItalic = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: firstBoldSegment.length + italicsSegment.length,
endOffset: text.length,
),
);
const plainText = firstBoldSegment + italicsSegment + secondBoldSegment;
final plainResult = node?.delta?.toPlainText();
expect(allTextBold, true);
expect(segmentItalic, true);
expect(lastSegmentItalic, true);
expect(plainText, plainResult);
await editor.dispose();
},
);
testWidgets(
'regular then **bold and _nested_ italics**',
(tester) async {
const doubleAsterisks = '**';
const singleAsterisk = '*';
const regularSegment = 'regular then ';
const firstBoldSegment = 'bold and ';
const underscore = '_';
const italicsSegment = 'nested';
const secondBoldSegment = ' italics';
const text = regularSegment +
doubleAsterisks +
firstBoldSegment +
underscore +
italicsSegment +
underscore +
secondBoldSegment +
singleAsterisk;
final editor = tester.editor..addParagraph(initialText: '');
await editor.startTesting();
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
for (final c in text.characters) {
await editor.ime.insertText(c);
}
await insertAsterisk(editor);
final node = editor.nodeAtPath([0]);
final textInAsterisksIsBold = allBoldInSelection(
node,
Selection.single(
path: [0],
startOffset: regularSegment.length,
endOffset: text.length,
),
);
final segmentItalic = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: regularSegment.length + firstBoldSegment.length,
endOffset: regularSegment.length +
firstBoldSegment.length +
italicsSegment.length -
1,
),
);
final lastSegmentItalic = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: text.length - secondBoldSegment.length,
endOffset: text.length,
),
);
const plainText = regularSegment +
firstBoldSegment +
italicsSegment +
secondBoldSegment;
final plainResult = node?.delta?.toPlainText();
expect(textInAsterisksIsBold, true);
expect(segmentItalic, true);
expect(lastSegmentItalic, true);
expect(plainText, plainResult);
await editor.dispose();
},
);
testWidgets(
'**bold and _double_ _nested_ italics**',
(tester) async {
const doubleAsterisks = '**';
const singleAsterisk = '*';
const firstBoldSegment = 'bold and ';
const firstItalicsSegment = 'double';
const underscore = '_';
const secondBoldSegment = ' ';
const secondItalicsSegment = 'nested';
const thirdBoldSegment = ' italics';
const text = doubleAsterisks +
firstBoldSegment +
underscore +
firstItalicsSegment +
underscore +
secondBoldSegment +
underscore +
secondItalicsSegment +
underscore +
thirdBoldSegment +
singleAsterisk;
final editor = tester.editor..addParagraph(initialText: '');
await editor.startTesting();
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
for (final c in text.characters) {
await editor.ime.insertText(c);
}
await insertAsterisk(editor);
final node = editor.nodeAtPath([0]);
final allTextBold = allBoldInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length,
),
);
final firstSegmentItalic = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: firstBoldSegment.length,
endOffset: firstBoldSegment.length + firstItalicsSegment.length - 1,
),
);
const secondItalicsSegmentIndex = firstBoldSegment.length +
firstItalicsSegment.length +
secondBoldSegment.length;
final secondSegmentItalic = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: secondItalicsSegmentIndex,
endOffset:
secondItalicsSegmentIndex + secondItalicsSegment.length - 1,
),
);
const plainText = firstBoldSegment +
firstItalicsSegment +
secondBoldSegment +
secondItalicsSegment +
thirdBoldSegment;
final plainResult = node?.delta?.toPlainText();
expect(allTextBold, true);
expect(firstSegmentItalic, true);
expect(secondSegmentItalic, true);
expect(plainText, plainResult);
await editor.dispose();
},
);
testWidgets(
'**bold and _nested_ italics with a _ unescaped**',
(tester) async {
const doubleAsterisks = '**';
const singleAsterisk = '*';
const firstBoldSegment = 'bold and ';
const underscore = '_';
const italicsSegment = 'nested';
const secondBoldSegment = ' italics with a _ unescaped';
const text = doubleAsterisks +
firstBoldSegment +
underscore +
italicsSegment +
underscore +
secondBoldSegment +
singleAsterisk;
final editor = tester.editor..addParagraph(initialText: '');
await editor.startTesting();
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
for (final c in text.characters) {
await editor.ime.insertText(c);
}
await insertAsterisk(editor);
final node = editor.nodeAtPath([0]);
final allTextBold = allBoldInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length,
),
);
final segmentItalic = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: firstBoldSegment.length,
endOffset: firstBoldSegment.length + italicsSegment.length - 1,
),
);
final lastSegmentItalic = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: firstBoldSegment.length + italicsSegment.length,
endOffset: text.length,
),
);
const plainText = firstBoldSegment + italicsSegment + secondBoldSegment;
final plainResult = node?.delta?.toPlainText();
expect(allTextBold, true);
expect(segmentItalic, true);
expect(lastSegmentItalic, true);
expect(plainText, plainResult);
await editor.dispose();
},
);
});
group('convert double underscore to bold', () {
Future<void> insertUnderscore(
TestableEditor editor, {
int repeat = 1,
}) async {
for (var i = 0; i < repeat; i++) {
await editor.pressKey(key: LogicalKeyboardKey.underscore);
}
}
testWidgets(
'__AppFlowy__ to bold AppFlowy',
((tester) async {
const text = '__AppFlowy_';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertUnderscore(editor);
final node = editor.nodeAtPath([0]);
final result = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: 0,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, true);
expect(node.delta!.toPlainText(), 'AppFlowy');
await editor.dispose();
}),
);
testWidgets(
'App__Flowy__ to bold AppFlowy',
((tester) async {
const text = 'App__Flowy_';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertUnderscore(editor);
final node = editor.nodeAtPath([0]);
final result = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: 3,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, true);
expect(node.delta!.toPlainText(), 'AppFlowy');
await editor.dispose();
}),
);
testWidgets(
'__*AppFlowy__ to bold *AppFlowy',
((tester) async {
const text = '__*AppFlowy_';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertUnderscore(editor);
final node = editor.nodeAtPath([0]);
final result = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: 1,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, true);
expect(node.delta!.toPlainText(), '*AppFlowy');
await editor.dispose();
}),
);
testWidgets(
'____ nothing changes',
((tester) async {
const text = '___';
final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.editorState.insertTextAtCurrentSelection(text);
await insertUnderscore(editor);
final node = editor.nodeAtPath([0]);
final result = allItalicInSelection(
node,
Selection.single(
path: [0],
startOffset: 1,
endOffset: node!.delta!.toPlainText().length,
),
);
expect(result, false);
expect(node.delta!.toPlainText(), '____');
await editor.dispose();
}),
);
});
}

View file

@ -0,0 +1,215 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('outdent_handler.dart', () {
testWidgets("press shift tab in plain text", (tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final snapshotDocument = editor.document;
await editor.updateSelection(Selection.single(path: [0], startOffset: 0));
await editor.pressKey(
key: LogicalKeyboardKey.tab,
isShiftPressed: true,
);
// nothing happens
expect(
editor.selection,
Selection.single(path: [0], startOffset: 0),
);
expect(editor.document.toJson(), snapshotDocument.toJson());
});
testWidgets("press shift tab where previous element is not list element",
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..addParagraph(initialText: text)
..addNode(bulletedListNode(delta: Delta()..insert(text)))
..addNode(bulletedListNode(delta: Delta()..insert(text)));
await editor.startTesting();
final snapshotDocument = editor.document;
final selection = Selection.single(path: [1], startOffset: 0);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.tab,
isShiftPressed: true,
);
// nothing happens
expect(
editor.selection,
Selection.single(path: [1], startOffset: 0),
);
expect(editor.document.toJson(), snapshotDocument.toJson());
await editor.dispose();
});
testWidgets(
"press shift tab in indented list with multiple nodes in same sub-level",
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..addNode(todoListNode(checked: false, delta: Delta()..insert(text)))
..addNode(todoListNode(checked: false, delta: Delta()..insert(text)))
..addNode(todoListNode(checked: false, delta: Delta()..insert(text)));
await editor.startTesting();
final selection = Selection.collapsed(Position(path: [1]));
await editor.updateSelection(selection);
await editor.pressKey(key: LogicalKeyboardKey.tab);
await editor.updateSelection(selection);
await editor.pressKey(key: LogicalKeyboardKey.tab);
// Before
// [] Welcome to Appflowy 😁
// [] Welcome to Appflowy 😁
// [] Welcome to Appflowy 😁
// After
// [] Welcome to Appflowy 😁
// [] Welcome to Appflowy 😁
// [] Welcome to Appflowy 😁
expect(
editor.selection,
Selection.collapsed(Position(path: [0, 1])),
);
expect(
editor.nodeAtPath([0])!.type,
'todo_list',
);
expect(editor.nodeAtPath([1]), null);
expect(editor.nodeAtPath([2]), null);
expect(
editor.nodeAtPath([0, 0])!.type,
'todo_list',
);
expect(
editor.nodeAtPath([0, 1])!.type,
'todo_list',
);
await editor.updateSelection(
Selection.single(path: [0, 1], startOffset: 0),
);
await editor.pressKey(
key: LogicalKeyboardKey.tab,
isShiftPressed: true,
);
// Before
// [] Welcome to Appflowy 😁
// [] Welcome to Appflowy 😁
// [] Welcome to Appflowy 😁
// After
// [] Welcome to Appflowy 😁
// [] Welcome to Appflowy 😁
// [] Welcome to Appflowy 😁
expect(
editor.nodeAtPath([1])!.type,
'todo_list',
);
expect(
editor.nodeAtPath([0, 0])!.type,
'todo_list',
);
expect(editor.nodeAtPath([0, 1]), null);
await editor.dispose();
},
);
testWidgets(
"press shift tab in indented list with only one node in same sub-level",
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..addNode(bulletedListNode(delta: Delta()..insert(text)))
..addNode(bulletedListNode(delta: Delta()..insert(text)))
..addNode(bulletedListNode(delta: Delta()..insert(text)));
await editor.startTesting();
var selection = Selection.single(path: [1], startOffset: 0);
await editor.updateSelection(selection);
await editor.pressKey(key: LogicalKeyboardKey.tab);
// Before
// * Welcome to Appflowy 😁
// * Welcome to Appflowy 😁
// * Welcome to Appflowy 😁
// After
// * Welcome to Appflowy 😁
// * Welcome to Appflowy 😁
// * Welcome to Appflowy 😁
expect(
editor.selection,
Selection.single(path: [0, 0], startOffset: 0),
);
expect(
editor.nodeAtPath([0])!.type,
'bulleted_list',
);
expect(
editor.nodeAtPath([0, 0])!.type,
'bulleted_list',
);
expect(
editor.nodeAtPath([1])!.type,
'bulleted_list',
);
expect(editor.nodeAtPath([2]), null);
await editor
.updateSelection(Selection.single(path: [0, 0], startOffset: 0));
await editor.pressKey(
key: LogicalKeyboardKey.tab,
isShiftPressed: true,
);
// Before
// * Welcome to Appflowy 😁
// * Welcome to Appflowy 😁
// * Welcome to Appflowy 😁
// After
// * Welcome to Appflowy 😁
// * Welcome to Appflowy 😁
// * Welcome to Appflowy 😁
expect(
editor.nodeAtPath([0])!.type,
'bulleted_list',
);
expect(
editor.nodeAtPath([1])!.type,
'bulleted_list',
);
expect(
editor.nodeAtPath([2])!.type,
'bulleted_list',
);
expect(editor.nodeAtPath([0, 0]), null);
await editor.dispose();
},
);
});
}

View file

@ -0,0 +1,71 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_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..addParagraphs(1000, initialText: text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
final scrollService = editor.editorState.service.scrollService!;
final page = scrollService.page!;
final onePageHeight = scrollService.onePageHeight!;
// Pressing the pageDown key continuously.
var currentOffsetY = 0.0;
for (int i = 1; i <= page; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.pageDown,
);
if (i == page) {
currentOffsetY = scrollService.maxScrollExtent;
} else {
currentOffsetY += onePageHeight;
}
final dy = scrollService.dy;
expect(dy, currentOffsetY);
}
for (int i = 1; i <= 5; i++) {
await editor.pressKey(
key: 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.pressKey(
key: LogicalKeyboardKey.pageUp,
);
if (i == 1) {
currentOffsetY = scrollService.minScrollExtent;
} else {
currentOffsetY -= onePageHeight;
}
final dy = scrollService.dy;
expect(dy, currentOffsetY);
}
for (int i = 1; i <= 5; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.pageUp,
);
final dy = scrollService.dy;
expect(dy == scrollService.minScrollExtent, true);
}
});
});
}

View file

@ -4,133 +4,267 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../../infra/clipboard_test.dart';
import '../../../infra/testable_editor.dart';
const text = 'Welcome to AppFlowy Editor 🔥!';
void main() async {
late MockClipboard mockClipboard;
setUp(() {
TestWidgetsFlutterBinding.ensureInitialized();
mockClipboard = const MockClipboard(html: null, text: null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (message) async {
switch (message.method) {
case "Clipboard.getData":
return mockClipboard.getData;
case "Clipboard.setData":
final args = message.arguments as Map<String, dynamic>;
mockClipboard = mockClipboard.copyWith(
text: args['text'],
);
}
return null;
});
});
group('paste_command_test.dart paste_plaintext', () {
testWidgets('works with formatted text', (tester) async {
final editor = tester.editor..addParagraph(initialText: text);
await editor.startTesting();
group('paste_command_test.dart', () {
testWidgets(
'Copy link',
(tester) async {
final editor = tester.editor..addParagraph(initialText: '');
await editor.startTesting();
await editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 0)),
);
await _applyFormatting(
editor,
BuiltInAttributeKey.underline,
LogicalKeyboardKey.keyU,
const link = 'https://appflowy.io/';
AppFlowyClipboard.mockSetData(
const AppFlowyClipboardData(text: link),
);
pasteCommand.execute(editor.editorState);
await tester.pumpAndSettle();
final delta = editor.nodeAtPath([0])!.delta!;
expect(delta.toPlainText(), link);
expect(
delta.everyAttributes(
(element) => element[AppFlowyRichTextKeys.href] == link,
),
true,
);
AppFlowyClipboard.mockSetData(null);
await editor.dispose();
},
);
testWidgets(
'Presses Command + A in small document and copy text and paste text',
(tester) async {
await _testHandleCopyPaste(tester, Document.fromJson(paragraphData));
});
testWidgets(
'Presses Command + A in small document and copy text and paste text multiple times',
(tester) async {
await _testHandleCopyMultiplePaste(
tester,
Document.fromJson(paragraphData),
);
await _applyFormatting(
editor,
BuiltInAttributeKey.italic,
LogicalKeyboardKey.keyI,
);
await _applyFormatting(
editor,
BuiltInAttributeKey.bold,
LogicalKeyboardKey.keyB,
);
await tester.pumpAndSettle();
final selection = Selection.single(
path: [0],
startOffset: 0,
endOffset: 7,
);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.keyC,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
AppFlowyClipboard.mockSetData(
AppFlowyClipboardData(
text: editor.editorState.getTextInSelection(selection).join('\n'),
),
);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.keyV,
isShiftPressed: true,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
await tester.pumpAndSettle();
await editor.updateSelection(selection);
final node = editor.nodeAtPath([0]);
_checkSelectionNotFormatted(
node!,
selection,
BuiltInAttributeKey.bold,
);
_checkSelectionNotFormatted(
node,
selection,
BuiltInAttributeKey.underline,
);
_checkSelectionNotFormatted(
node,
selection,
BuiltInAttributeKey.italic,
);
await editor.dispose();
});
});
}
Future<void> _applyFormatting(
TestableEditor editor,
String matchStyle,
LogicalKeyboardKey key,
Future<void> _testHandleCopyMultiplePaste(
WidgetTester tester,
Document document,
) async {
final selection = Selection.single(
path: [0],
startOffset: 0,
endOffset: 7,
);
await editor.updateSelection(selection);
final editor = tester.editor..initializeWithDocument(document);
await editor.startTesting();
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.pressKey(
key: key,
isMetaPressed: Platform.isMacOS,
key: LogicalKeyboardKey.keyA,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
final node = editor.nodeAtPath([0]);
handleCopy(editor.editorState);
deleteSelectedContent(editor.editorState);
expect(
node!.allSatisfyInSelection(selection, (delta) {
return delta.whereType<TextInsert>().every(
(el) => el.attributes?[matchStyle] == true,
);
}),
true,
pasteHTML(
editor.editorState,
documentToHTML(Document.fromJson(paragraphData)),
);
expect(
editor.editorState.document.toJson(),
paragraphData,
);
await editor.updateSelection(Selection.single(path: [0], startOffset: 10));
pasteHTML(
editor.editorState,
documentToHTML(Document.fromJson(paragraphData)),
);
expect(
editor.document.toJson(),
secondParagraph,
);
pasteHTML(
editor.editorState,
documentToHTML(Document.fromJson(paragraphData)),
);
expect(
editor.document.toJson(),
thirdParagraph,
);
await editor.dispose();
}
void _checkSelectionNotFormatted(
Node node,
Selection selection,
String matchStyle,
) {
expect(
node.allSatisfyInSelection(selection, (delta) {
return delta.whereType<TextInsert>().every(
(el) => el.attributes?[matchStyle] != true,
);
}),
true,
Future<void> _testHandleCopyPaste(
WidgetTester tester,
Document document,
) async {
final editor = tester.editor..initializeWithDocument(document);
await editor.startTesting(platform: TargetPlatform.windows);
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.pressKey(
key: LogicalKeyboardKey.keyA,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
handleCopy(editor.editorState);
deleteSelectedContent(editor.editorState);
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.pressKey(
key: LogicalKeyboardKey.keyP,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
final clipBoardData = await AppFlowyClipboard.getData();
handlePastePlainText(editor.editorState, clipBoardData.text!);
expect(editor.document.toJson(), plainTextJson);
await editor.dispose();
}
const paragraphData = {
"document": {
"type": "page",
"children": [
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'AppFlowy Editor is a '},
{
'insert': 'highly customizable',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'rich-text editor',
'attributes': {'italic': true}
}
]
}
}
]
}
};
const secondParagraph = {
"document": {
"type": "page",
"children": [
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
},
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
}
]
}
}
]
}
};
const plainTextJson = {
"document": {
"type": "page",
"children": [
{
"type": "paragraph",
"data": {
"delta": [
{
"insert":
"AppFlowy Editor is a highly customizable rich-text editor"
}
]
}
}
]
}
};
const thirdParagraph = {
"document": {
"type": "page",
"children": [
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
},
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
},
{"insert": "AppFlowy Editor is a "},
{
"insert": "highly customizable",
"attributes": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"attributes": {"italic": true}
}
]
}
}
]
}
};

View file

@ -0,0 +1,136 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
const text = 'Welcome to AppFlowy Editor 🔥!';
void main() async {
setUp(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('paste_plaintext_command', () {
testWidgets('works with formatted text', (tester) async {
final editor = tester.editor..addParagraph(initialText: text);
await editor.startTesting();
await _applyFormatting(
editor,
BuiltInAttributeKey.underline,
LogicalKeyboardKey.keyU,
);
await _applyFormatting(
editor,
BuiltInAttributeKey.italic,
LogicalKeyboardKey.keyI,
);
await _applyFormatting(
editor,
BuiltInAttributeKey.bold,
LogicalKeyboardKey.keyB,
);
await tester.pumpAndSettle();
final selection = Selection.single(
path: [0],
startOffset: 0,
endOffset: 7,
);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.keyC,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
AppFlowyClipboard.mockSetData(
AppFlowyClipboardData(
text: editor.editorState.getTextInSelection(selection).join('\n'),
),
);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.keyV,
isShiftPressed: true,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
await tester.pumpAndSettle();
await editor.updateSelection(selection);
final node = editor.nodeAtPath([0]);
_checkSelectionNotFormatted(
node!,
selection,
BuiltInAttributeKey.bold,
);
_checkSelectionNotFormatted(
node,
selection,
BuiltInAttributeKey.underline,
);
_checkSelectionNotFormatted(
node,
selection,
BuiltInAttributeKey.italic,
);
await editor.dispose();
});
});
}
Future<void> _applyFormatting(
TestableEditor editor,
String matchStyle,
LogicalKeyboardKey key,
) async {
final selection = Selection.single(
path: [0],
startOffset: 0,
endOffset: 7,
);
await editor.updateSelection(selection);
await editor.pressKey(
key: key,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
final node = editor.nodeAtPath([0]);
expect(
node!.allSatisfyInSelection(selection, (delta) {
return delta.whereType<TextInsert>().every(
(el) => el.attributes?[matchStyle] == true,
);
}),
true,
);
}
void _checkSelectionNotFormatted(
Node node,
Selection selection,
String matchStyle,
) {
expect(
node.allSatisfyInSelection(selection, (delta) {
return delta.whereType<TextInsert>().every(
(el) => el.attributes?[matchStyle] != true,
);
}),
true,
);
}

View file

@ -0,0 +1,268 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
import '../../../util/util.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('redo_undo_handler_test.dart', () {
// TODO: need to test more cases.
testWidgets('Redo should do nothing if Undo is not yet performed',
(tester) async {
await _testRedoWithoutUndo(tester);
});
testWidgets('Undo and Redo works properly with text formatting bold',
(tester) async {
await _testWithTextFormattingBold(tester);
});
testWidgets('Undo and Redo works properly with text formatting italics',
(tester) async {
await _testWithTextFormattingItalics(tester);
});
testWidgets('Undo and Redo works properly with text formatting underline',
(tester) async {
await _testWithTextFormattingUnderline(tester);
});
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> _testRedoWithoutUndo(WidgetTester tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final selection = Selection.single(path: [1], startOffset: 0);
await editor.updateSelection(selection);
expect(editor.documentRootLen, 3);
await _pressRedoCommand(editor);
expect(editor.documentRootLen, 3);
await editor.dispose();
}
Future<void> _testWithTextFormattingBold(WidgetTester tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final node = editor.nodeAtPath([0])!;
var selection = Selection.single(
path: [0],
startOffset: 1,
endOffset: text.length,
);
var result = node.allBold(selection);
expect(node.delta!.toPlainText(), text);
expect(result, false);
final start = Position(path: [0], offset: 0);
final end = Position(path: [0], offset: text.length);
selection = Selection(
start: start,
end: end,
);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.keyB,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
selection = Selection.single(
path: [0],
startOffset: 1,
endOffset: text.length,
);
result = node.allBold(selection);
expect(result, true);
//undo should remove bold style and make it normal.
await _pressUndoCommand(editor);
result = node.allBold(selection);
expect(result, false);
//redo should make text bold.
await _pressRedoCommand(editor);
result = node.allBold(selection);
expect(result, true);
await editor.dispose();
}
Future<void> _testWithTextFormattingItalics(WidgetTester tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final node = editor.nodeAtPath([0])!;
var allItalics = node.allItalic(
Selection.single(path: [0], startOffset: 1, endOffset: text.length),
);
expect(node.delta!.toPlainText(), text);
expect(allItalics, false);
final start = Position(path: [0], offset: 0);
final end = Position(path: [0], offset: text.length);
final selection = Selection(
start: start,
end: end,
);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.keyI,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
allItalics = node.allItalic(
Selection.single(path: [0], startOffset: 1, endOffset: text.length),
);
expect(allItalics, true);
//undo should remove italic style and make it normal.
await _pressUndoCommand(editor);
allItalics = node.allItalic(
Selection.single(path: [0], startOffset: 1, endOffset: text.length),
);
expect(allItalics, false);
//redo should make text italic again.
await _pressRedoCommand(editor);
allItalics = node.allItalic(
Selection.single(path: [0], startOffset: 1, endOffset: text.length),
);
expect(allItalics, true);
await editor.dispose();
}
Future<void> _testWithTextFormattingUnderline(WidgetTester tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final node = editor.nodeAtPath([0])!;
var allUnderline = node.allUnderline(
Selection.single(path: [0], startOffset: 1, endOffset: text.length),
);
expect(node.delta!.toPlainText(), text);
expect(allUnderline, false);
final start = Position(path: [0], offset: 0);
final end = Position(path: [0], offset: text.length);
final selection = Selection(
start: start,
end: end,
);
await editor.updateSelection(selection);
await editor.pressKey(
key: LogicalKeyboardKey.keyU,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
allUnderline = node.allUnderline(
Selection.single(path: [0], startOffset: 1, endOffset: text.length),
);
expect(allUnderline, true);
//undo should remove underline style and make it normal.
await _pressUndoCommand(editor);
allUnderline = node.allUnderline(
Selection.single(path: [0], startOffset: 1, endOffset: text.length),
);
expect(allUnderline, false);
//redo should make text underline.
await _pressRedoCommand(editor);
allUnderline = node.allUnderline(
Selection.single(path: [0], startOffset: 1, endOffset: text.length),
);
expect(allUnderline, true);
await editor.dispose();
}
Future<void> _testBackspaceUndoRedo(
WidgetTester tester,
bool isDownwardSelection,
) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: 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.pressKey(key: LogicalKeyboardKey.backspace);
expect(editor.documentRootLen, 2);
await _pressUndoCommand(editor);
expect(editor.documentRootLen, 3);
expect(editor.nodeAtPath([1])!.delta!.toPlainText(), text);
expect(editor.selection, selection);
await _pressRedoCommand(editor);
expect(editor.documentRootLen, 2);
await editor.dispose();
}
Future<void> _pressUndoCommand(TestableEditor editor) async {
await editor.pressKey(
key: LogicalKeyboardKey.keyZ,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isWindows || Platform.isLinux,
);
}
Future<void> _pressRedoCommand(TestableEditor editor) async {
await editor.pressKey(
key: Platform.isMacOS ? LogicalKeyboardKey.keyZ : LogicalKeyboardKey.keyY,
isMetaPressed: Platform.isMacOS,
isShiftPressed: Platform.isMacOS,
isControlPressed: !Platform.isMacOS,
);
}

View file

@ -0,0 +1,67 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('remove_line_command.dart ', () {
testWidgets('works properly and deletes a sentence', (tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: text.length),
);
//Welcome to Appflowy 😁|
await editor.pressKey(
key: LogicalKeyboardKey.backspace,
isControlPressed: Platform.isWindows || Platform.isLinux,
isAltPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
//fetching all the text that is still on the editor.
final selection = editor.selection!;
final node = editor.editorState.getNodeAtPath(selection.end.path)!;
expect(node.delta!.toPlainText().isEmpty, true);
await editor.dispose();
});
testWidgets('works in the middle of a word', (tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraph(initialText: text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 14),
);
//Welcome to App|flowy 😁
await editor.pressKey(
key: LogicalKeyboardKey.backspace,
isControlPressed: Platform.isWindows || Platform.isLinux,
isAltPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
//fetching all the text that is still on the editor.
final selection = editor.selection!;
final node = editor.editorState.getNodeAtPath(selection.end.path)!;
const expectedText = 'flowy 😁';
expect(node.delta!.toPlainText(), expectedText);
await editor.dispose();
});
});
}

View file

@ -101,7 +101,7 @@ void main() async {
);
await editor.startTesting();
// Welcome| to AppFlowy Editor 🔥!
// Welcome |to AppFlowy Editor 🔥!
const welcome = 'Welcome';
final selection = Selection.collapsed(
Position(path: [0], offset: welcome.length + 1),
@ -123,6 +123,133 @@ void main() async {
await editor.dispose();
});
testWidgets('repeatedly till line is empty', (tester) async {
List<String> words = ["Welcome", " ", "to", " ", "Appflowy", " ", "😁"];
final text = words.join();
final editor = tester.editor..addParagraph(initialText: text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: text.length),
);
await editor.pressKey(
key: LogicalKeyboardKey.backspace,
isControlPressed: Platform.isWindows || Platform.isLinux,
isAltPressed: Platform.isMacOS,
);
//fetching all the text that is still on the editor.
final selection = editor.selection!;
assert(selection.isSingle, true);
var node = editor.nodeAtPath(selection.end.path)!;
words.removeLast();
//expected: Welcome_to_Appflowy_
//here _ actually represents ' '
expect(node.delta!.toPlainText(), words.join());
await editor.pressKey(
key: LogicalKeyboardKey.backspace,
isControlPressed: Platform.isWindows || Platform.isLinux,
isAltPressed: Platform.isMacOS,
);
//fetching all the text that is still on the editor.
node = editor.nodeAtPath(selection.end.path)!;
//removes the whitespace
words.removeLast();
words.removeLast();
//expected is: Welcome_to_
expect(node.delta!.toPlainText(), words.join());
//we divide words.length by 2 becuase we know half words are whitespaces.
for (var i = 0; i < words.length / 2; i++) {
await editor.pressKey(
key: LogicalKeyboardKey.backspace,
isControlPressed: Platform.isWindows || Platform.isLinux,
isAltPressed: Platform.isMacOS,
);
}
node = editor.nodeAtPath(selection.end.path)!;
expect(node.delta!.toPlainText(), '');
await editor.dispose();
});
testWidgets('at the middle of a word', (tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraph(initialText: text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
await editor.pressKey(
key: LogicalKeyboardKey.backspace,
isControlPressed: Platform.isWindows || Platform.isLinux,
isAltPressed: Platform.isMacOS,
);
//fetching all the text that is still on the editor.
final selection = editor.selection!;
var node = editor.editorState.getNodeAtPath(selection.end.path)!;
//nothing happens when there is no words to the left of the cursor
expect(node.delta!.toPlainText(), text);
await editor.updateSelection(
Selection.single(path: [0], startOffset: 14),
);
//Welcome to App|flowy 😁
await editor.pressKey(
key: LogicalKeyboardKey.backspace,
isControlPressed: Platform.isWindows || Platform.isLinux,
isAltPressed: Platform.isMacOS,
);
//fetching all the text that is still on the editor.
node = editor.editorState.getNodeAtPath(selection.end.path)!;
const expectedText = 'Welcome to flowy 😁';
expect(node.delta!.toPlainText(), expectedText);
await editor.dispose();
});
testWidgets('works properly with only single whitespace', (tester) async {
//edge case that checks if pressing ctrl+backspace on null value
//after removing a whitespace, does not throw an exception.
const text = ' ';
final editor = tester.editor..addParagraph(initialText: text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: text.length),
);
// |
await editor.pressKey(
key: LogicalKeyboardKey.backspace,
isControlPressed: Platform.isWindows || Platform.isLinux,
isAltPressed: Platform.isMacOS,
);
//fetching all the text that is still on the editor.
final selection = editor.selection!;
final node = editor.editorState.getNodeAtPath(selection.end.path)!;
expect(node.delta!.toPlainText().isEmpty, true);
await editor.dispose();
});
});
group('remove the right word ', () {

Some files were not shown because too many files have changed in this diff Show more