1import 'package:coves_flutter/widgets/icons/animated_heart_icon.dart'; 2import 'package:flutter/material.dart'; 3import 'package:flutter_test/flutter_test.dart'; 4 5void main() { 6 group('AnimatedHeartIcon', () { 7 testWidgets('should render with default size', (tester) async { 8 await tester.pumpWidget( 9 const MaterialApp( 10 home: Scaffold( 11 body: AnimatedHeartIcon(isLiked: false), 12 ), 13 ), 14 ); 15 16 // Widget should render 17 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 18 19 // Find the SizedBox that defines the size 20 final sizedBox = tester.widget<SizedBox>( 21 find.descendant( 22 of: find.byType(AnimatedHeartIcon), 23 matching: find.byType(SizedBox), 24 ).first, 25 ); 26 27 // Default size should be 18 28 expect(sizedBox.width, 18); 29 expect(sizedBox.height, 18); 30 }); 31 32 testWidgets('should render with custom size', (tester) async { 33 await tester.pumpWidget( 34 const MaterialApp( 35 home: Scaffold( 36 body: AnimatedHeartIcon(isLiked: false, size: 32), 37 ), 38 ), 39 ); 40 41 // Find the SizedBox that defines the size 42 final sizedBox = tester.widget<SizedBox>( 43 find.descendant( 44 of: find.byType(AnimatedHeartIcon), 45 matching: find.byType(SizedBox), 46 ).first, 47 ); 48 49 // Custom size should be 32 50 expect(sizedBox.width, 32); 51 expect(sizedBox.height, 32); 52 }); 53 54 testWidgets('should use custom color when provided', (tester) async { 55 const customColor = Colors.blue; 56 57 await tester.pumpWidget( 58 const MaterialApp( 59 home: Scaffold( 60 body: AnimatedHeartIcon( 61 isLiked: false, 62 color: customColor, 63 ), 64 ), 65 ), 66 ); 67 68 // Widget should render with custom color 69 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 70 // Note: We can't easily verify the color without accessing the CustomPainter, 71 // but we can verify the widget accepts the parameter 72 }); 73 74 testWidgets('should use custom liked color when provided', (tester) async { 75 const customLikedColor = Colors.pink; 76 77 await tester.pumpWidget( 78 const MaterialApp( 79 home: Scaffold( 80 body: AnimatedHeartIcon( 81 isLiked: true, 82 likedColor: customLikedColor, 83 ), 84 ), 85 ), 86 ); 87 88 // Widget should render with custom liked color 89 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 90 }); 91 92 testWidgets('should start animation when isLiked changes to true', 93 (tester) async { 94 // Start with unliked state 95 await tester.pumpWidget( 96 const MaterialApp( 97 home: Scaffold( 98 body: AnimatedHeartIcon(isLiked: false), 99 ), 100 ), 101 ); 102 103 // Verify initial state 104 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 105 106 // Change to liked state 107 await tester.pumpWidget( 108 const MaterialApp( 109 home: Scaffold( 110 body: AnimatedHeartIcon(isLiked: true), 111 ), 112 ), 113 ); 114 115 // Pump frames to allow animation to start 116 await tester.pump(); 117 await tester.pump(const Duration(milliseconds: 100)); 118 119 // Widget should still be present and animating 120 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 121 }); 122 123 testWidgets('should not animate when isLiked changes to false', 124 (tester) async { 125 // Start with liked state 126 await tester.pumpWidget( 127 const MaterialApp( 128 home: Scaffold( 129 body: AnimatedHeartIcon(isLiked: true), 130 ), 131 ), 132 ); 133 134 await tester.pump(); 135 136 // Change to unliked state 137 await tester.pumpWidget( 138 const MaterialApp( 139 home: Scaffold( 140 body: AnimatedHeartIcon(isLiked: false), 141 ), 142 ), 143 ); 144 145 await tester.pump(); 146 147 // Widget should update without error 148 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 149 }); 150 151 testWidgets('should complete animation after duration', (tester) async { 152 // Start with unliked state 153 await tester.pumpWidget( 154 const MaterialApp( 155 home: Scaffold( 156 body: AnimatedHeartIcon(isLiked: false), 157 ), 158 ), 159 ); 160 161 // Change to liked state 162 await tester.pumpWidget( 163 const MaterialApp( 164 home: Scaffold( 165 body: AnimatedHeartIcon(isLiked: true), 166 ), 167 ), 168 ); 169 170 // Pump through the entire animation duration (800ms) 171 await tester.pump(); 172 await tester.pump(const Duration(milliseconds: 800)); 173 await tester.pumpAndSettle(); 174 175 // Widget should still be present after animation completes 176 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 177 }); 178 179 testWidgets('should handle rapid state changes', (tester) async { 180 // Start with unliked state 181 await tester.pumpWidget( 182 const MaterialApp( 183 home: Scaffold( 184 body: AnimatedHeartIcon(isLiked: false), 185 ), 186 ), 187 ); 188 189 // Rapidly toggle states 190 await tester.pumpWidget( 191 const MaterialApp( 192 home: Scaffold( 193 body: AnimatedHeartIcon(isLiked: true), 194 ), 195 ), 196 ); 197 await tester.pump(const Duration(milliseconds: 50)); 198 199 await tester.pumpWidget( 200 const MaterialApp( 201 home: Scaffold( 202 body: AnimatedHeartIcon(isLiked: false), 203 ), 204 ), 205 ); 206 await tester.pump(const Duration(milliseconds: 50)); 207 208 await tester.pumpWidget( 209 const MaterialApp( 210 home: Scaffold( 211 body: AnimatedHeartIcon(isLiked: true), 212 ), 213 ), 214 ); 215 await tester.pump(const Duration(milliseconds: 50)); 216 217 // Widget should handle rapid changes without error 218 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 219 }); 220 221 testWidgets('should use OverflowBox to allow animation overflow', 222 (tester) async { 223 await tester.pumpWidget( 224 const MaterialApp( 225 home: Scaffold( 226 body: AnimatedHeartIcon(isLiked: true), 227 ), 228 ), 229 ); 230 231 // Find the OverflowBox 232 expect(find.byType(OverflowBox), findsOneWidget); 233 234 final overflowBox = tester.widget<OverflowBox>( 235 find.byType(OverflowBox), 236 ); 237 238 // OverflowBox should have larger max dimensions (2.5x the icon size) 239 // to accommodate the 1.3x scale and particle burst 240 expect(overflowBox.maxWidth, 18 * 2.5); 241 expect(overflowBox.maxHeight, 18 * 2.5); 242 }); 243 244 testWidgets('should render CustomPaint for heart icon', (tester) async { 245 await tester.pumpWidget( 246 const MaterialApp( 247 home: Scaffold( 248 body: AnimatedHeartIcon(isLiked: false), 249 ), 250 ), 251 ); 252 253 // Find the CustomPaint widget (used for rendering the heart) 254 expect(find.byType(CustomPaint), findsAtLeastNWidgets(1)); 255 }); 256 257 testWidgets('should not animate on initial render when isLiked is true', 258 (tester) async { 259 // Render with isLiked=true initially 260 await tester.pumpWidget( 261 const MaterialApp( 262 home: Scaffold( 263 body: AnimatedHeartIcon(isLiked: true), 264 ), 265 ), 266 ); 267 268 await tester.pump(); 269 270 // Widget should render in liked state without animation 271 // (Animation only triggers on state change, not initial render) 272 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 273 }); 274 275 testWidgets('should dispose controller properly', (tester) async { 276 await tester.pumpWidget( 277 const MaterialApp( 278 home: Scaffold( 279 body: AnimatedHeartIcon(isLiked: false), 280 ), 281 ), 282 ); 283 284 // Remove the widget 285 await tester.pumpWidget( 286 const MaterialApp( 287 home: Scaffold( 288 body: SizedBox.shrink(), 289 ), 290 ), 291 ); 292 293 // Should dispose without error 294 // (No assertions needed - test passes if no exception is thrown) 295 }); 296 297 testWidgets('should rebuild when isLiked changes', (tester) async { 298 var buildCount = 0; 299 300 await tester.pumpWidget( 301 MaterialApp( 302 home: Scaffold( 303 body: Builder( 304 builder: (context) { 305 buildCount++; 306 return const AnimatedHeartIcon(isLiked: false); 307 }, 308 ), 309 ), 310 ), 311 ); 312 313 final initialBuildCount = buildCount; 314 315 // Change isLiked state 316 await tester.pumpWidget( 317 MaterialApp( 318 home: Scaffold( 319 body: Builder( 320 builder: (context) { 321 buildCount++; 322 return const AnimatedHeartIcon(isLiked: true); 323 }, 324 ), 325 ), 326 ), 327 ); 328 329 // Should rebuild 330 expect(buildCount, greaterThan(initialBuildCount)); 331 }); 332 }); 333}