at main 8.2 kB view raw
1import 'package:coves_flutter/models/comment.dart'; 2import 'package:coves_flutter/models/post.dart'; 3import 'package:coves_flutter/widgets/comment_thread.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 ThreadViewComment thread, { 51 int depth = 0, 52 int maxDepth = 5, 53 void Function(ThreadViewComment)? onCommentTap, 54 void Function(String uri)? onCollapseToggle, 55 void Function(ThreadViewComment, List<ThreadViewComment>)? onContinueThread, 56 Set<String> collapsedComments = const {}, 57 List<ThreadViewComment> ancestors = const [], 58 }) { 59 return MultiProvider( 60 providers: [ 61 ChangeNotifierProvider<MockAuthProvider>.value(value: mockAuthProvider), 62 ChangeNotifierProvider<MockVoteProvider>.value(value: mockVoteProvider), 63 ], 64 child: MaterialApp( 65 home: Scaffold( 66 body: SingleChildScrollView( 67 child: CommentThread( 68 thread: thread, 69 depth: depth, 70 maxDepth: maxDepth, 71 onCommentTap: onCommentTap, 72 onCollapseToggle: onCollapseToggle, 73 onContinueThread: onContinueThread, 74 collapsedComments: collapsedComments, 75 ancestors: ancestors, 76 ), 77 ), 78 ), 79 ), 80 ); 81 } 82 83 group('CommentThread', () { 84 group('countDescendants', () { 85 test('returns 0 for thread with no replies', () { 86 final thread = createThread(uri: 'comment/1'); 87 88 expect(CommentThread.countDescendants(thread), 0); 89 }); 90 91 test('returns 0 for thread with empty replies', () { 92 final thread = createThread(uri: 'comment/1', replies: []); 93 94 expect(CommentThread.countDescendants(thread), 0); 95 }); 96 97 test('counts direct replies', () { 98 final thread = createThread( 99 uri: 'comment/1', 100 replies: [ 101 createThread(uri: 'comment/2'), 102 createThread(uri: 'comment/3'), 103 ], 104 ); 105 106 expect(CommentThread.countDescendants(thread), 2); 107 }); 108 109 test('counts nested replies recursively', () { 110 final thread = createThread( 111 uri: 'comment/1', 112 replies: [ 113 createThread( 114 uri: 'comment/2', 115 replies: [ 116 createThread(uri: 'comment/3'), 117 createThread( 118 uri: 'comment/4', 119 replies: [ 120 createThread(uri: 'comment/5'), 121 ], 122 ), 123 ], 124 ), 125 ], 126 ); 127 128 // 1 direct reply + 2 nested + 1 deeply nested = 4 129 expect(CommentThread.countDescendants(thread), 4); 130 }); 131 }); 132 133 group( 134 'rendering', 135 skip: 'Provider type compatibility issues - needs mock refactoring', 136 () { 137 testWidgets('renders comment content', (tester) async { 138 final thread = createThread( 139 uri: 'comment/1', 140 content: 'Hello, world!', 141 ); 142 143 await tester.pumpWidget(createTestWidget(thread)); 144 145 expect(find.text('Hello, world!'), findsOneWidget); 146 }); 147 148 testWidgets('renders nested replies when depth < maxDepth', 149 (tester) async { 150 final thread = createThread( 151 uri: 'comment/1', 152 content: 'Parent', 153 replies: [ 154 createThread(uri: 'comment/2', content: 'Child 1'), 155 createThread(uri: 'comment/3', content: 'Child 2'), 156 ], 157 ); 158 159 await tester.pumpWidget(createTestWidget(thread)); 160 161 expect(find.text('Parent'), findsOneWidget); 162 expect(find.text('Child 1'), findsOneWidget); 163 expect(find.text('Child 2'), findsOneWidget); 164 }); 165 166 testWidgets('shows "Read X more replies" at maxDepth', (tester) async { 167 final thread = createThread( 168 uri: 'comment/1', 169 content: 'At max depth', 170 replies: [ 171 createThread(uri: 'comment/2', content: 'Hidden reply'), 172 ], 173 ); 174 175 await tester.pumpWidget(createTestWidget(thread, depth: 5)); 176 177 expect(find.text('At max depth'), findsOneWidget); 178 expect(find.textContaining('Read'), findsOneWidget); 179 expect(find.textContaining('more'), findsOneWidget); 180 // The hidden reply should NOT be rendered 181 expect(find.text('Hidden reply'), findsNothing); 182 }); 183 184 testWidgets('does not show "Read more" when depth < maxDepth', 185 (tester) async { 186 final thread = createThread( 187 uri: 'comment/1', 188 replies: [ 189 createThread(uri: 'comment/2'), 190 ], 191 ); 192 193 await tester.pumpWidget(createTestWidget(thread, depth: 3)); 194 195 expect(find.textContaining('Read'), findsNothing); 196 }); 197 198 testWidgets('calls onContinueThread with correct ancestors', 199 (tester) async { 200 ThreadViewComment? tappedThread; 201 List<ThreadViewComment>? receivedAncestors; 202 203 final thread = createThread( 204 uri: 'comment/1', 205 replies: [ 206 createThread(uri: 'comment/2'), 207 ], 208 ); 209 210 await tester.pumpWidget(createTestWidget( 211 thread, 212 depth: 5, 213 onContinueThread: (t, a) { 214 tappedThread = t; 215 receivedAncestors = a; 216 }, 217 )); 218 219 // Find and tap the "Read more" link 220 final readMoreFinder = find.textContaining('Read'); 221 expect(readMoreFinder, findsOneWidget); 222 223 await tester.tap(readMoreFinder); 224 await tester.pump(); 225 226 expect(tappedThread, isNotNull); 227 expect(tappedThread!.comment.uri, 'comment/1'); 228 expect(receivedAncestors, isNotNull); 229 // ancestors should NOT include the thread itself 230 expect(receivedAncestors, isEmpty); 231 }); 232 233 testWidgets('handles correct reply count pluralization', 234 (tester) async { 235 // Single reply 236 final singleReplyThread = createThread( 237 uri: 'comment/1', 238 replies: [ 239 createThread(uri: 'comment/2'), 240 ], 241 ); 242 243 await tester.pumpWidget( 244 createTestWidget(singleReplyThread, depth: 5), 245 ); 246 247 expect(find.text('Read 1 more reply'), findsOneWidget); 248 }); 249 250 testWidgets('handles multiple replies pluralization', (tester) async { 251 final multiReplyThread = createThread( 252 uri: 'comment/1', 253 replies: [ 254 createThread(uri: 'comment/2'), 255 createThread(uri: 'comment/3'), 256 createThread(uri: 'comment/4'), 257 ], 258 ); 259 260 await tester.pumpWidget(createTestWidget(multiReplyThread, depth: 5)); 261 262 expect(find.text('Read 3 more replies'), findsOneWidget); 263 }); 264 }, 265 ); 266 }); 267}