Main coves client
1import 'package:coves_flutter/models/post.dart';
2import 'package:coves_flutter/services/streamable_service.dart';
3import 'package:coves_flutter/widgets/post_card.dart';
4import 'package:dio/dio.dart';
5import 'package:flutter/material.dart';
6import 'package:flutter_test/flutter_test.dart';
7import 'package:http_mock_adapter/http_mock_adapter.dart';
8import 'package:provider/provider.dart';
9
10import '../test_helpers/mock_providers.dart';
11
12void main() {
13 late MockAuthProvider mockAuthProvider;
14 late MockVoteProvider mockVoteProvider;
15
16 setUp(() {
17 mockAuthProvider = MockAuthProvider();
18 mockVoteProvider = MockVoteProvider();
19 });
20
21 Widget createTestWidget(
22 FeedViewPost post, {
23 StreamableService? streamableService,
24 }) {
25 return MultiProvider(
26 providers: [
27 // ignore: argument_type_not_assignable
28 ChangeNotifierProvider<MockAuthProvider>.value(value: mockAuthProvider),
29 // ignore: argument_type_not_assignable
30 ChangeNotifierProvider<MockVoteProvider>.value(value: mockVoteProvider),
31 Provider<StreamableService>.value(
32 value: streamableService ?? StreamableService(),
33 ),
34 ],
35 child: MaterialApp(home: Scaffold(body: PostCard(post: post))),
36 );
37 }
38
39 group(
40 'PostCard',
41 skip: 'Provider type compatibility issues - needs mock refactoring',
42 () {
43 testWidgets('renders all basic components', (tester) async {
44 final post = FeedViewPost(
45 post: PostView(
46 uri: 'at://did:example/post/123',
47 cid: 'cid123',
48 rkey: '123',
49 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
50 community: CommunityRef(
51 did: 'did:plc:community',
52 name: 'test-community',
53 ),
54 createdAt: DateTime(2024),
55 indexedAt: DateTime(2024),
56 text: 'Test post content',
57 title: 'Test Post Title',
58 stats: PostStats(
59 upvotes: 10,
60 downvotes: 2,
61 score: 8,
62 commentCount: 5,
63 ),
64 ),
65 );
66
67 await tester.pumpWidget(createTestWidget(post));
68
69 // Verify title is displayed
70 expect(find.text('Test Post Title'), findsOneWidget);
71
72 // Verify community name is displayed
73 expect(find.text('c/test-community'), findsOneWidget);
74
75 // Verify author handle is displayed
76 expect(find.text('@author.test'), findsOneWidget);
77
78 // Verify text content is displayed
79 expect(find.text('Test post content'), findsOneWidget);
80
81 // Verify stats are displayed
82 expect(find.text('8'), findsOneWidget); // score
83 expect(find.text('5'), findsOneWidget); // comment count
84 });
85
86 testWidgets('displays community avatar when available', (tester) async {
87 final post = FeedViewPost(
88 post: PostView(
89 uri: 'at://did:example/post/123',
90 cid: 'cid123',
91 rkey: '123',
92 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
93 community: CommunityRef(
94 did: 'did:plc:community',
95 name: 'test-community',
96 avatar: 'https://example.com/avatar.jpg',
97 ),
98 createdAt: DateTime(2024),
99 indexedAt: DateTime(2024),
100 text: '',
101 stats: PostStats(
102 upvotes: 0,
103 downvotes: 0,
104 score: 0,
105 commentCount: 0,
106 ),
107 ),
108 );
109
110 await tester.pumpWidget(createTestWidget(post));
111 await tester.pumpAndSettle();
112
113 // Avatar image should be present
114 expect(find.byType(Image), findsWidgets);
115 });
116
117 testWidgets('shows fallback avatar when no avatar URL', (tester) async {
118 final post = FeedViewPost(
119 post: PostView(
120 uri: 'at://did:example/post/123',
121 cid: 'cid123',
122 rkey: '123',
123 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
124 community: CommunityRef(
125 did: 'did:plc:community',
126 name: 'TestCommunity',
127 ),
128 createdAt: DateTime(2024),
129 indexedAt: DateTime(2024),
130 text: '',
131 stats: PostStats(
132 upvotes: 0,
133 downvotes: 0,
134 score: 0,
135 commentCount: 0,
136 ),
137 ),
138 );
139
140 await tester.pumpWidget(createTestWidget(post));
141
142 // Verify fallback shows first letter
143 expect(find.text('T'), findsOneWidget);
144 });
145
146 testWidgets('displays external link bar when embed present', (
147 tester,
148 ) async {
149 final post = FeedViewPost(
150 post: PostView(
151 uri: 'at://did:example/post/123',
152 cid: 'cid123',
153 rkey: '123',
154 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
155 community: CommunityRef(
156 did: 'did:plc:community',
157 name: 'test-community',
158 ),
159 createdAt: DateTime(2024),
160 indexedAt: DateTime(2024),
161 text: '',
162 stats: PostStats(
163 upvotes: 0,
164 downvotes: 0,
165 score: 0,
166 commentCount: 0,
167 ),
168 embed: PostEmbed(
169 type: 'social.coves.embed.external',
170 external: ExternalEmbed(
171 uri: 'https://example.com/article',
172 domain: 'example.com',
173 title: 'Example Article',
174 ),
175 data: const {},
176 ),
177 ),
178 );
179
180 await tester.pumpWidget(createTestWidget(post));
181
182 // Verify external link bar is present
183 expect(find.text('example.com'), findsOneWidget);
184 expect(find.byIcon(Icons.open_in_new), findsOneWidget);
185 });
186
187 testWidgets('displays embed image when available', (tester) async {
188 final post = FeedViewPost(
189 post: PostView(
190 uri: 'at://did:example/post/123',
191 cid: 'cid123',
192 rkey: '123',
193 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
194 community: CommunityRef(
195 did: 'did:plc:community',
196 name: 'test-community',
197 ),
198 createdAt: DateTime(2024),
199 indexedAt: DateTime(2024),
200 text: '',
201 stats: PostStats(
202 upvotes: 0,
203 downvotes: 0,
204 score: 0,
205 commentCount: 0,
206 ),
207 embed: PostEmbed(
208 type: 'social.coves.embed.external',
209 external: ExternalEmbed(
210 uri: 'https://example.com/article',
211 thumb: 'https://example.com/thumb.jpg',
212 ),
213 data: const {},
214 ),
215 ),
216 );
217
218 await tester.pumpWidget(createTestWidget(post));
219 await tester.pump();
220
221 // Embed image should be loading/present
222 expect(find.byType(Image), findsWidgets);
223 });
224
225 testWidgets('renders without title', (tester) async {
226 final post = FeedViewPost(
227 post: PostView(
228 uri: 'at://did:example/post/123',
229 cid: 'cid123',
230 rkey: '123',
231 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
232 community: CommunityRef(
233 did: 'did:plc:community',
234 name: 'test-community',
235 ),
236 createdAt: DateTime(2024),
237 indexedAt: DateTime(2024),
238 text: 'Just body text',
239 stats: PostStats(
240 upvotes: 0,
241 downvotes: 0,
242 score: 0,
243 commentCount: 0,
244 ),
245 ),
246 );
247
248 await tester.pumpWidget(createTestWidget(post));
249
250 // Should render without errors
251 expect(find.text('Just body text'), findsOneWidget);
252 expect(find.text('c/test-community'), findsOneWidget);
253 });
254
255 testWidgets('has action buttons', (tester) async {
256 final post = FeedViewPost(
257 post: PostView(
258 uri: 'at://did:example/post/123',
259 cid: 'cid123',
260 rkey: '123',
261 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
262 community: CommunityRef(
263 did: 'did:plc:community',
264 name: 'test-community',
265 ),
266 createdAt: DateTime(2024),
267 indexedAt: DateTime(2024),
268 text: '',
269 stats: PostStats(
270 upvotes: 0,
271 downvotes: 0,
272 score: 0,
273 commentCount: 0,
274 ),
275 ),
276 );
277
278 await tester.pumpWidget(createTestWidget(post));
279
280 // Verify action buttons are present
281 expect(find.byIcon(Icons.more_horiz), findsOneWidget); // menu
282 // Share, comment, and heart icons are custom widgets, verify by count
283 expect(find.byType(InkWell), findsWidgets);
284 });
285
286 testWidgets('displays play button overlay for Streamable videos', (
287 tester,
288 ) async {
289 final post = FeedViewPost(
290 post: PostView(
291 uri: 'at://did:example/post/123',
292 cid: 'cid123',
293 rkey: '123',
294 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
295 community: CommunityRef(
296 did: 'did:plc:community',
297 name: 'test-community',
298 ),
299 createdAt: DateTime(2024),
300 indexedAt: DateTime(2024),
301 text: '',
302 stats: PostStats(
303 upvotes: 0,
304 downvotes: 0,
305 score: 0,
306 commentCount: 0,
307 ),
308 embed: PostEmbed(
309 type: 'social.coves.embed.external',
310 external: ExternalEmbed(
311 uri: 'https://streamable.com/abc123',
312 thumb: 'https://example.com/thumb.jpg',
313 embedType: 'video',
314 provider: 'streamable',
315 ),
316 data: const {},
317 ),
318 ),
319 );
320
321 await tester.pumpWidget(createTestWidget(post));
322 await tester.pump();
323
324 // Verify play button is displayed
325 expect(find.byIcon(Icons.play_arrow), findsOneWidget);
326 });
327
328 testWidgets(
329 'shows loading indicator when fetching video URL for Streamable',
330 (tester) async {
331 final dio = Dio(BaseOptions(baseUrl: 'https://api.streamable.com'));
332 final dioAdapter = DioAdapter(dio: dio);
333 final streamableService = StreamableService(dio: dio);
334
335 // Delay the response to test loading state
336 dioAdapter.onGet(
337 '/videos/abc123',
338 (server) => server.reply(200, {
339 'files': {
340 'mp4': {'url': '//cdn.streamable.com/video.mp4'},
341 },
342 }, delay: const Duration(milliseconds: 500)),
343 );
344
345 final post = FeedViewPost(
346 post: PostView(
347 uri: 'at://did:example/post/123',
348 cid: 'cid123',
349 rkey: '123',
350 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
351 community: CommunityRef(
352 did: 'did:plc:community',
353 name: 'test-community',
354 ),
355 createdAt: DateTime(2024),
356 indexedAt: DateTime(2024),
357 text: '',
358 stats: PostStats(
359 upvotes: 0,
360 downvotes: 0,
361 score: 0,
362 commentCount: 0,
363 ),
364 embed: PostEmbed(
365 type: 'social.coves.embed.external',
366 external: ExternalEmbed(
367 uri: 'https://streamable.com/abc123',
368 thumb: 'https://example.com/thumb.jpg',
369 embedType: 'video',
370 provider: 'streamable',
371 ),
372 data: const {},
373 ),
374 ),
375 );
376
377 await tester.pumpWidget(
378 createTestWidget(post, streamableService: streamableService),
379 );
380 await tester.pump();
381
382 // Tap the play button
383 await tester.tap(find.byIcon(Icons.play_arrow));
384 await tester.pump();
385
386 // Verify loading indicator is displayed
387 expect(find.byType(CircularProgressIndicator), findsOneWidget);
388 },
389 );
390
391 testWidgets('does not show play button for non-video embeds', (
392 tester,
393 ) async {
394 final post = FeedViewPost(
395 post: PostView(
396 uri: 'at://did:example/post/123',
397 cid: 'cid123',
398 rkey: '123',
399 author: AuthorView(did: 'did:plc:author', handle: 'author.test'),
400 community: CommunityRef(
401 did: 'did:plc:community',
402 name: 'test-community',
403 ),
404 createdAt: DateTime(2024),
405 indexedAt: DateTime(2024),
406 text: '',
407 stats: PostStats(
408 upvotes: 0,
409 downvotes: 0,
410 score: 0,
411 commentCount: 0,
412 ),
413 embed: PostEmbed(
414 type: 'social.coves.embed.external',
415 external: ExternalEmbed(
416 uri: 'https://example.com/article',
417 thumb: 'https://example.com/thumb.jpg',
418 ),
419 data: const {},
420 ),
421 ),
422 );
423
424 await tester.pumpWidget(createTestWidget(post));
425 await tester.pump();
426
427 // Verify play button is NOT displayed
428 expect(find.byIcon(Icons.play_arrow), findsNothing);
429 });
430 },
431 );
432}