From 89647ae68305f5c7d08403ec6598e53b882617a9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 31 Mar 2025 00:15:59 +0800 Subject: [PATCH 01/11] chore: logs for windows --- .../editor_plugins/ai/operations/ai_writer_cubit.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 076046624a..7fc93e1c07 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -277,10 +278,12 @@ class AiWriterCubit extends Cubit { ) async { final node = aiWriterNode; if (node == null) { + Log.warn('[AI writer] Node is null'); return (false, ''); } final selection = node.aiWriterSelection?.normalized; if (selection == null) { + Log.warn('[AI writer]Selection is null'); return (false, ''); } @@ -292,6 +295,7 @@ class AiWriterCubit extends Cubit { } final selectionText = await editorState.getMarkdownInSelection(selection); + Log.warn('[AI writer] Selection is null'); if (command == AiWriterCommand.userQuestion) { records.add( From 72fe1d7c471e5d069fd16156f20cf07512e6bd6d Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 31 Mar 2025 15:33:54 +0800 Subject: [PATCH 02/11] chore: add integration test --- .../document/document_ai_writer_test.dart | 162 ++++++++++++++++++ .../document_copy_and_paste_test.dart | 2 + .../integration_test/shared/ai_test_op.dart | 23 +++ .../integration_test/shared/base.dart | 37 +++- .../integration_test/shared/mock/mock_ai.dart | 112 ++++++++++++ .../lib/ai/service/appflowy_ai_service.dart | 2 +- .../appflowy_flutter/lib/env/cloud_env.dart | 7 +- .../self_host/self_host_bottom_sheet.dart | 2 +- .../ai/operations/ai_writer_cubit.dart | 7 +- .../lib/startup/deps_resolver.dart | 2 + .../settings/appflowy_cloud_urls_bloc.dart | 2 +- .../ai_writer_test/ai_writer_bloc_test.dart | 20 ++- 12 files changed, 358 insertions(+), 20 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart create mode 100644 frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart index f163608ccb..e5548b0d97 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart @@ -1,12 +1,19 @@ +import 'package:appflowy/ai/service/ai_entities.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import '../../../shared/ai_test_op.dart'; import '../../../shared/constants.dart'; +import '../../../shared/mock/mock_ai.dart'; import '../../../shared/util.dart'; void main() { @@ -17,6 +24,7 @@ void main() { (tester) async { await tester.initializeAppFlowy( cloudType: AuthenticatorType.appflowyCloudSelfHost, + aiRepositoryBuilder: () => MockAIRepository(), ); await tester.tapGoogleLoginInButton(); await tester.expectToSeeHomePageWithGetStartedPage(); @@ -43,5 +51,159 @@ void main() { // expect the ai writer block is not in the document expect(find.byType(AiWriterBlockComponent), findsNothing); }); + + testWidgets('Improve writing', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + const pageName = 'Document'; + await tester.createNewPageInSpace( + spaceName: Constants.generalSpaceName, + layout: ViewLayoutPB.Document, + pageName: pageName, + ); + + await tester.editor.tapLineOfEditorAt(0); + + // insert a paragraph + final text = 'I have an apple'; + await tester.editor.tapLineOfEditorAt(0); + await tester.ime.insertText(text); + await tester.editor.updateSelection( + Selection( + start: Position(path: [0]), + end: Position(path: [0], offset: text.length), + ), + ); + + await tester.pumpAndSettle(); + await tester.tapButton(find.byType(ImproveWritingButton)); + + final editorState = tester.editor.getCurrentEditorState(); + final document = editorState.document; + + expect(document.root.children.length, 3); + expect(document.root.children[1].type, ParagraphBlockKeys.type); + expect( + document.root.children[1].delta!.toPlainText(), + 'I have an apple and a banana', + ); + }); + + testWidgets('fix grammar', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + const pageName = 'Document'; + await tester.createNewPageInSpace( + spaceName: Constants.generalSpaceName, + layout: ViewLayoutPB.Document, + pageName: pageName, + ); + + await tester.editor.tapLineOfEditorAt(0); + + // insert a paragraph + final text = 'We didn’t had enough money'; + await tester.editor.tapLineOfEditorAt(0); + await tester.ime.insertText(text); + await tester.editor.updateSelection( + Selection( + start: Position(path: [0]), + end: Position(path: [0], offset: text.length), + ), + ); + + await tester.pumpAndSettle(); + await tester.tapButton(find.byType(AiWriterToolbarActionList)); + await tester.tapButton( + find.text(AiWriterCommand.fixSpellingAndGrammar.i18n), + ); + await tester.pumpAndSettle(); + + final editorState = tester.editor.getCurrentEditorState(); + final document = editorState.document; + + expect(document.root.children.length, 3); + expect(document.root.children[1].type, ParagraphBlockKeys.type); + expect( + document.root.children[1].delta!.toPlainText(), + 'We didn’t have enough money', + ); + }); + + testWidgets('ask ai', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + aiRepositoryBuilder: () => MockAIRepository( + validator: _CompletionHistoryValidator(), + ), + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + const pageName = 'Document'; + await tester.createNewPageInSpace( + spaceName: Constants.generalSpaceName, + layout: ViewLayoutPB.Document, + pageName: pageName, + ); + + await tester.editor.tapLineOfEditorAt(0); + + // insert a paragraph + final text = 'What is TPU?'; + await tester.editor.tapLineOfEditorAt(0); + await tester.ime.insertText(text); + await tester.editor.updateSelection( + Selection( + start: Position(path: [0]), + end: Position(path: [0], offset: text.length), + ), + ); + + await tester.pumpAndSettle(); + await tester.tapButton(find.byType(AiWriterToolbarActionList)); + await tester.tapButton( + find.text(AiWriterCommand.userQuestion.i18n), + ); + await tester.pumpAndSettle(); + + await tester.enterTextInPromptTextField("Explain the concept of TPU"); + + // click enter button + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(Duration(seconds: 10)); + }); }); } + +class _CompletionHistoryValidator extends StreamCompletionValidator { + @override + bool validate( + String text, + String? objectId, + CompletionTypePB completionType, + PredefinedFormat? format, + List sourceIds, + List history, + ) { + assert(completionType == CompletionTypePB.UserQuestion); + assert( + history.length == 1, + "expect history length is 1, but got ${history.length}", + ); + assert( + history[0].content.trim() == "What is TPU?", + "expect history[0].content is 'What is TPU?', but got '${history[0].content.trim()}'", + ); + + return true; + } +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart index c18b42939c..ec61034d40 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; @@ -530,6 +531,7 @@ extension on WidgetTester { (String, Uint8List?)? image, }) async { await initializeAppFlowy(); + await useAppFlowyCloudDevelop("http://localhost"); await tapAnonymousSignInButton(); // create a new document diff --git a/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart new file mode 100644 index 0000000000..6d5b34ba0c --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart @@ -0,0 +1,23 @@ +import 'package:appflowy/ai/ai.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:extended_text_field/extended_text_field.dart'; + +extension AppFlowyAITest on WidgetTester { + Future enterTextInPromptTextField(String text) async { + // Wait for the text field to be visible + await pumpAndSettle(); + + // Find the ExtendedTextField widget + final textField = find.descendant( + of: find.byType(PromptInputTextField), + matching: find.byType(ExtendedTextField), + ); + expect(textField, findsOneWidget, reason: 'ExtendedTextField not found'); + + final widget = element(textField).widget as ExtendedTextField; + expect(widget.enabled, isTrue, reason: 'TextField is not enabled'); + + testTextInput.enterText(text); + await pumpAndSettle(const Duration(milliseconds: 300)); + } +} diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart index 493cb4c1f0..bb0489a926 100644 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:appflowy/ai/service/appflowy_ai_service.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env_test.dart'; import 'package:appflowy/startup/entry_point.dart'; @@ -20,6 +21,8 @@ import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'mock/mock_ai.dart'; + class FlowyTestContext { FlowyTestContext({required this.applicationDataDirectory}); @@ -33,8 +36,9 @@ extension AppFlowyTestBase on WidgetTester { // use to specify the application data directory, if not specified, a temporary directory will be used. String? dataDirectory, Size windowSize = const Size(1600, 1200), - AuthenticatorType? cloudType, String? email, + AuthenticatorType? cloudType, + AIRepository Function()? aiRepositoryBuilder, }) async { if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { // Set the window size @@ -60,6 +64,10 @@ extension AppFlowyTestBase on WidgetTester { rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com"; rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password"; break; + case AuthenticatorType.appflowyCloudDevelop: + rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com"; + rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password"; + break; default: throw Exception("not supported"); } @@ -75,11 +83,32 @@ extension AppFlowyTestBase on WidgetTester { await useLocalServer(); break; case AuthenticatorType.appflowyCloudSelfHost: - await useTestSelfHostedAppFlowyCloud(); + await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl); getIt.unregister(); + getIt.unregister(); + getIt.registerFactory( () => AppFlowyCloudMockAuthService(email: email), ); + getIt.registerFactory( + aiRepositoryBuilder ?? () => MockAIRepository(), + ); + case AuthenticatorType.appflowyCloudDevelop: + if (integrationMode().isDevelop) { + await useAppFlowyCloudDevelop("http://localhost"); + } else { + await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl); + } + getIt.unregister(); + getIt.unregister(); + + getIt.registerFactory( + () => AppFlowyCloudMockAuthService(email: email), + ); + getIt.registerFactory( + aiRepositoryBuilder ?? () => MockAIRepository(), + ); + break; default: throw Exception("not supported"); } @@ -275,10 +304,6 @@ extension AppFlowyFinderTestBase on CommonFinders { } } -Future useTestSelfHostedAppFlowyCloud() async { - await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl); -} - Future mockApplicationDataStorage({ // use to append after the application data directory String? pathExtension, diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart new file mode 100644 index 0000000000..8f6d6757b4 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart @@ -0,0 +1,112 @@ +import 'dart:async'; + +import 'package:appflowy/ai/service/ai_entities.dart'; +import 'package:appflowy/ai/service/appflowy_ai_service.dart'; +import 'package:appflowy/ai/service/error.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart'; +import 'package:mocktail/mocktail.dart'; + +final _mockAiMap = >>{ + CompletionTypePB.ImproveWriting: { + "I have an apple": [ + "I", + "have", + "an", + "apple", + "and", + "a", + "banana", + ], + }, + CompletionTypePB.SpellingAndGrammar: { + "We didn’t had enough money": [ + "We", + "didn’t", + "have", + "enough", + "money", + ], + }, + CompletionTypePB.UserQuestion: { + "Explain the concept of TPU": [ + "TPU", + "is", + "a", + "tensor", + "processing", + "unit", + "that", + "is", + "designed", + "to", + "accelerate", + "machine", + ], + }, +}; + +abstract class StreamCompletionValidator { + bool validate( + String text, + String? objectId, + CompletionTypePB completionType, + PredefinedFormat? format, + List sourceIds, + List history, + ); +} + +class MockCompletionStream extends Mock implements CompletionStream {} + +class MockAIRepository extends Mock implements AppFlowyAIService { + MockAIRepository({this.validator}); + StreamCompletionValidator? validator; + + @override + Future<(String, CompletionStream)?> streamCompletion({ + String? objectId, + required String text, + PredefinedFormat? format, + List sourceIds = const [], + List history = const [], + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) processMessage, + required Future Function(String text) processAssistMessage, + required Future Function() onEnd, + required void Function(AIError error) onError, + required void Function(LocalAIStreamingState state) + onLocalAIStreamingStateChange, + }) async { + if (validator != null) { + if (!validator!.validate( + text, + objectId, + completionType, + format, + sourceIds, + history, + )) { + throw Exception('Invalid completion'); + } + } + final stream = MockCompletionStream(); + unawaited( + Future(() async { + await onStart(); + final lines = _mockAiMap[completionType]?[text.trim()]; + + if (lines == null) { + throw Exception('No mock ai found for $text and $completionType'); + } + + for (final line in lines) { + await processMessage('$line '); + } + await onEnd(); + }), + ); + return ('mock_id', stream); + } +} diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index 39487652f8..b55f09e1d6 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -21,7 +21,7 @@ enum LocalAIStreamingState { } abstract class AIRepository { - Future streamCompletion({ + Future<(String, CompletionStream)?> streamCompletion({ String? objectId, required String text, PredefinedFormat? format, diff --git a/frontend/appflowy_flutter/lib/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart index 15f3ada42e..9e24e929b1 100644 --- a/frontend/appflowy_flutter/lib/env/cloud_env.dart +++ b/frontend/appflowy_flutter/lib/env/cloud_env.dart @@ -167,11 +167,16 @@ Future useBaseWebDomain(String? url) async { ); } -Future useSelfHostedAppFlowyCloudWithURL(String url) async { +Future useSelfHostedAppFlowyCloud(String url) async { await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost); await _setAppFlowyCloudUrl(url); } +Future useAppFlowyCloudDevelop(String url) async { + await _setAuthenticatorType(AuthenticatorType.appflowyCloudDevelop); + await _setAppFlowyCloudUrl(url); +} + Future useAppFlowyBetaCloudWithURL( String url, AuthenticatorType authenticatorType, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart index dd19c2489d..5e19667c68 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart @@ -87,7 +87,7 @@ class _SelfHostUrlBottomSheetState extends State { case SelfHostUrlBottomSheetType.shareDomain: await useBaseWebDomain(url); case SelfHostUrlBottomSheetType.cloudURL: - await useSelfHostedAppFlowyCloudWithURL(url); + await useSelfHostedAppFlowyCloud(url); } await runAppFlowy(); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 7fc93e1c07..e5ec888244 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -23,15 +24,14 @@ class AiWriterCubit extends Cubit { this.onCreateNode, this.onRemoveNode, this.onAppendToDocument, - AppFlowyAIService? aiService, - }) : _aiService = aiService ?? AppFlowyAIService(), + }) : _aiService = getIt(), _textRobot = MarkdownTextRobot(editorState: editorState), selectedSourcesNotifier = ValueNotifier([documentId]), super(IdleAiWriterState()); final String documentId; final EditorState editorState; - final AppFlowyAIService _aiService; + final AIRepository _aiService; final MarkdownTextRobot _textRobot; final void Function()? onCreateNode; final void Function()? onRemoveNode; @@ -295,7 +295,6 @@ class AiWriterCubit extends Cubit { } final selectionText = await editorState.getMarkdownInSelection(selection); - Log.warn('[AI writer] Selection is null'); if (command == AiWriterCommand.userQuestion) { records.add( diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 621ba988cf..fffec2fd0b 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/ai/service/appflowy_ai_service.dart'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/env/cloud_env.dart'; @@ -59,6 +60,7 @@ Future _resolveCloudDeps(GetIt getIt) async { final env = await AppFlowyCloudSharedEnv.fromEnv(); Log.info("cloud setting: $env"); getIt.registerFactory(() => env); + getIt.registerFactory(() => AppFlowyAIService()); if (isAppFlowyCloudEnabled) { getIt.registerSingleton( diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart index 5652904180..371fd75583 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart @@ -48,7 +48,7 @@ class AppFlowyCloudURLsBloc await validateUrl(state.updatedServerUrl).fold( (url) async { - await useSelfHostedAppFlowyCloudWithURL(url); + await useSelfHostedAppFlowyCloud(url); isSuccess = true; }, (err) async => emit(state.copyWith(urlError: err)), diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index bcd8b13d39..d1873cbe8b 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -4,6 +4,7 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:bloc_test/bloc_test.dart'; @@ -145,6 +146,13 @@ class _MockErrorRepository extends Mock implements AppFlowyAIService { } } +void registerMockRepository(AppFlowyAIService mock) { + if (getIt.isRegistered()) { + getIt.unregister(); + } + getIt.registerFactory(() => mock); +} + void main() { group('AIWriterCubit:', () { const text1 = '1. Select text to style using the toolbar menu.'; @@ -174,10 +182,10 @@ void main() { ); final editorState = EditorState(document: document) ..selection = selection; + registerMockRepository(_MockAIRepository()); return AiWriterCubit( documentId: '', editorState: editorState, - aiService: _MockAIRepository(), ); }, act: (bloc) => bloc.register( @@ -230,10 +238,10 @@ void main() { ); final editorState = EditorState(document: document) ..selection = selection; + registerMockRepository(_MockErrorRepository()); return AiWriterCubit( documentId: '', editorState: editorState, - aiService: _MockErrorRepository(), ); }, act: (bloc) => bloc.register( @@ -279,10 +287,10 @@ void main() { final editorState = EditorState(document: document) ..selection = selection; final aiNode = editorState.getNodeAtPath([3])!; + registerMockRepository(_MockAIRepository()); final bloc = AiWriterCubit( documentId: '', editorState: editorState, - aiService: _MockAIRepository(), ); bloc.register(aiNode); await blocResponseFuture(); @@ -327,10 +335,10 @@ void main() { final editorState = EditorState(document: document) ..selection = selection; final aiNode = editorState.getNodeAtPath([3])!; + registerMockRepository(_MockAIRepository()); final bloc = AiWriterCubit( documentId: '', editorState: editorState, - aiService: _MockAIRepository(), ); bloc.register(aiNode); await blocResponseFuture(); @@ -366,10 +374,10 @@ void main() { final editorState = EditorState(document: document) ..selection = selection; final aiNode = editorState.getNodeAtPath([3])!; + registerMockRepository(_MockAIRepositoryLess()); final bloc = AiWriterCubit( documentId: '', editorState: editorState, - aiService: _MockAIRepositoryLess(), ); bloc.register(aiNode); await blocResponseFuture(); @@ -403,10 +411,10 @@ void main() { final editorState = EditorState(document: document) ..selection = selection; final aiNode = editorState.getNodeAtPath([3])!; + registerMockRepository(_MockAIRepositoryMore()); final bloc = AiWriterCubit( documentId: '', editorState: editorState, - aiService: _MockAIRepositoryMore(), ); bloc.register(aiNode); await blocResponseFuture(); From 5c99572e9304f8322157f3962cea22bd7b6a41ff Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 31 Mar 2025 21:00:14 +0800 Subject: [PATCH 03/11] chore: rename test --- .../desktop/cloud/set_env.dart | 6 +- .../document_copy_and_paste_test.dart | 2 - .../integration_test/shared/base.dart | 139 ++++++++++-------- 3 files changed, 80 insertions(+), 67 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/set_env.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/set_env.dart index b24c0faf27..df69de7446 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/set_env.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/set_env.dart @@ -8,11 +8,13 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('Empty', () { - testWidgets('set appflowy cloud', (tester) async { + group('Preset cloud env', () { + testWidgets('use self-hosted cloud', (tester) async { await tester.initializeAppFlowy( cloudType: AuthenticatorType.appflowyCloudSelfHost, ); + + await tester.pumpAndSettle(); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart index ec61034d40..c18b42939c 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; @@ -531,7 +530,6 @@ extension on WidgetTester { (String, Uint8List?)? image, }) async { await initializeAppFlowy(); - await useAppFlowyCloudDevelop("http://localhost"); await tapAnonymousSignInButton(); // create a new document diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart index bb0489a926..95bda89113 100644 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -41,7 +41,6 @@ extension AppFlowyTestBase on WidgetTester { AIRepository Function()? aiRepositoryBuilder, }) async { if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - // Set the window size await binding.setSurfaceSize(windowSize); } @@ -54,68 +53,12 @@ extension AppFlowyTestBase on WidgetTester { await FlowyRunner.run( AppFlowyApplication(), IntegrationMode.integrationTest, - rustEnvsBuilder: () { - final rustEnvs = {}; - if (cloudType != null) { - switch (cloudType) { - case AuthenticatorType.local: - break; - case AuthenticatorType.appflowyCloudSelfHost: - rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com"; - rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password"; - break; - case AuthenticatorType.appflowyCloudDevelop: - rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com"; - rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password"; - break; - default: - throw Exception("not supported"); - } - } - return rustEnvs; - }, - didInitGetItCallback: () { - return Future( - () async { - if (cloudType != null) { - switch (cloudType) { - case AuthenticatorType.local: - await useLocalServer(); - break; - case AuthenticatorType.appflowyCloudSelfHost: - await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl); - getIt.unregister(); - getIt.unregister(); - - getIt.registerFactory( - () => AppFlowyCloudMockAuthService(email: email), - ); - getIt.registerFactory( - aiRepositoryBuilder ?? () => MockAIRepository(), - ); - case AuthenticatorType.appflowyCloudDevelop: - if (integrationMode().isDevelop) { - await useAppFlowyCloudDevelop("http://localhost"); - } else { - await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl); - } - getIt.unregister(); - getIt.unregister(); - - getIt.registerFactory( - () => AppFlowyCloudMockAuthService(email: email), - ); - getIt.registerFactory( - aiRepositoryBuilder ?? () => MockAIRepository(), - ); - break; - default: - throw Exception("not supported"); - } - } - }, - ); - }, + rustEnvsBuilder: () => _buildRustEnvs(cloudType), + didInitGetItCallback: () => _initializeCloudServices( + cloudType: cloudType, + email: email, + aiRepositoryBuilder: aiRepositoryBuilder, + ), ); await waitUntilSignInPageShow(); @@ -124,6 +67,76 @@ extension AppFlowyTestBase on WidgetTester { ); } + Map _buildRustEnvs(AuthenticatorType? cloudType) { + final rustEnvs = {}; + if (cloudType != null) { + switch (cloudType) { + case AuthenticatorType.local: + break; + case AuthenticatorType.appflowyCloudSelfHost: + case AuthenticatorType.appflowyCloudDevelop: + rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com"; + rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password"; + break; + default: + throw Exception("Unsupported cloud type: $cloudType"); + } + } + return rustEnvs; + } + + Future _initializeCloudServices({ + required AuthenticatorType? cloudType, + String? email, + AIRepository Function()? aiRepositoryBuilder, + }) async { + if (cloudType == null) return; + + switch (cloudType) { + case AuthenticatorType.local: + await useLocalServer(); + break; + case AuthenticatorType.appflowyCloudSelfHost: + await _setupAppFlowyCloud( + useLocal: false, + email: email, + aiRepositoryBuilder: aiRepositoryBuilder, + ); + break; + case AuthenticatorType.appflowyCloudDevelop: + await _setupAppFlowyCloud( + useLocal: integrationMode().isDevelop, + email: email, + aiRepositoryBuilder: aiRepositoryBuilder, + ); + break; + default: + throw Exception("Unsupported cloud type: $cloudType"); + } + } + + Future _setupAppFlowyCloud({ + required bool useLocal, + String? email, + AIRepository Function()? aiRepositoryBuilder, + }) async { + if (useLocal) { + await useAppFlowyCloudDevelop("http://localhost"); + } else { + await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl); + } + + getIt.unregister(); + getIt.unregister(); + + getIt.registerFactory( + () => AppFlowyCloudMockAuthService(email: email), + ); + getIt.registerFactory( + aiRepositoryBuilder ?? () => MockAIRepository(), + ); + } + void mockHotKeyManagerHandlers() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(const MethodChannel('hotkey_manager'), From 73e3cbdc185959fa5d685ed2ca3fc3f555954746 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 31 Mar 2025 21:53:56 +0800 Subject: [PATCH 04/11] chore: remove env test --- .../integration_test/desktop/cloud/cloud_runner.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart index a8c05d5f80..96ea39e572 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart @@ -2,7 +2,7 @@ import 'data_migration/data_migration_test_runner.dart' as data_migration_test_runner; import 'database/database_test_runner.dart' as database_test_runner; import 'document/document_test_runner.dart' as document_test_runner; -import 'set_env.dart' as preset_af_cloud_env_test; +// import 'set_env.dart' as preset_af_cloud_env_test; import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test; import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test; import 'sidebar/sidebar_rename_untitled_test.dart' @@ -12,7 +12,7 @@ import 'uncategorized/uncategorized_test_runner.dart' import 'workspace/workspace_test_runner.dart' as workspace_test_runner; Future main() async { - preset_af_cloud_env_test.main(); + // preset_af_cloud_env_test.main(); data_migration_test_runner.main(); From 01dbcf7697e87e14614d12d99715b5a2332aaa99 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 31 Mar 2025 22:56:15 +0800 Subject: [PATCH 05/11] chore: don't try to force update selection --- .../editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart index 6b5f27e028..5747d4f7e0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart @@ -57,6 +57,7 @@ class _AiWriterScrollWrapperState extends State { @override void dispose() { aiWriterCubit.close(); + throttler.dispose(); super.dispose(); } From 0d0c6fff32a364a08152c280283c6b44f77b27ee Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 1 Apr 2025 13:32:22 +0800 Subject: [PATCH 06/11] chore: update test --- .../document/document_ai_writer_test.dart | 69 +++++++++++++------ .../integration_test/shared/ai_test_op.dart | 18 +++++ .../integration_test/shared/base.dart | 1 + .../integration_test/shared/mock/mock_ai.dart | 16 +++++ 4 files changed, 82 insertions(+), 22 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart index e5548b0d97..6b64f3f8af 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart @@ -121,11 +121,7 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tapButton(find.byType(AiWriterToolbarActionList)); - await tester.tapButton( - find.text(AiWriterCommand.fixSpellingAndGrammar.i18n), - ); - await tester.pumpAndSettle(); + await tester.selectAIWriter(AiWriterCommand.fixSpellingAndGrammar); final editorState = tester.editor.getCurrentEditorState(); final document = editorState.document; @@ -169,22 +165,38 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tapButton(find.byType(AiWriterToolbarActionList)); - await tester.tapButton( - find.text(AiWriterCommand.userQuestion.i18n), - ); - await tester.pumpAndSettle(); + await tester.selectAIWriter(AiWriterCommand.userQuestion); await tester.enterTextInPromptTextField("Explain the concept of TPU"); - - // click enter button await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(Duration(seconds: 10)); + await tester.pumpAndSettle(); + + await tester.selectModel("GPT-4o-mini"); + + await tester.enterTextInPromptTextField("How about GPU?"); + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); }); }); } class _CompletionHistoryValidator extends StreamCompletionValidator { + static const _expectedResponses = { + 1: "What is TPU?", + 3: [ + "What is TPU?", + "Explain the concept of TPU", + "TPU is a tensor processing unit that is designed to accelerate machine", + ], + 5: [ + "What is TPU?", + "Explain the concept of TPU", + "TPU is a tensor processing unit that is designed to accelerate machine", + "How about GPU?", + "GPU is a graphics processing unit that is designed to accelerate machine learning tasks.", + ], + }; + @override bool validate( String text, @@ -195,15 +207,28 @@ class _CompletionHistoryValidator extends StreamCompletionValidator { List history, ) { assert(completionType == CompletionTypePB.UserQuestion); - assert( - history.length == 1, - "expect history length is 1, but got ${history.length}", - ); - assert( - history[0].content.trim() == "What is TPU?", - "expect history[0].content is 'What is TPU?', but got '${history[0].content.trim()}'", - ); + if (history.isEmpty) return false; - return true; + final expectedMessages = _expectedResponses[history.length]; + if (expectedMessages == null) return false; + + if (expectedMessages is String) { + _assertMessage(history[0].content, expectedMessages); + return true; + } else if (expectedMessages is List) { + for (var i = 0; i < expectedMessages.length; i++) { + _assertMessage(history[i].content, expectedMessages[i]); + } + return true; + } + + return false; + } + + void _assertMessage(String actual, String expected) { + assert( + actual.trim() == expected, + "expected '$expected', but got '${actual.trim()}'", + ); } } diff --git a/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart index 6d5b34ba0c..e92a92b727 100644 --- a/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart @@ -1,8 +1,24 @@ import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:extended_text_field/extended_text_field.dart'; +import 'util.dart'; + extension AppFlowyAITest on WidgetTester { + Future selectAIWriter(AiWriterCommand command) async { + await tapButton(find.byType(AiWriterToolbarActionList)); + await tapButton(find.text(command.i18n)); + await pumpAndSettle(); + } + + Future selectModel(String modelName) async { + await tapButton(find.byType(SelectModelMenu)); + await tapButton(find.text(modelName)); + await pumpAndSettle(); + } + Future enterTextInPromptTextField(String text) async { // Wait for the text field to be visible await pumpAndSettle(); @@ -17,6 +33,8 @@ extension AppFlowyAITest on WidgetTester { final widget = element(textField).widget as ExtendedTextField; expect(widget.enabled, isTrue, reason: 'TextField is not enabled'); + await tap(textField); + testTextInput.enterText(text); await pumpAndSettle(const Duration(milliseconds: 300)); } diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart index 95bda89113..aadcc1d707 100644 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -43,6 +43,7 @@ extension AppFlowyTestBase on WidgetTester { if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { await binding.setSurfaceSize(windowSize); } + cloudType = AuthenticatorType.appflowyCloudDevelop; mockHotKeyManagerHandlers(); final applicationDataDirectory = dataDirectory ?? diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart index 8f6d6757b4..cd4ecb931f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart +++ b/frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart @@ -43,6 +43,22 @@ final _mockAiMap = >>{ "accelerate", "machine", ], + "How about GPU?": [ + "GPU", + "is", + "a", + "graphics", + "processing", + "unit", + "that", + "is", + "designed", + "to", + "accelerate", + "machine", + "learning", + "tasks", + ], }, }; From 13d8b1ed37405abf86259ef52ebf7e9b32ef0717 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 2 Apr 2025 18:03:03 +0800 Subject: [PATCH 07/11] chore: fix test --- frontend/appflowy_flutter/integration_test/shared/base.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart index aadcc1d707..cd28bb0d9f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -43,7 +43,7 @@ extension AppFlowyTestBase on WidgetTester { if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { await binding.setSurfaceSize(windowSize); } - cloudType = AuthenticatorType.appflowyCloudDevelop; + //cloudType = AuthenticatorType.appflowyCloudDevelop; mockHotKeyManagerHandlers(); final applicationDataDirectory = dataDirectory ?? From 6da7c17a9ef3aba8b0b60866bd4d129d3116d117 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 2 Apr 2025 22:37:31 +0800 Subject: [PATCH 08/11] chore: upgrade cloud version --- .github/workflows/flutter_ci.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index 1fc1b0e052..aa1ee4146d 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -28,7 +28,7 @@ env: FLUTTER_VERSION: "3.27.4" RUST_TOOLCHAIN: "1.81.0" CARGO_MAKE_VERSION: "0.37.18" - CLOUD_VERSION: 0.6.54-amd64 + CLOUD_VERSION: 0.9.37-amd64 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -40,7 +40,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest] + os: [ ubuntu-latest ] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -74,7 +74,7 @@ jobs: strategy: fail-fast: true matrix: - os: [windows-latest] + os: [ windows-latest ] include: - os: windows-latest flutter_profile: development-windows-x86 @@ -101,7 +101,7 @@ jobs: strategy: fail-fast: true matrix: - os: [macos-latest] + os: [ macos-latest ] include: - os: macos-latest flutter_profile: development-mac-x86_64 @@ -123,12 +123,12 @@ jobs: flutter_profile: ${{ matrix.flutter_profile }} unit_test: - needs: [prepare-linux] + needs: [ prepare-linux ] if: github.event.pull_request.draft != true strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ ubuntu-latest ] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -217,11 +217,11 @@ jobs: shell: bash cloud_integration_test: - needs: [prepare-linux] + needs: [ prepare-linux ] strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ ubuntu-latest ] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -340,13 +340,13 @@ jobs: shell: bash integration_test: - needs: [prepare-linux] + needs: [ prepare-linux ] if: github.event.pull_request.draft != true strategy: fail-fast: false matrix: - os: [ubuntu-latest] - test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9] + os: [ ubuntu-latest ] + test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] include: - os: ubuntu-latest target: "x86_64-unknown-linux-gnu" From 14fc72bac6d0bec582c50e92b8be152ae4d0c7cd Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 24 Apr 2025 23:53:43 +0800 Subject: [PATCH 09/11] fix: test --- .../appflowy_flutter/integration_test/shared/ai_test_op.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart index e92a92b727..ad96a45a12 100644 --- a/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart @@ -1,6 +1,7 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:extended_text_field/extended_text_field.dart'; @@ -26,11 +27,11 @@ extension AppFlowyAITest on WidgetTester { // Find the ExtendedTextField widget final textField = find.descendant( of: find.byType(PromptInputTextField), - matching: find.byType(ExtendedTextField), + matching: find.byType(TextField), ); expect(textField, findsOneWidget, reason: 'ExtendedTextField not found'); - final widget = element(textField).widget as ExtendedTextField; + final widget = element(textField).widget as TextField; expect(widget.enabled, isTrue, reason: 'TextField is not enabled'); await tap(textField); From a673b12514ff30cacfb56f1cf0bc838b51088820 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Apr 2025 00:16:04 +0800 Subject: [PATCH 10/11] fix: test --- .../appflowy_flutter/integration_test/shared/ai_test_op.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart index ad96a45a12..e6d8c00275 100644 --- a/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart @@ -3,7 +3,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writ import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:extended_text_field/extended_text_field.dart'; import 'util.dart'; From 4d2380a954b9fa820aa4fd518b1f7bb5ed762712 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Apr 2025 10:35:09 +0800 Subject: [PATCH 11/11] fix: test --- .../desktop/cloud/document/document_ai_writer_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart index 6b64f3f8af..4dcbc317ae 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart @@ -136,7 +136,7 @@ void main() { testWidgets('ask ai', (tester) async { await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, + cloudType: AuthenticatorType.appflowyCloudDevelop, aiRepositoryBuilder: () => MockAIRepository( validator: _CompletionHistoryValidator(), ), @@ -171,7 +171,7 @@ void main() { await tester.simulateKeyEvent(LogicalKeyboardKey.enter); await tester.pumpAndSettle(); - await tester.selectModel("GPT-4o-mini"); + // await tester.selectModel("GPT-4o-mini"); await tester.enterTextInPromptTextField("How about GPU?"); await tester.simulateKeyEvent(LogicalKeyboardKey.enter);