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}