Merge pull request #853 from LucasXu0/test/page_up_down

Implement more tests.
This commit is contained in:
Nathan.fooo 2022-08-15 20:08:10 +08:00 committed by GitHub
commit dcbf657d84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 457 additions and 71 deletions

View File

@ -28,10 +28,12 @@ List<String> defaultListToolbarEventNames = [
'H1',
'H2',
'H3',
// 'B-List',
// 'N-List',
];
mixin ToolbarMixin<T extends StatefulWidget> on State<T> {
void hide();
}
class ToolbarWidget extends StatefulWidget {
const ToolbarWidget({
Key? key,
@ -50,7 +52,7 @@ class ToolbarWidget extends StatefulWidget {
State<ToolbarWidget> createState() => _ToolbarWidgetState();
}
class _ToolbarWidgetState extends State<ToolbarWidget> {
class _ToolbarWidgetState extends State<ToolbarWidget> with ToolbarMixin {
final GlobalKey _listToolbarKey = GlobalKey();
final toolbarHeight = 32.0;
@ -63,21 +65,6 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
OverlayEntry? _listToolbarOverlay;
@override
void initState() {
super.initState();
widget.editorState.service.selectionService.currentSelection
.addListener(_onSelectionChange);
}
@override
void dispose() {
widget.editorState.service.selectionService.currentSelection
.removeListener(_onSelectionChange);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned(
@ -92,6 +79,12 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
);
}
@override
void hide() {
_listToolbarOverlay?.remove();
_listToolbarOverlay = null;
}
Widget _buildToolbar(BuildContext context) {
return Material(
borderRadius: BorderRadius.circular(cornerRadius),
@ -212,9 +205,4 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
}
assert(false, 'Could not find the event handler for $eventName');
}
void _onSelectionChange() {
_listToolbarOverlay?.remove();
_listToolbarOverlay = null;
}
}

View File

@ -2,25 +2,16 @@ import 'package:flowy_editor/flowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
double? getEditorHeight(EditorState editorState) {
final renderObj =
editorState.service.scrollServiceKey.currentContext?.findRenderObject();
if (renderObj is RenderBox) {
return renderObj.size.height;
}
return null;
}
FlowyKeyEventHandler pageUpDownHandler = (editorState, event) {
if (event.logicalKey == LogicalKeyboardKey.pageUp) {
final scrollHeight = getEditorHeight(editorState);
final scrollHeight = editorState.service.scrollService?.onePageHeight;
final scrollService = editorState.service.scrollService;
if (scrollHeight != null && scrollService != null) {
scrollService.scrollTo(scrollService.dy - scrollHeight);
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.pageDown) {
final scrollHeight = getEditorHeight(editorState);
final scrollHeight = editorState.service.scrollService?.onePageHeight;
final scrollService = editorState.service.scrollService;
if (scrollHeight != null && scrollService != null) {
scrollService.scrollTo(scrollService.dy + scrollHeight);

View File

@ -11,6 +11,9 @@ import 'package:flowy_editor/src/extensions/node_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@visibleForTesting
List<PopupListItem> get popupListItems => _popupListItems;
final List<PopupListItem> _popupListItems = [
PopupListItem(
text: 'Text',
@ -94,6 +97,14 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
_editorState = editorState;
WidgetsBinding.instance.addPostFrameCallback((_) {
_selectionChangeBySlash = false;
editorState.service.selectionService.currentSelection
.removeListener(clearPopupList);
editorState.service.selectionService.currentSelection
.addListener(clearPopupList);
editorState.service.scrollService?.disable();
showPopupList(context, editorState, selectionRects.first.bottomRight);
});
@ -115,24 +126,21 @@ void showPopupList(
);
Overlay.of(context)?.insert(_popupListOverlay!);
editorState.service.selectionService.currentSelection
.removeListener(clearPopupList);
editorState.service.selectionService.currentSelection
.addListener(clearPopupList);
editorState.service.scrollService?.disable();
}
void clearPopupList() {
if (_popupListOverlay == null || _editorState == null) {
return;
}
final isSelectionDisposed =
_editorState?.service.selectionServiceKey.currentState != null;
if (isSelectionDisposed) {
final selection =
_editorState?.service.selectionService.currentSelection.value;
if (selection == null) {
return;
}
}
if (_selectionChangeBySlash) {
_selectionChangeBySlash = false;
return;

View File

@ -3,9 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flowy_editor/src/document/node.dart';
import 'package:flowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
import 'package:flowy_editor/src/service/keyboard_service.dart';
import 'package:flutter/services.dart';
FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
if (!event.isMetaPressed || event.character == null) {
if (!event.isMetaPressed) {
return KeyEventResult.ignored;
}
@ -17,26 +18,19 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
return KeyEventResult.ignored;
}
switch (event.character!) {
// bold
case 'B':
case 'b':
if (event.logicalKey == LogicalKeyboardKey.keyB) {
formatBold(editorState);
return KeyEventResult.handled;
case 'I':
case 'i':
} else if (event.logicalKey == LogicalKeyboardKey.keyI) {
formatItalic(editorState);
return KeyEventResult.handled;
case 'U':
case 'u':
} else if (event.logicalKey == LogicalKeyboardKey.keyU) {
formatUnderline(editorState);
return KeyEventResult.handled;
case 'S':
case 's':
} else if (event.logicalKey == LogicalKeyboardKey.keyS &&
event.isShiftPressed) {
formatStrikethrough(editorState);
return KeyEventResult.handled;
default:
break;
}
return KeyEventResult.ignored;

View File

@ -1,8 +1,15 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flowy_editor/src/extensions/object_extensions.dart';
abstract class FlowyScrollService {
double get dy;
double? get onePageHeight;
int? get page;
double get maxScrollExtent;
double get minScrollExtent;
void scrollTo(double dy);
@ -32,6 +39,27 @@ class _FlowyScrollState extends State<FlowyScroll>
@override
double get dy => _scrollController.position.pixels;
@override
double? get onePageHeight {
final renderBox = context.findRenderObject()?.unwrapOrNull<RenderBox>();
return renderBox?.size.height;
}
@override
double get maxScrollExtent => _scrollController.position.maxScrollExtent;
@override
double get minScrollExtent => _scrollController.position.minScrollExtent;
@override
int? get page {
if (onePageHeight != null) {
final scrollExtent = maxScrollExtent - minScrollExtent;
return (scrollExtent / onePageHeight!).ceil();
}
return null;
}
@override
Widget build(BuildContext context) {
return Listener(

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/src/render/selection/toolbar_widget.dart';
import 'package:flowy_editor/src/extensions/object_extensions.dart';
abstract class FlowyToolbarService {
/// Show the toolbar widget beside the offset.
@ -28,12 +29,15 @@ class FlowyToolbar extends StatefulWidget {
class _FlowyToolbarState extends State<FlowyToolbar>
implements FlowyToolbarService {
OverlayEntry? _toolbarOverlay;
final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget');
@override
void showInOffset(Offset offset, LayerLink layerLink) {
_toolbarOverlay?.remove();
hide();
_toolbarOverlay = OverlayEntry(
builder: (context) => ToolbarWidget(
key: _toolbarWidgetKey,
editorState: widget.editorState,
layerLink: layerLink,
offset: offset.translate(0, -37.0),
@ -45,6 +49,7 @@ class _FlowyToolbarState extends State<FlowyToolbar>
@override
void hide() {
_toolbarWidgetKey.currentState?.unwrapOrNull<ToolbarMixin>()?.hide();
_toolbarOverlay?.remove();
_toolbarOverlay = null;
}
@ -55,4 +60,11 @@ class _FlowyToolbarState extends State<FlowyToolbar>
child: widget.child,
);
}
@override
void dispose() {
hide();
super.dispose();
}
}

View File

@ -72,8 +72,20 @@ class EditorWidgetTester {
await tester.pumpAndSettle();
}
Future<void> pressLogicKey(LogicalKeyboardKey key) async {
final testRawKeyEventData = TestRawKeyEventData(logicalKey: key).toKeyEvent;
Future<void> pressLogicKey(
LogicalKeyboardKey key, {
bool isControlPressed = false,
bool isShiftPressed = false,
bool isAltPressed = false,
bool isMetaPressed = false,
}) async {
final testRawKeyEventData = TestRawKeyEventData(
logicalKey: key,
isControlPressed: isControlPressed,
isShiftPressed: isShiftPressed,
isAltPressed: isAltPressed,
isMetaPressed: isMetaPressed,
).toKeyEvent;
_editorState.service.keyboardService!.onKey(testRawKeyEventData);
await tester.pumpAndSettle();
}

View File

@ -1,7 +1,25 @@
import 'package:flutter/services.dart';
class TestRawKeyEvent extends RawKeyDownEvent {
const TestRawKeyEvent({required super.data});
const TestRawKeyEvent({
required super.data,
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;
}
class TestRawKeyEventData extends RawKeyEventData {
@ -46,7 +64,13 @@ class TestRawKeyEventData extends RawKeyEventData {
String get keyLabel => throw UnimplementedError();
RawKeyEvent get toKeyEvent {
return TestRawKeyEvent(data: this);
return TestRawKeyEvent(
data: this,
isAltPressed: isAltPressed,
isControlPressed: isControlPressed,
isMetaPressed: isMetaPressed,
isShiftPressed: isShiftPressed,
);
}
}
@ -61,6 +85,36 @@ extension on LogicalKeyboardKey {
if (this == LogicalKeyboardKey.delete) {
return PhysicalKeyboardKey.delete;
}
if (this == LogicalKeyboardKey.pageDown) {
return PhysicalKeyboardKey.pageDown;
}
if (this == LogicalKeyboardKey.pageUp) {
return PhysicalKeyboardKey.pageUp;
}
if (this == LogicalKeyboardKey.slash) {
return PhysicalKeyboardKey.slash;
}
if (this == LogicalKeyboardKey.arrowDown) {
return PhysicalKeyboardKey.arrowDown;
}
if (this == LogicalKeyboardKey.keyA) {
return PhysicalKeyboardKey.keyA;
}
if (this == LogicalKeyboardKey.keyB) {
return PhysicalKeyboardKey.keyB;
}
if (this == LogicalKeyboardKey.keyI) {
return PhysicalKeyboardKey.keyI;
}
if (this == LogicalKeyboardKey.keyS) {
return PhysicalKeyboardKey.keyS;
}
if (this == LogicalKeyboardKey.keyU) {
return PhysicalKeyboardKey.keyU;
}
if (this == LogicalKeyboardKey.keyZ) {
return PhysicalKeyboardKey.keyZ;
}
throw UnimplementedError();
}
}

View File

@ -0,0 +1,75 @@
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('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;
for (var i = 0; i < 1000; i++) {
editor.insertTextNode(text);
}
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
final scrollService = editor.editorState.service.scrollService;
expect(scrollService != null, true);
if (scrollService == null) {
return;
}
final page = scrollService.page;
final onePageHeight = scrollService.onePageHeight;
expect(page != null, true);
expect(onePageHeight != null, true);
// Pressing the pageDown key continuously.
var currentOffsetY = 0.0;
for (int i = 1; i <= page!; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.pageDown,
);
currentOffsetY += onePageHeight!;
final dy = scrollService.dy;
expect(dy, currentOffsetY);
}
for (int i = 1; i <= 5; i++) {
await editor.pressLogicKey(
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.pressLogicKey(
LogicalKeyboardKey.pageUp,
);
currentOffsetY -= onePageHeight!;
final dy = editor.editorState.service.scrollService?.dy;
expect(dy, currentOffsetY);
}
for (int i = 1; i <= 5; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.pageUp,
);
final dy = scrollService.dy;
expect(dy == scrollService.minScrollExtent, true);
}
});
});
}

View File

@ -0,0 +1,60 @@
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('redo_undo_handler_test.dart', () {
// TODO: need to test more cases.
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> _testBackspaceUndoRedo(
WidgetTester tester, bool isDownwardSelection) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text)
..insertTextNode(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.pressLogicKey(LogicalKeyboardKey.backspace);
expect(editor.documentLength, 2);
await editor.pressLogicKey(
LogicalKeyboardKey.keyZ,
isMetaPressed: true,
);
expect(editor.documentLength, 3);
expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text);
expect(editor.documentSelection, selection);
await editor.pressLogicKey(
LogicalKeyboardKey.keyZ,
isMetaPressed: true,
isShiftPressed: true,
);
expect(editor.documentLength, 2);
}

View File

@ -0,0 +1,38 @@
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('select_all_handler_test.dart', () {
testWidgets('Presses Command + A in small document', (tester) async {
await _testSelectAllHandler(tester, 10);
});
testWidgets('Presses Command + A in small document', (tester) async {
await _testSelectAllHandler(tester, 1000);
});
});
}
Future<void> _testSelectAllHandler(WidgetTester tester, int lines) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor;
for (var i = 0; i < lines; i++) {
editor.insertTextNode(text);
}
await editor.startTesting();
await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true);
expect(
editor.documentSelection,
Selection(
start: Position(path: [0], offset: 0),
end: Position(path: [lines - 1], offset: text.length),
),
);
}

View File

@ -0,0 +1,39 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.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('slash_handler.dart', () {
testWidgets('Presses / to trigger popup list ', (tester) async {
const text = 'Welcome to Appflowy 😁';
const lines = 3;
final editor = tester.editor;
for (var i = 0; i < lines; i++) {
editor.insertTextNode(text);
}
await editor.startTesting();
await editor.updateSelection(Selection.single(path: [1], startOffset: 0));
await editor.pressLogicKey(LogicalKeyboardKey.slash);
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
expect(find.byType(PopupListWidget, skipOffstage: false), findsOneWidget);
for (final item in popupListItems) {
expect(find.byWidget(item.icon), findsOneWidget);
}
await editor.updateSelection(Selection.single(path: [1], startOffset: 0));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.byType(PopupListWidget, skipOffstage: false), findsNothing);
});
});
}

View File

@ -0,0 +1,87 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:flowy_editor/src/extensions/text_node_extensions.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('update_text_style_by_command_x_handler.dart', () {
testWidgets('Presses Command + B to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.bold,
LogicalKeyboardKey.keyB,
);
});
testWidgets('Presses Command + I to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.italic,
LogicalKeyboardKey.keyI,
);
});
testWidgets('Presses Command + U to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.underline,
LogicalKeyboardKey.keyU,
);
});
testWidgets('Presses Command + S to update text style', (tester) async {
await _testUpdateTextStyleByCommandX(
tester,
StyleKey.strikethrough,
LogicalKeyboardKey.keyS,
);
});
});
}
Future<void> _testUpdateTextStyleByCommandX(
WidgetTester tester, String matchStyle, LogicalKeyboardKey key) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text)
..insertTextNode(text);
await editor.startTesting();
var selection =
Selection.single(path: [1], startOffset: 2, endOffset: text.length - 2);
await editor.updateSelection(selection);
await editor.pressLogicKey(
key,
isShiftPressed: key == LogicalKeyboardKey.keyS,
isMetaPressed: true,
);
var textNode = editor.nodeAtPath([1]) as TextNode;
expect(textNode.allSatisfyInSelection(matchStyle, selection), true);
selection =
Selection.single(path: [1], startOffset: 0, endOffset: text.length);
await editor.updateSelection(selection);
await editor.pressLogicKey(
key,
isShiftPressed: key == LogicalKeyboardKey.keyS,
isMetaPressed: true,
);
textNode = editor.nodeAtPath([1]) as TextNode;
expect(textNode.allSatisfyInSelection(matchStyle, selection), true);
await editor.updateSelection(selection);
await editor.pressLogicKey(
key,
isShiftPressed: key == LogicalKeyboardKey.keyS,
isMetaPressed: true,
);
textNode = editor.nodeAtPath([1]) as TextNode;
expect(textNode.allSatisfyInSelection(matchStyle, selection), false);
}