Skip to content

Commit 0f3b249

Browse files
committed
test: send messages without default messages
1 parent 527cfa2 commit 0f3b249

File tree

4 files changed

+228
-46
lines changed

4 files changed

+228
-46
lines changed

frontend/appflowy_flutter/integration_test/desktop/chat/chat_page_test.dart

Lines changed: 209 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_animation_list_widget.dart';
12
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
23
import 'package:flutter_chat_core/flutter_chat_core.dart';
34
import 'package:flutter_test/flutter_test.dart';
@@ -10,7 +11,9 @@ void main() {
1011
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
1112

1213
group('chat page:', () {
13-
testWidgets('send messages', (tester) async {
14+
testWidgets('send messages with default messages', (tester) async {
15+
skipAIChatWelcomePage = true;
16+
1417
await tester.initializeAppFlowy();
1518
await tester.tapAnonymousSignInButton();
1619

@@ -22,12 +25,46 @@ void main() {
2225
openAfterCreated: false,
2326
);
2427

28+
await tester.pumpAndSettle(const Duration(milliseconds: 300));
29+
2530
final userId = '457037009907617792';
2631
final user = User(id: userId, lastName: 'Lucas');
27-
final aiUserId = '457037009907617793';
32+
final aiUserId = '0';
2833
final aiUser = User(id: aiUserId, lastName: 'AI');
2934

30-
// focus on the chat page
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
3168
final int messageId = 1;
3269

3370
// send a message
@@ -39,6 +76,7 @@ void main() {
3976
createdAt: DateTime.now(),
4077
),
4178
);
79+
await tester.pumpAndSettle(Duration(seconds: 1));
4280

4381
// receive a message
4482
await tester.receiveAIMessage(
@@ -52,8 +90,175 @@ void main() {
5290
createdAt: DateTime.now(),
5391
),
5492
);
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+
}
55259

56-
await tester.wait(100000);
260+
final chatBloc = tester.getCurrentChatBloc();
261+
expect(chatBloc.chatController.messages.length, equals(10));
57262
});
58263
});
59264
}

frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,25 @@ extension AppFlowyAITest on WidgetTester {
4444
await pumpAndSettle(const Duration(milliseconds: 300));
4545
}
4646

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+
4757
Future<void> sendUserMessage(Message message) async {
48-
final chatBloc = element(find.byType(ChatContentPage)).read<ChatBloc>();
58+
final chatBloc = getCurrentChatBloc();
4959
// using received message to simulate the user message
5060
chatBloc.add(ChatEvent.receiveMessage(message));
5161
await blocResponseFuture();
5262
}
5363

5464
Future<void> receiveAIMessage(Message message) async {
55-
final chatBloc = element(find.byType(ChatContentPage)).read<ChatBloc>();
65+
final chatBloc = getCurrentChatBloc();
5666
chatBloc.add(ChatEvent.receiveMessage(message));
5767
await blocResponseFuture();
5868
}

frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,6 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
152152
itemPositionsListener.itemPositions.addListener(() {
153153
_handleLoadPreviousMessages();
154154
});
155-
156-
// A trick to avoid the first message being scrolled to the top
157155
}
158156

159157
@override
@@ -170,6 +168,7 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
170168
final builders = context.watch<Builders>();
171169
final height = MediaQuery.of(context).size.height;
172170

171+
// A trick to avoid the first message being scrolled to the top
173172
initialScrollIndex = messages.length;
174173
initialAlignment = 1.0;
175174
if (messages.length <= 2) {

frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_page/chat_animation_list_widget.dart

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import 'dart:async';
2-
3-
import 'package:appflowy/ai/service/ai_prompt_input_bloc.dart';
1+
import 'package:appflowy/ai/ai.dart';
42
import 'package:appflowy/plugins/ai_chat/application/ai_chat_prelude.dart';
53
import 'package:appflowy/plugins/ai_chat/presentation/animated_chat_list.dart';
64
import 'package:appflowy/plugins/ai_chat/presentation/animated_chat_list_reversed.dart';
@@ -11,6 +9,9 @@ import 'package:flutter/material.dart';
119
import 'package:flutter_bloc/flutter_bloc.dart';
1210
import 'package:flutter_chat_core/flutter_chat_core.dart';
1311

12+
@visibleForTesting
13+
bool skipAIChatWelcomePage = false;
14+
1415
class ChatAnimationListWidget extends StatefulWidget {
1516
const ChatAnimationListWidget({
1617
super.key,
@@ -31,45 +32,12 @@ class ChatAnimationListWidget extends StatefulWidget {
3132
}
3233

3334
class _ChatAnimationListWidgetState extends State<ChatAnimationListWidget> {
34-
bool hasMessage = false;
35-
StreamSubscription<ChatOperation>? subscription;
36-
37-
@override
38-
void initState() {
39-
super.initState();
40-
41-
final bloc = context.read<ChatBloc>();
42-
if (bloc.chatController.messages.isNotEmpty) {
43-
hasMessage = true;
44-
}
45-
46-
subscription = bloc.chatController.operationsStream.listen((operation) {
47-
final newHasMessage = bloc.chatController.messages.isNotEmpty;
48-
49-
if (!mounted) {
50-
return;
51-
}
52-
53-
if (hasMessage != newHasMessage) {
54-
setState(() {
55-
hasMessage = newHasMessage;
56-
});
57-
}
58-
});
59-
}
60-
61-
@override
62-
void dispose() {
63-
subscription?.cancel();
64-
65-
super.dispose();
66-
}
67-
6835
@override
6936
Widget build(BuildContext context) {
7037
final bloc = context.read<ChatBloc>();
7138

72-
if (!hasMessage) {
39+
// this logic is quite weird, why don't we just get the message from the state?
40+
if (bloc.chatController.messages.isEmpty && !skipAIChatWelcomePage) {
7341
return ChatWelcomePage(
7442
userProfile: widget.userProfile,
7543
onSelectedQuestion: (question) {

0 commit comments

Comments
 (0)