main
dart 295 lines 9.3 KB
Raw
1 import 'package:flutter/material.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'package:flutter_test/flutter_test.dart';
4 import 'package:gpt_markdown/gpt_markdown.dart';
5 import 'package:localmind/core/providers/app_providers.dart';
6 import 'package:localmind/features/chat/views/components/reasoning_widget.dart';
7 import 'package:localmind/features/settings/data/models/app_settings.dart';
8 import 'package:localmind/l10n/app_localizations.dart';
9
10 Widget _buildTestWidget({
11 required String reasoningContent,
12 bool isStreaming = false,
13 bool hasMainContent = true,
14 AppSettings? settings,
15 }) {
16 final appSettings = settings ?? AppSettings();
17 return ProviderScope(
18 overrides: [
19 settingsProvider.overrideWith(() => _MockSettingsNotifier(appSettings)),
20 ],
21 child: MaterialApp(
22 localizationsDelegates: AppLocalizations.localizationsDelegates,
23 supportedLocales: AppLocalizations.supportedLocales,
24 home: Scaffold(
25 body: SingleChildScrollView(
26 child: ReasoningWidget(
27 reasoningContent: reasoningContent,
28 isStreaming: isStreaming,
29 hasMainContent: hasMainContent,
30 ),
31 ),
32 ),
33 ),
34 );
35 }
36
37 class _MockSettingsNotifier extends SettingsNotifier {
38 final AppSettings _initial;
39 _MockSettingsNotifier(this._initial);
40
41 @override
42 AppSettings build() => _initial;
43 }
44
45 void main() {
46 group('ReasoningWidget Tests', () {
47 testWidgets(
48 'remains expanded when hasMainContent is false (no main response)',
49 (tester) async {
50 await tester.pumpWidget(
51 _buildTestWidget(
52 reasoningContent: 'Thinking about the problem...',
53 isStreaming: false,
54 hasMainContent: false,
55 ),
56 );
57 await tester.pumpAndSettle();
58
59 expect(find.byType(GptMarkdown), findsOneWidget);
60 expect(find.text('Thinking about the problem...'), findsOneWidget);
61
62 final sizeTransition = tester.widget<SizeTransition>(
63 find.byType(SizeTransition),
64 );
65 expect(sizeTransition.sizeFactor.value, 1.0);
66 },
67 );
68
69 testWidgets(
70 'remains expanded after streaming if autoCollapseThinking is false',
71 (tester) async {
72 await tester.pumpWidget(
73 _buildTestWidget(
74 reasoningContent: 'Step-by-step reasoning',
75 isStreaming: false,
76 hasMainContent: true,
77 settings: AppSettings(autoCollapseThinking: false),
78 ),
79 );
80 await tester.pumpAndSettle();
81
82 final sizeTransition = tester.widget<SizeTransition>(
83 find.byType(SizeTransition),
84 );
85 expect(sizeTransition.sizeFactor.value, 1.0);
86 expect(find.text('Step-by-step reasoning'), findsOneWidget);
87 },
88 );
89
90 testWidgets(
91 'default settings keep the bubble expanded after generation (issue #28)',
92 (tester) async {
93 // No settings override: relies on the default value of autoCollapseThinking.
94 await tester.pumpWidget(
95 _buildTestWidget(
96 reasoningContent: 'The answer lived inside the thinking block',
97 isStreaming: false,
98 hasMainContent: true,
99 ),
100 );
101 await tester.pumpAndSettle();
102
103 final sizeTransition = tester.widget<SizeTransition>(
104 find.byType(SizeTransition),
105 );
106 expect(sizeTransition.sizeFactor.value, 1.0);
107 expect(
108 find.text('The answer lived inside the thinking block'),
109 findsOneWidget,
110 );
111 },
112 );
113
114 testWidgets('is expanded while isStreaming is true', (tester) async {
115 await tester.pumpWidget(
116 _buildTestWidget(
117 reasoningContent: 'Currently thinking live...',
118 isStreaming: true,
119 hasMainContent: true,
120 ),
121 );
122 await tester.pump(const Duration(milliseconds: 300));
123
124 final sizeTransition = tester.widget<SizeTransition>(
125 find.byType(SizeTransition),
126 );
127 expect(sizeTransition.sizeFactor.value, 1.0);
128 expect(find.byType(CircularProgressIndicator), findsOneWidget);
129 });
130
131 testWidgets(
132 'collapses when streaming ends if hasMainContent is true and autoCollapseThinking is true',
133 (tester) async {
134 await tester.pumpWidget(
135 _buildTestWidget(
136 reasoningContent: 'Finished thinking',
137 isStreaming: false,
138 hasMainContent: true,
139 settings: AppSettings(autoCollapseThinking: true),
140 ),
141 );
142 await tester.pumpAndSettle();
143
144 final sizeTransition = tester.widget<SizeTransition>(
145 find.byType(SizeTransition),
146 );
147 expect(sizeTransition.sizeFactor.value, 0.0);
148 },
149 );
150
151 testWidgets('toggles expanded state on tap', (tester) async {
152 await tester.pumpWidget(
153 _buildTestWidget(
154 reasoningContent: 'Click to see details',
155 isStreaming: false,
156 hasMainContent: true,
157 settings: AppSettings(autoCollapseThinking: true),
158 ),
159 );
160 await tester.pumpAndSettle();
161
162 // Initially collapsed
163 var sizeTransition = tester.widget<SizeTransition>(
164 find.byType(SizeTransition),
165 );
166 expect(sizeTransition.sizeFactor.value, 0.0);
167
168 // Tap header to expand
169 await tester.tap(find.byType(InkWell));
170 await tester.pumpAndSettle();
171
172 sizeTransition = tester.widget<SizeTransition>(
173 find.byType(SizeTransition),
174 );
175 expect(sizeTransition.sizeFactor.value, 1.0);
176
177 // Tap header to collapse again
178 await tester.tap(find.byType(InkWell));
179 await tester.pumpAndSettle();
180
181 sizeTransition = tester.widget<SizeTransition>(
182 find.byType(SizeTransition),
183 );
184 expect(sizeTransition.sizeFactor.value, 0.0);
185 });
186
187 testWidgets(
188 'transitions from streaming to non-streaming: remains expanded if hasMainContent is false',
189 (tester) async {
190 await tester.pumpWidget(
191 _buildTestWidget(
192 reasoningContent: 'Full answer in reasoning block',
193 isStreaming: true,
194 hasMainContent: false,
195 settings: AppSettings(autoCollapseThinking: true),
196 ),
197 );
198 await tester.pump(const Duration(milliseconds: 300));
199
200 // Stream ends, rebuild with isStreaming: false
201 await tester.pumpWidget(
202 _buildTestWidget(
203 reasoningContent: 'Full answer in reasoning block',
204 isStreaming: false,
205 hasMainContent: false,
206 settings: AppSettings(autoCollapseThinking: true),
207 ),
208 );
209 await tester.pumpAndSettle();
210
211 final sizeTransition = tester.widget<SizeTransition>(
212 find.byType(SizeTransition),
213 );
214 expect(sizeTransition.sizeFactor.value, 1.0);
215 },
216 );
217
218 testWidgets(
219 'transitions from streaming to non-streaming: collapses if hasMainContent is true and autoCollapseThinking is true',
220 (tester) async {
221 await tester.pumpWidget(
222 _buildTestWidget(
223 reasoningContent: 'Intermediary reasoning',
224 isStreaming: true,
225 hasMainContent: true,
226 settings: AppSettings(autoCollapseThinking: true),
227 ),
228 );
229 await tester.pump(const Duration(milliseconds: 300));
230
231 var sizeTransition = tester.widget<SizeTransition>(
232 find.byType(SizeTransition),
233 );
234 expect(sizeTransition.sizeFactor.value, 1.0);
235
236 // Stream ends, rebuild with isStreaming: false and hasMainContent: true
237 await tester.pumpWidget(
238 _buildTestWidget(
239 reasoningContent: 'Intermediary reasoning',
240 isStreaming: false,
241 hasMainContent: true,
242 settings: AppSettings(autoCollapseThinking: true),
243 ),
244 );
245 await tester.pumpAndSettle();
246
247 sizeTransition = tester.widget<SizeTransition>(
248 find.byType(SizeTransition),
249 );
250 expect(sizeTransition.sizeFactor.value, 0.0);
251 },
252 );
253
254 testWidgets(
255 'transitions from streaming to non-streaming: respects manual expand/collapse toggle',
256 (tester) async {
257 await tester.pumpWidget(
258 _buildTestWidget(
259 reasoningContent: 'Streaming reasoning...',
260 isStreaming: true,
261 hasMainContent: true,
262 settings: AppSettings(autoCollapseThinking: true),
263 ),
264 );
265 await tester.pump(const Duration(milliseconds: 300));
266
267 // User manually collapses while streaming
268 await tester.tap(find.byType(InkWell));
269 await tester.pump();
270 await tester.pump(const Duration(milliseconds: 300));
271
272 var sizeTransition = tester.widget<SizeTransition>(
273 find.byType(SizeTransition),
274 );
275 expect(sizeTransition.sizeFactor.value, 0.0);
276
277 // Stream finishes: since user manually collapsed, it stays collapsed
278 await tester.pumpWidget(
279 _buildTestWidget(
280 reasoningContent: 'Streaming reasoning...',
281 isStreaming: false,
282 hasMainContent: true,
283 settings: AppSettings(autoCollapseThinking: true),
284 ),
285 );
286 await tester.pumpAndSettle();
287
288 sizeTransition = tester.widget<SizeTransition>(
289 find.byType(SizeTransition),
290 );
291 expect(sizeTransition.sizeFactor.value, 0.0);
292 },
293 );
294 });
295 }