Main coves client
1import 'package:coves_flutter/models/comment.dart';
2import 'package:coves_flutter/models/post.dart';
3import 'package:coves_flutter/screens/home/focused_thread_screen.dart';
4import 'package:flutter/material.dart';
5import 'package:flutter_test/flutter_test.dart';
6import 'package:provider/provider.dart';
7
8import '../test_helpers/mock_providers.dart';
9
10void main() {
11 late MockAuthProvider mockAuthProvider;
12 late MockVoteProvider mockVoteProvider;
13
14 setUp(() {
15 mockAuthProvider = MockAuthProvider();
16 mockVoteProvider = MockVoteProvider();
17 });
18
19 /// Helper to create a test comment
20 CommentView createComment({
21 required String uri,
22 String content = 'Test comment',
23 String handle = 'test.user',
24 }) {
25 return CommentView(
26 uri: uri,
27 cid: 'cid-$uri',
28 content: content,
29 createdAt: DateTime(2025),
30 indexedAt: DateTime(2025),
31 author: AuthorView(did: 'did:plc:author', handle: handle),
32 post: CommentRef(uri: 'at://did:plc:test/post/123', cid: 'post-cid'),
33 stats: CommentStats(upvotes: 5, downvotes: 1, score: 4),
34 );
35 }
36
37 /// Helper to create a thread with nested replies
38 ThreadViewComment createThread({
39 required String uri,
40 String content = 'Test comment',
41 List<ThreadViewComment>? replies,
42 }) {
43 return ThreadViewComment(
44 comment: createComment(uri: uri, content: content),
45 replies: replies,
46 );
47 }
48
49 Widget createTestWidget({
50 required ThreadViewComment thread,
51 List<ThreadViewComment> ancestors = const [],
52 Future<void> Function(String, ThreadViewComment)? onReply,
53 }) {
54 return MultiProvider(
55 providers: [
56 ChangeNotifierProvider<MockAuthProvider>.value(value: mockAuthProvider),
57 ChangeNotifierProvider<MockVoteProvider>.value(value: mockVoteProvider),
58 ],
59 child: MaterialApp(
60 home: FocusedThreadScreen(
61 thread: thread,
62 ancestors: ancestors,
63 onReply: onReply ?? (content, parent) async {},
64 ),
65 ),
66 );
67 }
68
69 group(
70 'FocusedThreadScreen',
71 skip: 'Provider type compatibility issues - needs mock refactoring',
72 () {
73 testWidgets('renders anchor comment', (tester) async {
74 final thread = createThread(
75 uri: 'comment/anchor',
76 content: 'This is the anchor comment',
77 );
78
79 await tester.pumpWidget(createTestWidget(thread: thread));
80 await tester.pumpAndSettle();
81
82 expect(find.text('This is the anchor comment'), findsOneWidget);
83 });
84
85 testWidgets('renders ancestor comments', (tester) async {
86 final ancestor1 = createThread(
87 uri: 'comment/1',
88 content: 'First ancestor',
89 );
90 final ancestor2 = createThread(
91 uri: 'comment/2',
92 content: 'Second ancestor',
93 );
94 final anchor = createThread(
95 uri: 'comment/anchor',
96 content: 'Anchor comment',
97 );
98
99 await tester.pumpWidget(createTestWidget(
100 thread: anchor,
101 ancestors: [ancestor1, ancestor2],
102 ));
103 await tester.pumpAndSettle();
104
105 expect(find.text('First ancestor'), findsOneWidget);
106 expect(find.text('Second ancestor'), findsOneWidget);
107 expect(find.text('Anchor comment'), findsOneWidget);
108 });
109
110 testWidgets('renders replies below anchor', (tester) async {
111 final thread = createThread(
112 uri: 'comment/anchor',
113 content: 'Anchor comment',
114 replies: [
115 createThread(uri: 'comment/reply1', content: 'First reply'),
116 createThread(uri: 'comment/reply2', content: 'Second reply'),
117 ],
118 );
119
120 await tester.pumpWidget(createTestWidget(thread: thread));
121 await tester.pumpAndSettle();
122
123 expect(find.text('Anchor comment'), findsOneWidget);
124 expect(find.text('First reply'), findsOneWidget);
125 expect(find.text('Second reply'), findsOneWidget);
126 });
127
128 testWidgets('shows empty state when no replies', (tester) async {
129 final thread = createThread(
130 uri: 'comment/anchor',
131 content: 'Anchor with no replies',
132 );
133
134 await tester.pumpWidget(createTestWidget(thread: thread));
135 await tester.pumpAndSettle();
136
137 expect(find.text('No replies yet'), findsOneWidget);
138 expect(
139 find.text('Be the first to reply to this comment'),
140 findsOneWidget,
141 );
142 });
143
144 testWidgets('does not duplicate thread in ancestors', (tester) async {
145 // This tests the fix for the duplication bug
146 final ancestor = createThread(
147 uri: 'comment/ancestor',
148 content: 'Ancestor content',
149 );
150 final anchor = createThread(
151 uri: 'comment/anchor',
152 content: 'Anchor content',
153 );
154
155 await tester.pumpWidget(createTestWidget(
156 thread: anchor,
157 ancestors: [ancestor],
158 ));
159 await tester.pumpAndSettle();
160
161 // Anchor should appear exactly once
162 expect(find.text('Anchor content'), findsOneWidget);
163 // Ancestor should appear exactly once
164 expect(find.text('Ancestor content'), findsOneWidget);
165 });
166
167 testWidgets('shows Thread title in app bar', (tester) async {
168 final thread = createThread(uri: 'comment/1');
169
170 await tester.pumpWidget(createTestWidget(thread: thread));
171 await tester.pumpAndSettle();
172
173 expect(find.text('Thread'), findsOneWidget);
174 });
175
176 testWidgets('ancestors are styled with reduced opacity', (tester) async {
177 final ancestor = createThread(
178 uri: 'comment/ancestor',
179 content: 'Ancestor',
180 );
181 final anchor = createThread(
182 uri: 'comment/anchor',
183 content: 'Anchor',
184 );
185
186 await tester.pumpWidget(createTestWidget(
187 thread: anchor,
188 ancestors: [ancestor],
189 ));
190 await tester.pumpAndSettle();
191
192 // Find the Opacity widget wrapping ancestor
193 final opacityFinder = find.ancestor(
194 of: find.text('Ancestor'),
195 matching: find.byType(Opacity),
196 );
197
198 expect(opacityFinder, findsOneWidget);
199
200 final opacity = tester.widget<Opacity>(opacityFinder);
201 expect(opacity.opacity, 0.6);
202 });
203 },
204 );
205}