2024-01-29 10:26:45 +08:00
|
|
|
import 'dart:async';
|
2023-06-27 15:17:51 +08:00
|
|
|
import 'dart:ui';
|
|
|
|
|
|
|
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
2023-11-02 15:24:17 +08:00
|
|
|
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
2023-09-26 14:37:10 +08:00
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
2024-02-20 08:22:06 +05:30
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
|
2024-09-22 09:35:11 +08:00
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
|
2023-06-27 15:17:51 +08:00
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
2024-09-25 22:44:59 +08:00
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_title.dart';
|
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
|
2023-06-27 15:17:51 +08:00
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
2024-07-25 14:47:08 +02:00
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
2023-10-02 09:12:24 +02:00
|
|
|
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
2024-08-09 21:50:47 +08:00
|
|
|
import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
|
2024-08-06 11:47:38 +08:00
|
|
|
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
2024-09-12 14:40:19 +08:00
|
|
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
2023-06-27 15:17:51 +08:00
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
2024-08-06 11:47:38 +08:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/services.dart';
|
2023-11-14 22:33:07 +08:00
|
|
|
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
2023-06-15 16:33:44 +08:00
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
|
|
|
import 'util.dart';
|
|
|
|
|
|
|
|
extension EditorWidgetTester on WidgetTester {
|
|
|
|
EditorOperations get editor => EditorOperations(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
class EditorOperations {
|
|
|
|
const EditorOperations(this.tester);
|
|
|
|
|
|
|
|
final WidgetTester tester;
|
|
|
|
|
2024-04-30 14:09:08 +02:00
|
|
|
EditorState getCurrentEditorState() =>
|
|
|
|
tester.widget<AppFlowyEditor>(find.byType(AppFlowyEditor)).editorState;
|
2023-06-21 19:53:29 +08:00
|
|
|
|
2024-09-22 09:35:11 +08:00
|
|
|
Node getNodeAtPath(Path path) {
|
|
|
|
final editorState = getCurrentEditorState();
|
|
|
|
return editorState.getNodeAtPath(path)!;
|
|
|
|
}
|
|
|
|
|
2023-06-15 16:33:44 +08:00
|
|
|
/// Tap the line of editor at [index]
|
|
|
|
Future<void> tapLineOfEditorAt(int index) async {
|
2023-07-11 18:49:29 +07:00
|
|
|
final textBlocks = find.byType(AppFlowyRichText);
|
2023-06-29 17:58:30 +05:30
|
|
|
index = index.clamp(0, textBlocks.evaluate().length - 1);
|
2023-06-15 16:33:44 +08:00
|
|
|
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
|
2023-06-29 07:04:24 +05:00
|
|
|
await tester.pumpAndSettle();
|
2023-06-15 16:33:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Hover on cover plugin button above the document
|
2023-06-27 15:17:51 +08:00
|
|
|
Future<void> hoverOnCoverToolbar() async {
|
|
|
|
final coverToolbar = find.byType(DocumentHeaderToolbar);
|
|
|
|
await tester.startGesture(
|
|
|
|
tester.getBottomLeft(coverToolbar).translate(5, -5),
|
|
|
|
kind: PointerDeviceKind.mouse,
|
|
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Taps on the 'Add Icon' button in the cover toolbar
|
|
|
|
Future<void> tapAddIconButton() async {
|
|
|
|
await tester.tapButtonWithName(
|
|
|
|
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
|
|
|
);
|
2023-11-02 15:24:17 +08:00
|
|
|
expect(find.byType(FlowyEmojiPicker), findsOneWidget);
|
|
|
|
}
|
|
|
|
|
2023-11-13 12:00:03 +08:00
|
|
|
Future<void> tapGettingStartedIcon() async {
|
|
|
|
await tester.tapButton(
|
|
|
|
find.descendant(
|
2024-04-30 16:55:15 +08:00
|
|
|
of: find.byType(DocumentCoverWidget),
|
2023-11-13 12:00:03 +08:00
|
|
|
matching: find.findTextInFlowyText('⭐️'),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-11-02 15:24:17 +08:00
|
|
|
/// Taps on the 'Skin tone' button
|
|
|
|
///
|
|
|
|
/// Must call [tapAddIconButton] first.
|
|
|
|
Future<void> changeEmojiSkinTone(EmojiSkinTone skinTone) async {
|
|
|
|
await tester.tapButton(
|
|
|
|
find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()),
|
|
|
|
);
|
2023-11-13 12:00:03 +08:00
|
|
|
final skinToneButton = find.byKey(emojiSkinToneKey(skinTone.icon));
|
2023-11-02 15:24:17 +08:00
|
|
|
await tester.tapButton(skinToneButton);
|
2023-06-27 15:17:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
|
|
|
|
Future<void> tapRemoveIconButton({bool isInPicker = false}) async {
|
2024-05-27 08:51:49 +08:00
|
|
|
final Finder button = !isInPicker
|
|
|
|
? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr())
|
|
|
|
: find.descendant(
|
2024-08-06 11:47:38 +08:00
|
|
|
of: find.byType(FlowyIconEmojiPicker),
|
2024-05-27 08:51:49 +08:00
|
|
|
matching: find.text(LocaleKeys.button_remove.tr()),
|
|
|
|
);
|
2023-06-27 15:17:51 +08:00
|
|
|
await tester.tapButton(button);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Requires that the document must already have an icon. This opens the icon
|
|
|
|
/// picker
|
|
|
|
Future<void> tapOnIconWidget() async {
|
|
|
|
final iconWidget = find.byType(EmojiIconWidget);
|
|
|
|
await tester.tapButton(iconWidget);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> tapOnAddCover() async {
|
|
|
|
await tester.tapButtonWithName(
|
|
|
|
LocaleKeys.document_plugins_cover_addCover.tr(),
|
2023-06-15 16:33:44 +08:00
|
|
|
);
|
2023-06-27 15:17:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> tapOnChangeCover() async {
|
|
|
|
await tester.tapButtonWithName(
|
|
|
|
LocaleKeys.document_plugins_cover_changeCover.tr(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> switchSolidColorBackground() async {
|
|
|
|
final findPurpleButton = find.byWidgetPredicate(
|
2023-08-10 17:35:27 +07:00
|
|
|
(widget) => widget is ColorItem && widget.option.name == 'Purple',
|
2023-06-27 15:17:51 +08:00
|
|
|
);
|
|
|
|
await tester.tapButton(findPurpleButton);
|
|
|
|
}
|
|
|
|
|
2023-07-01 23:13:09 +08:00
|
|
|
Future<void> addNetworkImageCover(String imageUrl) async {
|
2024-01-20 23:16:18 +08:00
|
|
|
final embedLinkButton = find.findTextInFlowyText(
|
|
|
|
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
|
|
|
);
|
|
|
|
await tester.tapButton(embedLinkButton);
|
2023-07-01 23:13:09 +08:00
|
|
|
|
|
|
|
final imageUrlTextField = find.descendant(
|
2024-01-20 23:16:18 +08:00
|
|
|
of: find.byType(EmbedImageUrlWidget),
|
2023-07-01 23:13:09 +08:00
|
|
|
matching: find.byType(TextField),
|
|
|
|
);
|
2024-03-06 16:31:30 +01:00
|
|
|
await tester.enterText(imageUrlTextField, imageUrl);
|
2024-01-20 23:16:18 +08:00
|
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tapButton(
|
|
|
|
find.descendant(
|
|
|
|
of: find.byType(EmbedImageUrlWidget),
|
|
|
|
matching: find.findTextInFlowyText(
|
|
|
|
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
|
|
|
),
|
|
|
|
),
|
2023-07-01 23:13:09 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-04-30 14:09:08 +02:00
|
|
|
Future<void> tapOnRemoveCover() async =>
|
|
|
|
tester.tapButton(find.byType(DeleteCoverButton));
|
2023-06-27 15:17:51 +08:00
|
|
|
|
|
|
|
/// A cover must be present in the document to function properly since this
|
|
|
|
/// catches all cover types collectively
|
|
|
|
Future<void> hoverOnCover() async {
|
|
|
|
final cover = find.byType(DocumentCover);
|
|
|
|
await tester.startGesture(
|
|
|
|
tester.getCenter(cover),
|
|
|
|
kind: PointerDeviceKind.mouse,
|
2023-06-15 16:33:44 +08:00
|
|
|
);
|
2023-06-27 15:17:51 +08:00
|
|
|
await tester.pumpAndSettle();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> dismissCoverPicker() async {
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
|
|
|
await tester.pumpAndSettle();
|
2023-06-15 16:33:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// trigger the slash command (selection menu)
|
|
|
|
Future<void> showSlashMenu() async {
|
|
|
|
await tester.ime.insertCharacter('/');
|
|
|
|
}
|
|
|
|
|
2023-11-03 21:30:24 +01:00
|
|
|
/// trigger the mention (@) command
|
2023-06-29 07:04:24 +05:00
|
|
|
Future<void> showAtMenu() async {
|
|
|
|
await tester.ime.insertCharacter('@');
|
|
|
|
}
|
|
|
|
|
2023-06-15 16:33:44 +08:00
|
|
|
/// Tap the slash menu item with [name]
|
|
|
|
///
|
|
|
|
/// Must call [showSlashMenu] first.
|
2024-08-09 21:50:47 +08:00
|
|
|
Future<void> tapSlashMenuItemWithName(
|
|
|
|
String name, {
|
|
|
|
double offset = 200,
|
|
|
|
}) async {
|
2024-07-29 14:30:11 +08:00
|
|
|
final slashMenu = find
|
|
|
|
.ancestor(
|
|
|
|
of: find.byType(SelectionMenuItemWidget),
|
|
|
|
matching: find.byWidgetPredicate(
|
|
|
|
(widget) => widget is Scrollable,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.first;
|
2023-06-15 16:33:44 +08:00
|
|
|
final slashMenuItem = find.text(name, findRichText: true);
|
2024-08-09 21:50:47 +08:00
|
|
|
await tester.scrollUntilVisible(
|
|
|
|
slashMenuItem,
|
|
|
|
offset,
|
|
|
|
scrollable: slashMenu,
|
|
|
|
duration: const Duration(milliseconds: 250),
|
|
|
|
);
|
|
|
|
assert(slashMenuItem.hasFound);
|
2023-06-15 16:33:44 +08:00
|
|
|
await tester.tapButton(slashMenuItem);
|
|
|
|
}
|
2023-06-29 07:04:24 +05:00
|
|
|
|
|
|
|
/// Tap the at menu item with [name]
|
|
|
|
///
|
|
|
|
/// Must call [showAtMenu] first.
|
|
|
|
Future<void> tapAtMenuItemWithName(String name) async {
|
|
|
|
final atMenuItem = find.descendant(
|
2023-10-02 09:12:24 +02:00
|
|
|
of: find.byType(InlineActionsHandler),
|
2023-06-29 07:04:24 +05:00
|
|
|
matching: find.text(name, findRichText: true),
|
|
|
|
);
|
|
|
|
await tester.tapButton(atMenuItem);
|
|
|
|
}
|
2023-07-09 10:03:22 +07:00
|
|
|
|
|
|
|
/// Update the editor's selection
|
|
|
|
Future<void> updateSelection(Selection selection) async {
|
|
|
|
final editorState = getCurrentEditorState();
|
2024-01-29 10:26:45 +08:00
|
|
|
unawaited(
|
|
|
|
editorState.updateSelectionWithReason(
|
|
|
|
selection,
|
|
|
|
reason: SelectionUpdateReason.uiEvent,
|
|
|
|
),
|
2023-07-09 10:03:22 +07:00
|
|
|
);
|
|
|
|
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
|
|
|
}
|
2023-09-26 14:37:10 +08:00
|
|
|
|
|
|
|
/// hover and click on the + button beside the block component.
|
|
|
|
Future<void> hoverAndClickOptionAddButton(
|
|
|
|
Path path,
|
|
|
|
bool withModifiedKey, // alt on windows or linux, option on macos
|
|
|
|
) async {
|
|
|
|
final optionAddButton = find.byWidgetPredicate(
|
|
|
|
(widget) =>
|
|
|
|
widget is BlockComponentActionWrapper &&
|
|
|
|
widget.node.path.equals(path),
|
|
|
|
);
|
|
|
|
await tester.hoverOnWidget(
|
|
|
|
optionAddButton,
|
|
|
|
onHover: () async {
|
|
|
|
if (withModifiedKey) {
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
|
|
|
}
|
|
|
|
await tester.tapButton(
|
|
|
|
find.byWidgetPredicate(
|
|
|
|
(widget) =>
|
|
|
|
widget is BlockAddButton &&
|
|
|
|
widget.blockComponentContext.node.path.equals(path),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
if (withModifiedKey) {
|
|
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2024-02-20 08:22:06 +05:30
|
|
|
|
|
|
|
/// hover and click on the option menu button beside the block component.
|
|
|
|
Future<void> hoverAndClickOptionMenuButton(Path path) async {
|
|
|
|
final optionMenuButton = find.byWidgetPredicate(
|
|
|
|
(widget) =>
|
|
|
|
widget is BlockComponentActionWrapper &&
|
|
|
|
widget.node.path.equals(path),
|
|
|
|
);
|
|
|
|
await tester.hoverOnWidget(
|
|
|
|
optionMenuButton,
|
|
|
|
onHover: () async {
|
|
|
|
await tester.tapButton(
|
|
|
|
find.byWidgetPredicate(
|
|
|
|
(widget) =>
|
|
|
|
widget is BlockOptionButton &&
|
|
|
|
widget.blockComponentContext.node.path.equals(path),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2024-09-22 09:35:11 +08:00
|
|
|
|
|
|
|
/// Drag block
|
|
|
|
///
|
|
|
|
/// [offset] is the offset to move the block.
|
|
|
|
///
|
|
|
|
/// [path] is the path of the block to move.
|
|
|
|
Future<void> dragBlock(
|
|
|
|
Path path,
|
|
|
|
Offset offset,
|
|
|
|
) async {
|
|
|
|
final dragToMoveAction = find.byWidgetPredicate(
|
|
|
|
(widget) =>
|
|
|
|
widget is DraggableOptionButton &&
|
|
|
|
widget.blockComponentContext.node.path.equals(path),
|
|
|
|
);
|
|
|
|
|
|
|
|
await tester.hoverOnWidget(
|
|
|
|
dragToMoveAction,
|
|
|
|
onHover: () async {
|
|
|
|
final dragToMoveTooltip = find.findFlowyTooltip(
|
|
|
|
LocaleKeys.blockActions_dragTooltip.tr(),
|
|
|
|
);
|
|
|
|
await tester.pumpUntilFound(dragToMoveTooltip);
|
|
|
|
final location = tester.getCenter(dragToMoveAction);
|
|
|
|
final gesture = await tester.startGesture(
|
|
|
|
location,
|
|
|
|
pointer: 7,
|
|
|
|
);
|
|
|
|
await tester.pump();
|
|
|
|
|
|
|
|
// divide the steps to small move to avoid the drag area not found error
|
|
|
|
const steps = 5;
|
|
|
|
final stepOffset = Offset(offset.dx / steps, offset.dy / steps);
|
|
|
|
|
|
|
|
for (var i = 0; i < steps; i++) {
|
|
|
|
await gesture.moveBy(stepOffset);
|
|
|
|
await tester.pump(Durations.short1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if the drag to move action is dragging
|
|
|
|
expect(
|
|
|
|
isDraggingAppFlowyEditorBlock.value,
|
|
|
|
isTrue,
|
|
|
|
);
|
|
|
|
|
|
|
|
await gesture.up();
|
|
|
|
await tester.pump();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
await tester.pumpAndSettle(Durations.short1);
|
|
|
|
}
|
2024-09-25 22:44:59 +08:00
|
|
|
|
|
|
|
Finder findDocumentTitle(String title) {
|
|
|
|
return find.descendant(
|
|
|
|
of: find.byType(CoverTitle),
|
|
|
|
matching: find.byWidgetPredicate(
|
|
|
|
(widget) {
|
|
|
|
if (widget is! TextField) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (widget.controller?.text == title) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (title.isEmpty) {
|
|
|
|
return widget.controller?.text.isEmpty ?? false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2023-06-15 16:33:44 +08:00
|
|
|
}
|