mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-11-04 03:54:44 +00:00 
			
		
		
		
	chore: add integration test
This commit is contained in:
		
							parent
							
								
									89647ae683
								
							
						
					
					
						commit
						72fe1d7c47
					
				@ -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<String> sourceIds,
 | 
			
		||||
    List<AiWriterRecord> 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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<void> 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));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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<AuthService>();
 | 
			
		||||
                  getIt.unregister<AIRepository>();
 | 
			
		||||
 | 
			
		||||
                  getIt.registerFactory<AuthService>(
 | 
			
		||||
                    () => AppFlowyCloudMockAuthService(email: email),
 | 
			
		||||
                  );
 | 
			
		||||
                  getIt.registerFactory<AIRepository>(
 | 
			
		||||
                    aiRepositoryBuilder ?? () => MockAIRepository(),
 | 
			
		||||
                  );
 | 
			
		||||
                case AuthenticatorType.appflowyCloudDevelop:
 | 
			
		||||
                  if (integrationMode().isDevelop) {
 | 
			
		||||
                    await useAppFlowyCloudDevelop("http://localhost");
 | 
			
		||||
                  } else {
 | 
			
		||||
                    await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl);
 | 
			
		||||
                  }
 | 
			
		||||
                  getIt.unregister<AuthService>();
 | 
			
		||||
                  getIt.unregister<AIRepository>();
 | 
			
		||||
 | 
			
		||||
                  getIt.registerFactory<AuthService>(
 | 
			
		||||
                    () => AppFlowyCloudMockAuthService(email: email),
 | 
			
		||||
                  );
 | 
			
		||||
                  getIt.registerFactory<AIRepository>(
 | 
			
		||||
                    aiRepositoryBuilder ?? () => MockAIRepository(),
 | 
			
		||||
                  );
 | 
			
		||||
                  break;
 | 
			
		||||
                default:
 | 
			
		||||
                  throw Exception("not supported");
 | 
			
		||||
              }
 | 
			
		||||
@ -275,10 +304,6 @@ extension AppFlowyFinderTestBase on CommonFinders {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<void> useTestSelfHostedAppFlowyCloud() async {
 | 
			
		||||
  await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<String> mockApplicationDataStorage({
 | 
			
		||||
  // use to append after the application data directory
 | 
			
		||||
  String? pathExtension,
 | 
			
		||||
 | 
			
		||||
@ -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, Map<String, List<String>>>{
 | 
			
		||||
  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<String> sourceIds,
 | 
			
		||||
    List<AiWriterRecord> 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<String> sourceIds = const [],
 | 
			
		||||
    List<AiWriterRecord> history = const [],
 | 
			
		||||
    required CompletionTypePB completionType,
 | 
			
		||||
    required Future<void> Function() onStart,
 | 
			
		||||
    required Future<void> Function(String text) processMessage,
 | 
			
		||||
    required Future<void> Function(String text) processAssistMessage,
 | 
			
		||||
    required Future<void> 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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -21,7 +21,7 @@ enum LocalAIStreamingState {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
abstract class AIRepository {
 | 
			
		||||
  Future<void> streamCompletion({
 | 
			
		||||
  Future<(String, CompletionStream)?> streamCompletion({
 | 
			
		||||
    String? objectId,
 | 
			
		||||
    required String text,
 | 
			
		||||
    PredefinedFormat? format,
 | 
			
		||||
 | 
			
		||||
@ -167,11 +167,16 @@ Future<void> useBaseWebDomain(String? url) async {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
 | 
			
		||||
Future<void> useSelfHostedAppFlowyCloud(String url) async {
 | 
			
		||||
  await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);
 | 
			
		||||
  await _setAppFlowyCloudUrl(url);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<void> useAppFlowyCloudDevelop(String url) async {
 | 
			
		||||
  await _setAuthenticatorType(AuthenticatorType.appflowyCloudDevelop);
 | 
			
		||||
  await _setAppFlowyCloudUrl(url);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<void> useAppFlowyBetaCloudWithURL(
 | 
			
		||||
  String url,
 | 
			
		||||
  AuthenticatorType authenticatorType,
 | 
			
		||||
 | 
			
		||||
@ -87,7 +87,7 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
 | 
			
		||||
              case SelfHostUrlBottomSheetType.shareDomain:
 | 
			
		||||
                await useBaseWebDomain(url);
 | 
			
		||||
              case SelfHostUrlBottomSheetType.cloudURL:
 | 
			
		||||
                await useSelfHostedAppFlowyCloudWithURL(url);
 | 
			
		||||
                await useSelfHostedAppFlowyCloud(url);
 | 
			
		||||
            }
 | 
			
		||||
            await runAppFlowy();
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
@ -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<AiWriterState> {
 | 
			
		||||
    this.onCreateNode,
 | 
			
		||||
    this.onRemoveNode,
 | 
			
		||||
    this.onAppendToDocument,
 | 
			
		||||
    AppFlowyAIService? aiService,
 | 
			
		||||
  })  : _aiService = aiService ?? AppFlowyAIService(),
 | 
			
		||||
  })  : _aiService = getIt<AIRepository>(),
 | 
			
		||||
        _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<AiWriterState> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final selectionText = await editorState.getMarkdownInSelection(selection);
 | 
			
		||||
    Log.warn('[AI writer] Selection is null');
 | 
			
		||||
 | 
			
		||||
    if (command == AiWriterCommand.userQuestion) {
 | 
			
		||||
      records.add(
 | 
			
		||||
 | 
			
		||||
@ -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<void> _resolveCloudDeps(GetIt getIt) async {
 | 
			
		||||
  final env = await AppFlowyCloudSharedEnv.fromEnv();
 | 
			
		||||
  Log.info("cloud setting: $env");
 | 
			
		||||
  getIt.registerFactory<AppFlowyCloudSharedEnv>(() => env);
 | 
			
		||||
  getIt.registerFactory<AIRepository>(() => AppFlowyAIService());
 | 
			
		||||
 | 
			
		||||
  if (isAppFlowyCloudEnabled) {
 | 
			
		||||
    getIt.registerSingleton(
 | 
			
		||||
 | 
			
		||||
@ -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)),
 | 
			
		||||
 | 
			
		||||
@ -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<AIRepository>()) {
 | 
			
		||||
    getIt.unregister<AIRepository>();
 | 
			
		||||
  }
 | 
			
		||||
  getIt.registerFactory<AIRepository>(() => 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();
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user