Skip to content

Ai writer test #7660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 25, 2025
22 changes: 11 additions & 11 deletions .github/workflows/flutter_ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -12,7 +12,7 @@ import 'uncategorized/uncategorized_test_runner.dart'
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;

Future<void> main() async {
preset_af_cloud_env_test.main();
// preset_af_cloud_env_test.main();

data_migration_test_runner.main();

Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -17,6 +24,7 @@ void main() {
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
aiRepositoryBuilder: () => MockAIRepository(),
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
Expand All @@ -43,5 +51,184 @@ 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.selectAIWriter(AiWriterCommand.fixSpellingAndGrammar);

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.appflowyCloudDevelop,
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.selectAIWriter(AiWriterCommand.userQuestion);

await tester.enterTextInPromptTextField("Explain the concept of TPU");
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
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,
String? objectId,
CompletionTypePB completionType,
PredefinedFormat? format,
List<String> sourceIds,
List<AiWriterRecord> history,
) {
assert(completionType == CompletionTypePB.UserQuestion);
if (history.isEmpty) return false;

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<String>) {
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()}'",
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
}
41 changes: 41 additions & 0 deletions frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 'util.dart';

extension AppFlowyAITest on WidgetTester {
Future<void> selectAIWriter(AiWriterCommand command) async {
await tapButton(find.byType(AiWriterToolbarActionList));
await tapButton(find.text(command.i18n));
await pumpAndSettle();
}

Future<void> selectModel(String modelName) async {
await tapButton(find.byType(SelectModelMenu));
await tapButton(find.text(modelName));
await pumpAndSettle();
}

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(TextField),
);
expect(textField, findsOneWidget, reason: 'ExtendedTextField not found');

final widget = element(textField).widget as TextField;
expect(widget.enabled, isTrue, reason: 'TextField is not enabled');

await tap(textField);

testTextInput.enterText(text);
await pumpAndSettle(const Duration(milliseconds: 300));
}
}
Loading
Loading