fix: ingore keyup event in cover title (#6468)

* fix: ingore keyup event in cover title

* feat: add text field with line metric

* chore: refactor test strcuture

* test: add arrow down key test
This commit is contained in:
Lucas 2024-10-04 14:00:10 +08:00 committed by GitHub
parent caa882dc37
commit 153416604d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 199 additions and 182 deletions

View File

@ -1,28 +1,18 @@
import 'document/document_delete_block_test.dart' as document_delete_block_test;
import 'document/document_drag_block_test.dart' as document_drag_block_test;
import 'document/document_option_actions_test.dart'
as document_option_actions_test;
import 'document/document_test_runner.dart' as document_test_runner;
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'uncategorized/anon_user_continue_test.dart' as anon_user_continue_test;
import 'uncategorized/appflowy_cloud_auth_test.dart'
as appflowy_cloud_auth_test;
import 'uncategorized/empty_test.dart' as preset_af_cloud_env_test;
import 'uncategorized/user_setting_sync_test.dart' as user_sync_test;
import 'uncategorized/uncategorized_test_runner.dart'
as uncategorized_test_runner;
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
Future<void> main() async {
preset_af_cloud_env_test.main();
appflowy_cloud_auth_test.main();
user_sync_test.main();
anon_user_continue_test.main();
// uncategorized
uncategorized_test_runner.main();
// workspace
workspace_test_runner.startTesting();
workspace_test_runner.main();
// document
document_option_actions_test.main();
document_drag_block_test.main();
document_delete_block_test.main();
document_test_runner.main();
// sidebar
sidebar_move_page_test.main();

View File

@ -1,60 +0,0 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document delete block: ', () {
testWidgets('hover on the block and delete it', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before delete
final path = [1];
final beforeDeletedBlock = tester.editor.getNodeAtPath(path);
// hover on the block and delete it
final optionButton = find.byWidgetPredicate(
(widget) =>
widget is DraggableOptionButton &&
widget.blockComponentContext.node.path.equals(path),
);
await tester.hoverOnWidget(
optionButton,
onHover: () async {
// click the delete button
await tester.tapButton(optionButton);
},
);
await tester.pumpAndSettle(Durations.short1);
// click the delete button
final deleteButton =
find.findTextInFlowyText(LocaleKeys.button_delete.tr());
await tester.tapButton(deleteButton);
// wait for the deletion
await tester.pumpAndSettle(Durations.short1);
// check if the block is deleted
final afterDeletedBlock = tester.editor.getNodeAtPath([1]);
expect(afterDeletedBlock.id, isNot(equals(beforeDeletedBlock.id)));
});
});
}

View File

@ -1,101 +0,0 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document option actions:', () {
testWidgets('drag block to the top', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before move
final beforeMoveBlock = tester.editor.getNodeAtPath([1]);
// move the desktop guide to the top, above the getting started
await tester.editor.dragBlock(
[1],
const Offset(20, -80),
);
// wait for the move animation to complete
await tester.pumpAndSettle(Durations.short1);
// check if the block is moved to the top
final afterMoveBlock = tester.editor.getNodeAtPath([0]);
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
});
testWidgets('drag block to other block\'s child', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before move
final beforeMoveBlock = tester.editor.getNodeAtPath([10]);
// move the checkbox to the child of the block at path [9]
await tester.editor.dragBlock(
[10],
const Offset(80, -30),
);
// wait for the move animation to complete
await tester.pumpAndSettle(Durations.short1);
// check if the block is moved to the child of the block at path [9]
final afterMoveBlock = tester.editor.getNodeAtPath([9, 0]);
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
});
testWidgets('copy block link', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// hover and click on the option menu button beside the block component.
await tester.editor.hoverAndClickOptionMenuButton([0]);
// click the copy link to block option
await tester.tap(
find.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),
),
);
await tester.pumpAndSettle(Durations.short1);
// check the clipboard
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text,
matches(
r'^https:\/\/appflowy\.com\/app\/[a-f0-9-]{36}\/[a-f0-9-]{36}\?blockId=[A-Za-z0-9_-]+$',
),
);
});
});
}

View File

@ -1,5 +1,7 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -97,5 +99,48 @@ void main() {
),
);
});
testWidgets('hover on the block and delete it', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before delete
final path = [1];
final beforeDeletedBlock = tester.editor.getNodeAtPath(path);
// hover on the block and delete it
final optionButton = find.byWidgetPredicate(
(widget) =>
widget is DraggableOptionButton &&
widget.blockComponentContext.node.path.equals(path),
);
await tester.hoverOnWidget(
optionButton,
onHover: () async {
// click the delete button
await tester.tapButton(optionButton);
},
);
await tester.pumpAndSettle(Durations.short1);
// click the delete button
final deleteButton =
find.findTextInFlowyText(LocaleKeys.button_delete.tr());
await tester.tapButton(deleteButton);
// wait for the deletion
await tester.pumpAndSettle(Durations.short1);
// check if the block is deleted
final afterDeletedBlock = tester.editor.getNodeAtPath([1]);
expect(afterDeletedBlock.id, isNot(equals(beforeDeletedBlock.id)));
});
});
}

View File

@ -0,0 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'document_option_actions_test.dart' as document_option_actions_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
document_option_actions_test.main();
}

View File

@ -0,0 +1,11 @@
import 'anon_user_continue_test.dart' as anon_user_continue_test;
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'empty_test.dart' as preset_af_cloud_env_test;
import 'user_setting_sync_test.dart' as user_sync_test;
void main() async {
preset_af_cloud_env_test.main();
appflowy_cloud_auth_test.main();
user_sync_test.main();
anon_user_continue_test.main();
}

View File

@ -5,7 +5,7 @@ import 'collaborative_workspace_test.dart' as collaborative_workspace_test;
import 'share_menu_test.dart' as share_menu_test;
import 'workspace_settings_test.dart' as workspace_settings_test;
void startTesting() {
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
workspace_settings_test.main();

View File

@ -220,5 +220,33 @@ void main() {
final newTitle = tester.editor.findDocumentTitle(_testDocumentName);
expect(newTitle, findsOneWidget);
});
testWidgets('press arrow down key in title, check if the cursor flashes',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.enterText(title, _testDocumentName);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
const inputText = 'Hello World';
await tester.ime.insertText(inputText);
await tester.tapButton(
tester.editor.findDocumentTitle(_testDocumentName),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown);
final editorState = tester.editor.getCurrentEditorState();
expect(
editorState.selection,
Selection.collapsed(
Position(path: [0], offset: inputText.length),
),
);
});
});
}

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/log.dart';
@ -52,6 +53,7 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
late final editorContext = context.read<SharedEditorContext>();
late final editorState = context.read<EditorState>();
bool isTitleFocused = false;
int lineCount = 1;
@override
void initState() {
@ -111,11 +113,11 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
DefaultAppearanceSettings.getDefaultSelectionColor(context),
),
),
child: TextField(
child: TextFieldWithMetricLines(
controller: titleTextController,
focusNode: titleFocusNode,
maxLines: null,
style: fontStyle,
onLineCountChange: (count) => lineCount = count,
decoration: InputDecoration(
border: InputBorder.none,
hintText: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
@ -175,6 +177,10 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
}
KeyEventResult _onKeyEvent(FocusNode focusNode, KeyEvent event) {
if (event is KeyUpEvent) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.enter) {
// if enter is pressed, jump the first line of editor.
_createNewLine();
@ -218,7 +224,8 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
final text = titleTextController.text;
// if the cursor is not at the end of the text, ignore the event
if (!selection.isCollapsed || text.length != selection.extentOffset) {
if (lineCount != 1 &&
(!selection.isCollapsed || text.length != selection.extentOffset)) {
return KeyEventResult.ignored;
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
class TextFieldWithMetricLines extends StatefulWidget {
const TextFieldWithMetricLines({
super.key,
this.controller,
this.focusNode,
this.maxLines,
this.style,
this.decoration,
this.onLineCountChange,
});
final TextEditingController? controller;
final FocusNode? focusNode;
final int? maxLines;
final TextStyle? style;
final InputDecoration? decoration;
final void Function(int count)? onLineCountChange;
@override
State<TextFieldWithMetricLines> createState() =>
_TextFieldWithMetricLinesState();
}
class _TextFieldWithMetricLinesState extends State<TextFieldWithMetricLines> {
final key = GlobalKey();
late final controller = widget.controller ?? TextEditingController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
updateDisplayedLineCount(context);
});
}
@override
void dispose() {
if (widget.controller == null) {
// dispose the controller if it was created by this widget
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
key: key,
controller: widget.controller,
focusNode: widget.focusNode,
maxLines: widget.maxLines,
style: widget.style,
decoration: widget.decoration,
onChanged: (_) => updateDisplayedLineCount(context),
);
}
// calculate the number of lines that would be displayed in the text field
void updateDisplayedLineCount(BuildContext context) {
if (widget.onLineCountChange == null) {
return;
}
final renderObject = key.currentContext?.findRenderObject();
if (renderObject == null || renderObject is! RenderBox) {
return;
}
final size = renderObject.size;
final text = controller.buildTextSpan(
context: context,
style: widget.style,
withComposing: false,
);
final textPainter = TextPainter(
text: text,
textDirection: Directionality.of(context),
);
textPainter.layout(minWidth: size.width, maxWidth: size.width);
final lines = textPainter.computeLineMetrics().length;
widget.onLineCountChange?.call(lines);
}
}