appflowy-editor/test/service/selection_service_test.dart
Lucas cf0907738c
chore: optimize context menu logic (#1161)
* chore: optimize context menu logic

* chore: disable keyboard service and shortcut service
2025-11-12 09:33:18 +08:00

452 lines
15 KiB
Dart

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/src/editor/block_component/table_block_component/util.dart';
import '../new/infra/testable_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('selection_service.dart', () {
testWidgets('Single tap test ', (tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
// tap at the beginning
await tester.tapAt(rect.centerLeft);
expect(
editor.selection,
Selection.single(path: [1], startOffset: 0),
);
await tester.pumpAndSettle(const Duration(seconds: 1));
// tap at the ending
await tester.tapAt(rect.centerRight);
expect(
editor.selection,
Selection.single(path: [1], startOffset: text.length),
);
await editor.dispose();
});
testWidgets('Test double tap', (tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
// double tap
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.pump();
expect(
editor.selection,
Selection.single(path: [1], startOffset: 0, endOffset: 7),
);
await editor.dispose();
});
testWidgets('Test triple tap', (tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
// triple tap
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.pump();
expect(
editor.selection,
Selection.single(path: [1], startOffset: 0, endOffset: text.length),
);
await editor.dispose();
});
testWidgets(
'Test secondary tap (right-click) - no selection creates collapsed selection',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
expect(editor.selection, isNull);
await tester.tapAt(
rect.centerLeft + const Offset(50.0, 0.0),
buttons: kSecondaryButton,
);
await tester.pump();
expect(editor.selection, isNotNull);
expect(editor.selection!.isCollapsed, isTrue);
expect(editor.selection!.start.path, [1]);
final contextMenu = find.byType(ContextMenu);
expect(contextMenu, findsOneWidget);
await editor.dispose();
},
);
testWidgets(
'Test secondary tap - collapsed selection remains unchanged',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
await tester.tapAt(rect.centerLeft + const Offset(30.0, 0.0));
await tester.pump();
final originalSelection = editor.selection;
expect(originalSelection, isNotNull);
expect(originalSelection!.isCollapsed, isTrue);
await tester.tapAt(
rect.centerLeft + const Offset(80.0, 0.0),
buttons: kSecondaryButton,
);
await tester.pump();
expect(editor.selection, equals(originalSelection));
final contextMenu = find.byType(ContextMenu);
expect(contextMenu, findsOneWidget);
await editor.dispose();
},
);
testWidgets(
'Test secondary tap - non-collapsed selection remains when tapping within selected node',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.pumpAndSettle();
final originalSelection = editor.selection;
expect(originalSelection, isNotNull);
expect(originalSelection!.isCollapsed, isFalse);
expect(originalSelection.start.path, [1]);
expect(originalSelection.end.path, [1]);
await tester.tapAt(
rect.centerLeft + const Offset(20.0, 0.0),
buttons: kSecondaryButton,
);
await tester.pumpAndSettle();
expect(editor.selection, equals(originalSelection));
final contextMenu = find.byType(ContextMenu);
expect(contextMenu, findsOneWidget);
await editor.dispose();
},
);
testWidgets(
'Test secondary tap - multi-node selection remains when tapping within selected nodes',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final firstNode = editor.nodeAtPath([0]);
final secondNode = editor.nodeAtPath([1]);
final firstFinder = find.byKey(firstNode!.key);
final secondFinder = find.byKey(secondNode!.key);
final firstRect = tester.getRect(firstFinder);
final secondRect = tester.getRect(secondFinder);
await tester.timedDragFrom(
firstRect.centerLeft + const Offset(10.0, 0.0),
secondRect.center - firstRect.centerLeft - const Offset(10.0, 0.0),
const Duration(milliseconds: 500),
);
await tester.pumpAndSettle();
final originalSelection = editor.selection;
expect(originalSelection, isNotNull);
expect(originalSelection!.isCollapsed, isFalse);
await tester.tapAt(
secondRect.centerLeft + const Offset(30.0, 0.0),
buttons: kSecondaryButton,
);
await tester.pump();
expect(editor.selection, equals(originalSelection));
final contextMenu = find.byType(ContextMenu);
expect(contextMenu, findsOneWidget);
await editor.dispose();
},
);
testWidgets(
'Test secondary tap - selection changes when tapping outside selected nodes',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final firstNode = editor.nodeAtPath([0]);
final secondNode = editor.nodeAtPath([1]);
final thirdNode = editor.nodeAtPath([2]);
final firstFinder = find.byKey(firstNode!.key);
final secondFinder = find.byKey(secondNode!.key);
final thirdFinder = find.byKey(thirdNode!.key);
final firstRect = tester.getRect(firstFinder);
final secondRect = tester.getRect(secondFinder);
final thirdRect = tester.getRect(thirdFinder);
await tester.timedDragFrom(
firstRect.centerLeft + const Offset(10.0, 0.0),
secondRect.center - firstRect.centerLeft - const Offset(10.0, 0.0),
const Duration(milliseconds: 500),
);
await tester.pumpAndSettle();
final originalSelection = editor.selection;
expect(originalSelection, isNotNull);
expect(originalSelection!.isCollapsed, isFalse);
await tester.tapAt(
thirdRect.centerLeft + const Offset(30.0, 0.0),
buttons: kSecondaryButton,
);
await tester.pump();
expect(editor.selection, isNotNull);
expect(editor.selection!.isCollapsed, isTrue);
expect(editor.selection!.start.path, [2]);
expect(editor.selection, isNot(equals(originalSelection)));
final contextMenu = find.byType(ContextMenu);
expect(contextMenu, findsOneWidget);
await editor.dispose();
},
);
testWidgets(
'Test secondary tap - toolbar re-enabled when context menu is dismissed by clicking mask',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.pumpAndSettle();
final originalSelection = editor.selection;
expect(originalSelection, isNotNull);
expect(originalSelection!.isCollapsed, isFalse);
await tester.tapAt(
rect.centerLeft + const Offset(20.0, 0.0),
buttons: kSecondaryButton,
);
await tester.pumpAndSettle();
final contextMenu = find.byType(ContextMenu);
expect(contextMenu, findsOneWidget);
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
expect(find.byType(ContextMenu), findsNothing);
expect(editor.selection, equals(originalSelection));
expect(editor.selection, isNotNull);
await editor.dispose();
},
);
testWidgets(
'Test secondary tap - context menu and toolbar behavior',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.pumpAndSettle();
final originalSelection = editor.selection;
expect(originalSelection, isNotNull);
expect(originalSelection!.isCollapsed, isFalse);
await tester.tapAt(
rect.centerLeft + const Offset(20.0, 0.0),
buttons: kSecondaryButton,
);
await tester.pumpAndSettle();
final contextMenu = find.byType(ContextMenu);
expect(contextMenu, findsOneWidget);
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
expect(find.byType(ContextMenu), findsNothing);
expect(editor.selection, isNotNull);
await editor.dispose();
},
);
testWidgets(
'Test secondary tap - shortcuts blocked when context menu shows',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraphs(3, initialText: text);
await editor.startTesting();
final secondNode = editor.nodeAtPath([1]);
final finder = find.byKey(secondNode!.key);
final rect = tester.getRect(finder);
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
await tester.pumpAndSettle();
final originalSelection = editor.selection;
expect(originalSelection, isNotNull);
expect(originalSelection!.isCollapsed, isFalse);
await tester.tapAt(
rect.centerLeft + const Offset(20.0, 0.0),
buttons: kSecondaryButton,
);
await tester.pumpAndSettle();
final contextMenu = find.byType(ContextMenu);
expect(contextMenu, findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
expect(editor.selection, equals(originalSelection));
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
expect(find.byType(ContextMenu), findsNothing);
await editor.dispose();
},
);
testWidgets('single tap with horizontal nodes', (tester) async {
var tableNode = TableNode.fromList([
['00', '01', '02', '03', '04'],
['10', '11', '12', '13', '14'],
['20', '21', '22', '23', '24'],
['30', '31', '32', '33', '34'],
['40', '41', '42', '43', '44'],
['50', '51', '52', '53', '54'],
['60', '61', '62', '63', '64'],
['70', '71', '72', '73', '74'],
['80', '81', '82', '83', '84'],
]);
final editor = tester.editor..addNode(tableNode.node);
await editor.startTesting();
final cell04 = getCellNode(tableNode.node, 0, 4);
final finder = find.byKey(cell04!.key);
final rect = tester.getRect(finder);
// tap at the beginning
await tester.tapAt(rect.centerLeft);
expect(
editor.selection,
Selection.single(
path: cell04.childAtIndexOrNull(0)!.path,
startOffset: 0,
),
);
await editor.dispose();
});
testWidgets('Block selection and then single tap', (tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor..addParagraph(initialText: text);
await editor.startTesting();
final firstNode = editor.nodeAtPath([0]);
final finder = find.byKey(firstNode!.key);
final rect = tester.getRect(finder);
editor.editorState.selectionType = SelectionType.block;
// tap at the beginning
await tester.tapAt(rect.centerLeft);
expect(
editor.selection,
Selection.single(path: [0], startOffset: 0),
);
expect(editor.editorState.selectionType, SelectionType.inline);
await editor.dispose();
});
});
}