feat: implement editor test infra

This commit is contained in:
Lucas.Xu 2022-08-13 00:18:59 +08:00
parent f6fad26e5e
commit a6bba5a0f9
12 changed files with 322 additions and 10 deletions

View File

@ -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';

View File

@ -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;

View File

@ -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++) {

View File

@ -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), () {

View File

@ -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);
}
}

View File

@ -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);
});
});
}

View File

@ -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);
});
}

View File

@ -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;
}

View File

@ -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);
}
}