Skip to content

Commit c79ac32

Browse files
authored
test: simple ai chat tests (#7901)
* fix: repeated setState in animation list * test: add simple chat integration test * chore: update chat list * test: send messages without default messages * fix: refresh space after moving page
1 parent 7f7657e commit c79ac32

File tree

12 files changed

+360
-31
lines changed

12 files changed

+360
-31
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_animation_list_widget.dart';
2+
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
3+
import 'package:flutter_chat_core/flutter_chat_core.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:integration_test/integration_test.dart';
6+
7+
import '../../shared/ai_test_op.dart';
8+
import '../../shared/util.dart';
9+
10+
void main() {
11+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
12+
13+
group('chat page:', () {
14+
testWidgets('send messages with default messages', (tester) async {
15+
skipAIChatWelcomePage = true;
16+
17+
await tester.initializeAppFlowy();
18+
await tester.tapAnonymousSignInButton();
19+
20+
// create a chat page
21+
final pageName = 'Untitled';
22+
await tester.createNewPageWithNameUnderParent(
23+
name: pageName,
24+
layout: ViewLayoutPB.Chat,
25+
openAfterCreated: false,
26+
);
27+
28+
await tester.pumpAndSettle(const Duration(milliseconds: 300));
29+
30+
final userId = '457037009907617792';
31+
final user = User(id: userId, lastName: 'Lucas');
32+
final aiUserId = '0';
33+
final aiUser = User(id: aiUserId, lastName: 'AI');
34+
35+
await tester.loadDefaultMessages(
36+
[
37+
Message.text(
38+
id: '1746776401',
39+
text: 'How to use Kanban to manage tasks?',
40+
author: user,
41+
createdAt: DateTime.now().add(const Duration(seconds: 1)),
42+
),
43+
Message.text(
44+
id: '1746776401_ans',
45+
text:
46+
'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',
47+
author: aiUser,
48+
createdAt: DateTime.now().add(const Duration(seconds: 2)),
49+
),
50+
Message.text(
51+
id: '1746776402',
52+
text: 'How to use Kanban to manage tasks?',
53+
author: user,
54+
createdAt: DateTime.now().add(const Duration(seconds: 3)),
55+
),
56+
Message.text(
57+
id: '1746776402_ans',
58+
text:
59+
'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',
60+
author: aiUser,
61+
createdAt: DateTime.now().add(const Duration(seconds: 4)),
62+
),
63+
].reversed.toList(),
64+
);
65+
await tester.pumpAndSettle(Duration(seconds: 1));
66+
67+
// start chat
68+
final int messageId = 1;
69+
70+
// send a message
71+
await tester.sendUserMessage(
72+
Message.text(
73+
id: messageId.toString(),
74+
text: 'How to use AppFlowy?',
75+
author: user,
76+
createdAt: DateTime.now(),
77+
),
78+
);
79+
await tester.pumpAndSettle(Duration(seconds: 1));
80+
81+
// receive a message
82+
await tester.receiveAIMessage(
83+
Message.text(
84+
id: '${messageId}_ans',
85+
text: '''# How to Use AppFlowy
86+
- Download and install AppFlowy from the official website (appflowy.io) or through app stores for your operating system (Windows, macOS, Linux, or mobile)
87+
- Create an account or sign in when you first launch the app
88+
- The main interface shows your workspace with a sidebar for navigation and a content area''',
89+
author: aiUser,
90+
createdAt: DateTime.now(),
91+
),
92+
);
93+
await tester.pumpAndSettle(Duration(seconds: 1));
94+
95+
final chatBloc = tester.getCurrentChatBloc();
96+
expect(chatBloc.chatController.messages.length, equals(6));
97+
});
98+
99+
testWidgets('send messages without default messages', (tester) async {
100+
skipAIChatWelcomePage = true;
101+
102+
await tester.initializeAppFlowy();
103+
await tester.tapAnonymousSignInButton();
104+
105+
// create a chat page
106+
final pageName = 'Untitled';
107+
await tester.createNewPageWithNameUnderParent(
108+
name: pageName,
109+
layout: ViewLayoutPB.Chat,
110+
openAfterCreated: false,
111+
);
112+
113+
await tester.pumpAndSettle(const Duration(milliseconds: 300));
114+
115+
final userId = '457037009907617792';
116+
final user = User(id: userId, lastName: 'Lucas');
117+
final aiUserId = '0';
118+
final aiUser = User(id: aiUserId, lastName: 'AI');
119+
120+
// start chat
121+
int messageId = 1;
122+
123+
// round 1
124+
{
125+
// send a message
126+
await tester.sendUserMessage(
127+
Message.text(
128+
id: messageId.toString(),
129+
text: 'How to use AppFlowy?',
130+
author: user,
131+
createdAt: DateTime.now(),
132+
),
133+
);
134+
await tester.pumpAndSettle(Duration(seconds: 1));
135+
136+
// receive a message
137+
await tester.receiveAIMessage(
138+
Message.text(
139+
id: '${messageId}_ans',
140+
text: '''# How to Use AppFlowy
141+
- Download and install AppFlowy from the official website (appflowy.io) or through app stores for your operating system (Windows, macOS, Linux, or mobile)
142+
- Create an account or sign in when you first launch the app
143+
- The main interface shows your workspace with a sidebar for navigation and a content area''',
144+
author: aiUser,
145+
createdAt: DateTime.now(),
146+
),
147+
);
148+
await tester.pumpAndSettle(Duration(seconds: 1));
149+
messageId++;
150+
}
151+
152+
// round 2
153+
{
154+
// send a message
155+
await tester.sendUserMessage(
156+
Message.text(
157+
id: messageId.toString(),
158+
text: 'How to use AppFlowy?',
159+
author: user,
160+
createdAt: DateTime.now(),
161+
),
162+
);
163+
await tester.pumpAndSettle(Duration(seconds: 1));
164+
165+
// receive a message
166+
await tester.receiveAIMessage(
167+
Message.text(
168+
id: '${messageId}_ans',
169+
text:
170+
'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',
171+
author: aiUser,
172+
createdAt: DateTime.now(),
173+
),
174+
);
175+
await tester.pumpAndSettle(Duration(seconds: 1));
176+
messageId++;
177+
}
178+
179+
// round 3
180+
{
181+
// send a message
182+
await tester.sendUserMessage(
183+
Message.text(
184+
id: messageId.toString(),
185+
text: 'What document formatting options are available?',
186+
author: user,
187+
createdAt: DateTime.now(),
188+
),
189+
);
190+
await tester.pumpAndSettle(Duration(seconds: 1));
191+
192+
// receive a message
193+
await tester.receiveAIMessage(
194+
Message.text(
195+
id: '${messageId}_ans',
196+
text:
197+
'# AppFlowy Document Formatting\n- Basic formatting: Bold, italic, underline, strikethrough\n- Headings: 6 levels of headings for structuring content\n- Lists: Bullet points, numbered lists, and checklists\n- Code blocks: Format text as code with syntax highlighting\n- Tables: Create and format data tables\n- Embedded content: Add images, files, and other rich media',
198+
author: aiUser,
199+
createdAt: DateTime.now(),
200+
),
201+
);
202+
await tester.pumpAndSettle(Duration(seconds: 1));
203+
messageId++;
204+
}
205+
206+
// round 4
207+
{
208+
// send a message
209+
await tester.sendUserMessage(
210+
Message.text(
211+
id: messageId.toString(),
212+
text: 'How do I export my data from AppFlowy?',
213+
author: user,
214+
createdAt: DateTime.now(),
215+
),
216+
);
217+
await tester.pumpAndSettle(Duration(seconds: 1));
218+
219+
// receive a message
220+
await tester.receiveAIMessage(
221+
Message.text(
222+
id: '${messageId}_ans',
223+
text:
224+
'# Exporting from AppFlowy\n- Export documents in multiple formats: Markdown, HTML, PDF\n- Export databases as CSV or Excel files\n- Batch export entire workspaces for backup\n- Use the export menu (three dots → Export) on any page\n- Exported files maintain most formatting and structure',
225+
author: aiUser,
226+
createdAt: DateTime.now(),
227+
),
228+
);
229+
await tester.pumpAndSettle(Duration(seconds: 1));
230+
messageId++;
231+
}
232+
233+
// round 5
234+
{
235+
// send a message
236+
await tester.sendUserMessage(
237+
Message.text(
238+
id: messageId.toString(),
239+
text: 'Is there a mobile version of AppFlowy?',
240+
author: user,
241+
createdAt: DateTime.now(),
242+
),
243+
);
244+
await tester.pumpAndSettle(Duration(seconds: 1));
245+
246+
// receive a message
247+
await tester.receiveAIMessage(
248+
Message.text(
249+
id: '${messageId}_ans',
250+
text:
251+
'# AppFlowy on Mobile\n- Yes, AppFlowy is available for iOS and Android devices\n- Download from the App Store or Google Play Store\n- Mobile app includes core functionality: document editing, databases, and boards\n- Offline mode allows working without internet connection\n- Sync automatically when you reconnect\n- Responsive design adapts to different screen sizes',
252+
author: aiUser,
253+
createdAt: DateTime.now(),
254+
),
255+
);
256+
await tester.pumpAndSettle(Duration(seconds: 1));
257+
messageId++;
258+
}
259+
260+
final chatBloc = tester.getCurrentChatBloc();
261+
expect(chatBloc.chatController.messages.length, equals(10));
262+
});
263+
});
264+
}

frontend/appflowy_flutter/integration_test/desktop_runner_9.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:integration_test/integration_test.dart';
22

3+
import 'desktop/chat/chat_page_test.dart' as chat_page_test;
34
import 'desktop/database/database_icon_test.dart' as database_icon_test;
45
import 'desktop/first_test/first_test.dart' as first_test;
56
import 'desktop/uncategorized/code_block_language_selector_test.dart'
@@ -17,4 +18,5 @@ Future<void> runIntegration9OnDesktop() async {
1718
tabs_test.main();
1819
code_language_selector.main();
1920
database_icon_test.main();
21+
chat_page_test.main();
2022
}

frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import 'package:appflowy/ai/ai.dart';
2+
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
3+
import 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_content_page.dart';
24
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart';
35
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
46
import 'package:flutter/material.dart';
7+
import 'package:flutter_bloc/flutter_bloc.dart';
8+
import 'package:flutter_chat_core/flutter_chat_core.dart';
59
import 'package:flutter_test/flutter_test.dart';
610

11+
import '../../test/util.dart';
712
import 'util.dart';
813

914
extension AppFlowyAITest on WidgetTester {
@@ -38,4 +43,27 @@ extension AppFlowyAITest on WidgetTester {
3843
testTextInput.enterText(text);
3944
await pumpAndSettle(const Duration(milliseconds: 300));
4045
}
46+
47+
ChatBloc getCurrentChatBloc() {
48+
return element(find.byType(ChatContentPage)).read<ChatBloc>();
49+
}
50+
51+
Future<void> loadDefaultMessages(List<Message> messages) async {
52+
final chatBloc = getCurrentChatBloc();
53+
chatBloc.add(ChatEvent.didLoadLatestMessages(messages));
54+
await blocResponseFuture();
55+
}
56+
57+
Future<void> sendUserMessage(Message message) async {
58+
final chatBloc = getCurrentChatBloc();
59+
// using received message to simulate the user message
60+
chatBloc.add(ChatEvent.receiveMessage(message));
61+
await blocResponseFuture();
62+
}
63+
64+
Future<void> receiveAIMessage(Message message) async {
65+
final chatBloc = getCurrentChatBloc();
66+
chatBloc.add(ChatEvent.receiveMessage(message));
67+
await blocResponseFuture();
68+
}
4169
}

frontend/appflowy_flutter/integration_test/shared/common_operations.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,8 @@ extension ViewLayoutPBTest on ViewLayoutPB {
10511051
return LocaleKeys.document_menuName.tr();
10521052
case ViewLayoutPB.Calendar:
10531053
return LocaleKeys.calendar_menuName.tr();
1054+
case ViewLayoutPB.Chat:
1055+
return LocaleKeys.chat_newChat.tr();
10541056
default:
10551057
throw UnsupportedError('Unsupported layout: $this');
10561058
}

frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
8282
await event.when(
8383
// Loading messages
8484
didLoadLatestMessages: (List<Message> messages) async {
85+
Log.debug(
86+
"[ChatBloc] did load latest messages: ${messages.length}",
87+
);
88+
8589
for (final message in messages) {
90+
Log.debug("[ChatBloc] insert message: ${message.toJson()}");
8691
await chatController.insert(message, index: 0);
8792
}
8893

@@ -160,6 +165,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
160165
chatController.insert(message);
161166
},
162167
receiveMessage: (Message message) {
168+
Log.debug("[ChatBloc] receive message: ${message.toJson()}");
163169
final oldMessage = chatController.messages
164170
.firstWhereOrNull((m) => m.id == message.id);
165171
if (oldMessage == null) {

0 commit comments

Comments
 (0)