Main coves client
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}