mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement editor test infra
This commit is contained in:
parent
f6fad26e5e
commit
a6bba5a0f9
@ -6,6 +6,7 @@ export 'src/document/position.dart';
|
||||
export 'src/document/selection.dart';
|
||||
export 'src/document/state_tree.dart';
|
||||
export 'src/document/text_delta.dart';
|
||||
export 'src/document/attributes.dart';
|
||||
export 'src/editor_state.dart';
|
||||
export 'src/operation/operation.dart';
|
||||
export 'src/operation/transaction.dart';
|
||||
|
@ -111,6 +111,27 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||
return childAtIndex(path.first)?.childAtPath(path.sublist(1));
|
||||
}
|
||||
|
||||
void insert(Node entry, {int? index}) {
|
||||
index ??= children.length;
|
||||
|
||||
if (children.isEmpty) {
|
||||
entry.parent = this;
|
||||
children.add(entry);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final length = children.length;
|
||||
|
||||
if (index >= length) {
|
||||
children.last.insertAfter(entry);
|
||||
} else if (index <= 0) {
|
||||
children.first.insertBefore(entry);
|
||||
} else {
|
||||
childAtIndex(index)?.insertBefore(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void insertAfter(Node entry) {
|
||||
entry.parent = parent;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flowy_editor/src/document/node.dart';
|
||||
import 'package:flowy_editor/src/document/path.dart';
|
||||
import 'package:flowy_editor/src/document/text_delta.dart';
|
||||
@ -27,9 +29,18 @@ class StateTree {
|
||||
return false;
|
||||
}
|
||||
Node? insertedNode = root.childAtPath(
|
||||
path.sublist(0, path.length - 1) + [path.last - 1],
|
||||
path.sublist(0, path.length - 1) + [max(0, path.last - 1)],
|
||||
);
|
||||
if (insertedNode == null) {
|
||||
final insertedNode = root.childAtPath(
|
||||
path.sublist(0, path.length - 1),
|
||||
);
|
||||
if (insertedNode != null) {
|
||||
for (final node in nodes) {
|
||||
insertedNode.insert(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
|
@ -51,6 +51,9 @@ class EditorState {
|
||||
final UndoManager undoManager = UndoManager();
|
||||
Selection? _cursorSelection;
|
||||
|
||||
/// TODO: only for testing.
|
||||
bool disableSealTimer = false;
|
||||
|
||||
Selection? get cursorSelection {
|
||||
return _cursorSelection;
|
||||
}
|
||||
@ -106,6 +109,9 @@ class EditorState {
|
||||
}
|
||||
|
||||
_debouncedSealHistoryItem() {
|
||||
if (disableSealTimer) {
|
||||
return;
|
||||
}
|
||||
_debouncedSealHistoryItemTimer?.cancel();
|
||||
_debouncedSealHistoryItemTimer =
|
||||
Timer(const Duration(milliseconds: 1000), () {
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class FlowyKeyboardService {
|
||||
KeyEventResult onKey(RawKeyEvent event);
|
||||
void enable();
|
||||
void disable();
|
||||
}
|
||||
@ -65,15 +66,8 @@ class _FlowyKeyboardState extends State<FlowyKeyboard>
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
|
||||
void _onFocusChange(bool value) {
|
||||
debugPrint('[KeyBoard Service] focus change $value');
|
||||
}
|
||||
|
||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||
if (!isFocus) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@override
|
||||
KeyEventResult onKey(RawKeyEvent event) {
|
||||
debugPrint('on keyboard event $event');
|
||||
|
||||
if (event is! RawKeyDownEvent) {
|
||||
@ -97,4 +91,16 @@ class _FlowyKeyboardState extends State<FlowyKeyboard>
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
void _onFocusChange(bool value) {
|
||||
debugPrint('[KeyBoard Service] focus change $value');
|
||||
}
|
||||
|
||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||
if (!isFocus) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
return onKey(event);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,69 @@
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('Enter key without shift handler', () {
|
||||
testWidgets('Pressing enter key in empty document', (tester) async {
|
||||
final editor = tester.editor
|
||||
..initialize()
|
||||
..insertEmptyTextNode();
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(
|
||||
Selection.collapsed(
|
||||
Position(path: [0], offset: 0),
|
||||
),
|
||||
);
|
||||
// Pressing the enter key continuously.
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.enter,
|
||||
);
|
||||
expect(editor.documentLength, i + 1);
|
||||
expect(editor.documentSelection,
|
||||
Selection.collapsed(Position(path: [i], offset: 0)));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Pressing enter key in non-empty document', (tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
var lines = 5;
|
||||
|
||||
final editor = tester.editor..initialize();
|
||||
for (var i = 1; i <= lines; i++) {
|
||||
editor.insertTextNode(text: text);
|
||||
}
|
||||
await editor.startTesting();
|
||||
|
||||
expect(editor.documentLength, lines);
|
||||
|
||||
// Pressing the enter key in last line.
|
||||
await editor.updateSelection(
|
||||
Selection.collapsed(
|
||||
Position(path: [lines - 1], offset: 0),
|
||||
),
|
||||
);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.enter,
|
||||
);
|
||||
lines += 1;
|
||||
|
||||
expect(editor.documentLength, lines);
|
||||
expect(editor.documentSelection,
|
||||
Selection.collapsed(Position(path: [lines - 1], offset: 0)));
|
||||
var lastNode = editor.nodeAtPath([lines - 1]);
|
||||
expect(lastNode != null, true);
|
||||
expect(lastNode is TextNode, true);
|
||||
lastNode = lastNode as TextNode;
|
||||
expect(lastNode.delta.toRawString(), text);
|
||||
expect((lastNode.previous as TextNode).delta.toRawString(), '');
|
||||
expect(
|
||||
(lastNode.previous!.previous as TextNode).delta.toRawString(), text);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../infra/test_raw_key_event.dart';
|
||||
|
||||
void main() async {
|
||||
final file = File('test_assets/example.json');
|
||||
final json = jsonDecode(await file.readAsString());
|
||||
print(json);
|
||||
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
testWidgets('init FlowyEditor ', (tester) async {
|
||||
final editorState = EditorState(
|
||||
document: StateTree.fromJson(json),
|
||||
);
|
||||
final flowyEditor = FlowyEditor(editorState: editorState);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: flowyEditor,
|
||||
));
|
||||
editorState.service.selectionService
|
||||
.updateSelection(Selection.collapsed(Position(path: [0], offset: 1)));
|
||||
await tester.pumpAndSettle();
|
||||
final key = const TestRawKeyEventData(
|
||||
logicalKey: LogicalKeyboardKey.enter,
|
||||
physicalKey: PhysicalKeyboardKey.enter,
|
||||
).toKeyEvent;
|
||||
editorState.service.keyboardService!.onKey(key);
|
||||
await tester.pumpAndSettle();
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
});
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'test_raw_key_event.dart';
|
||||
|
||||
class EditorWidgetTester {
|
||||
EditorWidgetTester({
|
||||
required this.tester,
|
||||
});
|
||||
|
||||
final WidgetTester tester;
|
||||
late EditorState _editorState;
|
||||
|
||||
EditorState get editorState => _editorState;
|
||||
Node get root => _editorState.document.root;
|
||||
|
||||
int get documentLength => _editorState.document.root.children.length;
|
||||
Selection? get documentSelection =>
|
||||
_editorState.service.selectionService.currentSelection.value;
|
||||
|
||||
Future<EditorWidgetTester> startTesting() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: FlowyEditor(
|
||||
editorState: _editorState,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
_editorState = _createEmptyDocument();
|
||||
}
|
||||
|
||||
insert<T extends Node>(T node) {
|
||||
_editorState.document.root.insert(node);
|
||||
}
|
||||
|
||||
insertEmptyTextNode() {
|
||||
insert(TextNode.empty());
|
||||
}
|
||||
|
||||
insertTextNode({String? text, Attributes? attributes}) {
|
||||
insert(
|
||||
TextNode(
|
||||
type: 'text',
|
||||
delta: Delta(
|
||||
[TextInsert(text ?? 'Test')],
|
||||
),
|
||||
attributes: attributes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Node? nodeAtPath(Path path) {
|
||||
return root.childAtPath(path);
|
||||
}
|
||||
|
||||
Future<void> updateSelection(Selection? selection) async {
|
||||
if (selection == null) {
|
||||
_editorState.service.selectionService.clearSelection();
|
||||
} else {
|
||||
_editorState.service.selectionService.updateSelection(selection);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> pressLogicKey(LogicalKeyboardKey key) async {
|
||||
late RawKeyEvent testRawKeyEventData;
|
||||
if (key == LogicalKeyboardKey.enter) {
|
||||
testRawKeyEventData = const TestRawKeyEventData(
|
||||
logicalKey: LogicalKeyboardKey.enter,
|
||||
physicalKey: PhysicalKeyboardKey.enter,
|
||||
).toKeyEvent;
|
||||
}
|
||||
_editorState.service.keyboardService!.onKey(testRawKeyEventData);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Node _createEmptyEditorRoot() {
|
||||
return Node(
|
||||
type: 'editor',
|
||||
children: LinkedList(),
|
||||
attributes: {},
|
||||
);
|
||||
}
|
||||
|
||||
EditorState _createEmptyDocument() {
|
||||
return EditorState(
|
||||
document: StateTree(
|
||||
root: _createEmptyEditorRoot(),
|
||||
),
|
||||
)..disableSealTimer = true;
|
||||
}
|
||||
}
|
||||
|
||||
extension TestEditorExtension on WidgetTester {
|
||||
EditorWidgetTester get editor => EditorWidgetTester(tester: this);
|
||||
EditorState get editorState => editor.editorState;
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class TestRawKeyEvent extends RawKeyDownEvent {
|
||||
const TestRawKeyEvent({required super.data});
|
||||
}
|
||||
|
||||
class TestRawKeyEventData extends RawKeyEventData {
|
||||
const TestRawKeyEventData({
|
||||
required this.logicalKey,
|
||||
required this.physicalKey,
|
||||
this.isControlPressed = false,
|
||||
this.isShiftPressed = false,
|
||||
this.isAltPressed = false,
|
||||
this.isMetaPressed = false,
|
||||
});
|
||||
|
||||
@override
|
||||
final bool isControlPressed;
|
||||
|
||||
@override
|
||||
final bool isShiftPressed;
|
||||
|
||||
@override
|
||||
final bool isAltPressed;
|
||||
|
||||
@override
|
||||
final bool isMetaPressed;
|
||||
|
||||
@override
|
||||
final LogicalKeyboardKey logicalKey;
|
||||
|
||||
@override
|
||||
final PhysicalKeyboardKey physicalKey;
|
||||
|
||||
@override
|
||||
KeyboardSide? getModifierSide(ModifierKey key) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
bool isModifierPressed(ModifierKey key,
|
||||
{KeyboardSide side = KeyboardSide.any}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
String get keyLabel => throw UnimplementedError();
|
||||
|
||||
RawKeyEvent get toKeyEvent {
|
||||
return TestRawKeyEvent(data: this);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user