Skip to content

Commit 0d9152c

Browse files
committed
Add dart theme screen test
1 parent be2a302 commit 0d9152c

File tree

1 file changed

+374
-0
lines changed

1 file changed

+374
-0
lines changed
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_bloc_app_template/bloc/theme/app_theme.dart';
5+
import 'package:flutter_bloc_app_template/bloc/theme/theme_cubit.dart';
6+
import 'package:flutter_bloc_app_template/features/appearance/dark_theme_screen.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
import 'package:mocktail/mocktail.dart';
9+
10+
import '../../bloc/utils.dart';
11+
12+
class MockThemeCubit extends Mock implements ThemeCubit {}
13+
14+
class MockAppThemeSettings extends Mock implements AppThemeSettings {}
15+
16+
class MockDarkTheme extends Mock implements DarkThemePreference {}
17+
18+
class MockBuildContext extends Mock implements BuildContext {}
19+
20+
void main() {
21+
late MockThemeCubit mockThemeCubit;
22+
late MockAppThemeSettings mockThemeSettings;
23+
late MockDarkTheme mockDarkTheme;
24+
25+
setUpAll(() {
26+
registerFallbackValue(AppThemeSettings(
27+
darkTheme: DarkThemePreference(), appTheme: AppTheme.system));
28+
});
29+
30+
setUp(() {
31+
mockThemeCubit = MockThemeCubit();
32+
mockThemeSettings = MockAppThemeSettings();
33+
mockDarkTheme = MockDarkTheme();
34+
35+
// Setup default mock behaviors
36+
when(() => mockThemeSettings.darkTheme).thenReturn(mockDarkTheme);
37+
when(() => mockDarkTheme.darkThemeValue)
38+
.thenReturn(DarkThemePreference.followSystem);
39+
when(() => mockDarkTheme.copyWith(
40+
darkThemeValue: any(named: 'darkThemeValue')))
41+
.thenReturn(mockDarkTheme);
42+
when(() => mockThemeSettings.copyWith(darkTheme: any(named: 'darkTheme')))
43+
.thenReturn(mockThemeSettings);
44+
when(() => mockThemeCubit.state).thenReturn(mockThemeSettings);
45+
when(() => mockThemeCubit.stream)
46+
.thenAnswer((_) => Stream.value(mockThemeSettings));
47+
when(() => mockThemeCubit.updateTheme(any())).thenReturn(null);
48+
});
49+
50+
group('DarkThemeScreen', () {
51+
testWidgets('renders correctly with all theme options', (tester) async {
52+
await tester.pumpLocalizedWidgetWithBloc<ThemeCubit>(
53+
bloc: mockThemeCubit,
54+
child: const DarkThemeScreen(),
55+
locale: const Locale('en'),
56+
);
57+
58+
await tester.pump();
59+
60+
// Verify app bar
61+
expect(find.text('Dark Theme'), findsOneWidget);
62+
63+
// Verify all radio options are present
64+
expect(find.text('Dark'), findsOneWidget);
65+
expect(find.text('Light'), findsOneWidget);
66+
expect(find.text('System default'), findsOneWidget);
67+
68+
// Verify radio tiles
69+
expect(find.byType(RadioListTile<int>), findsNWidgets(3));
70+
});
71+
72+
testWidgets('displays correct icons for each theme option', (tester) async {
73+
await tester.pumpLocalizedWidgetWithBloc<ThemeCubit>(
74+
bloc: mockThemeCubit,
75+
child: const DarkThemeScreen(),
76+
locale: const Locale('en'),
77+
);
78+
79+
await tester.pump();
80+
81+
// Verify icons are present
82+
expect(find.byIcon(Icons.dark_mode_outlined), findsOneWidget);
83+
expect(find.byIcon(Icons.light_mode_outlined), findsOneWidget);
84+
expect(find.byIcon(Icons.settings_outlined), findsOneWidget);
85+
});
86+
87+
testWidgets('shows correct selected state for "Light" preference',
88+
(tester) async {
89+
await tester.pumpLocalizedWidgetWithBloc<ThemeCubit>(
90+
bloc: mockThemeCubit,
91+
child: const DarkThemeScreen(),
92+
locale: const Locale('en'),
93+
);
94+
95+
await tester.pump();
96+
97+
when(() => mockDarkTheme.darkThemeValue)
98+
.thenReturn(DarkThemePreference.on);
99+
100+
// Find the RadioListTile with "On" value
101+
final radioListTile = tester.widget<RadioListTile<int>>(
102+
find.byKey(const Key('Light')),
103+
);
104+
105+
expect(radioListTile.value, DarkThemePreference.off);
106+
});
107+
108+
testWidgets('shows correct selected state for "Off" preference',
109+
(tester) async {
110+
await tester.pumpLocalizedWidgetWithBloc<ThemeCubit>(
111+
bloc: mockThemeCubit,
112+
child: const DarkThemeScreen(),
113+
locale: const Locale('en'),
114+
);
115+
116+
await tester.pump();
117+
118+
when(() => mockDarkTheme.darkThemeValue)
119+
.thenReturn(DarkThemePreference.off);
120+
121+
final radioListTile = tester.widget<RadioListTile<int>>(
122+
find.byKey(const Key('Dark')),
123+
);
124+
125+
expect(radioListTile.value, DarkThemePreference.on);
126+
});
127+
128+
testWidgets('shows correct selected state for "System default" preference',
129+
(tester) async {
130+
await tester.pumpLocalizedWidgetWithBloc<ThemeCubit>(
131+
bloc: mockThemeCubit,
132+
child: const DarkThemeScreen(),
133+
locale: const Locale('en'),
134+
);
135+
136+
await tester.pump();
137+
138+
when(() => mockDarkTheme.darkThemeValue)
139+
.thenReturn(DarkThemePreference.followSystem);
140+
141+
final radioListTile = tester.widget<RadioListTile<int>>(
142+
find.byKey(const Key('System default')),
143+
);
144+
145+
expect(radioListTile.value, DarkThemePreference.followSystem);
146+
});
147+
148+
testWidgets('calls updateTheme when "Light" is selected', (tester) async {
149+
await tester.pumpLocalizedWidgetWithBloc<ThemeCubit>(
150+
bloc: mockThemeCubit,
151+
child: const DarkThemeScreen(),
152+
locale: const Locale('en'),
153+
);
154+
155+
await tester.pump();
156+
157+
await tester.tap(find.text('Light'));
158+
await tester.pump();
159+
160+
verify(() => mockThemeCubit.updateTheme(any())).called(1);
161+
});
162+
163+
testWidgets('calls updateTheme when "Off" is selected', (tester) async {
164+
await tester.pumpLocalizedWidgetWithBloc<ThemeCubit>(
165+
bloc: mockThemeCubit,
166+
child: const DarkThemeScreen(),
167+
locale: const Locale('en'),
168+
);
169+
170+
await tester.pump();
171+
172+
await tester.tap(find.text('Dark'));
173+
await tester.pump();
174+
175+
verify(() => mockThemeCubit.updateTheme(any())).called(1);
176+
});
177+
});
178+
179+
group('ThemeIcon', () {
180+
testWidgets('renders with provided icon', (tester) async {
181+
await tester.pumpWidget(
182+
const MaterialApp(
183+
home: Scaffold(
184+
body: ThemeIcon(
185+
icon: Icons.dark_mode_outlined,
186+
isSelected: false,
187+
),
188+
),
189+
),
190+
);
191+
192+
expect(find.byIcon(Icons.dark_mode_outlined), findsOneWidget);
193+
});
194+
195+
testWidgets('animates rotation when selected changes to true',
196+
(tester) async {
197+
await tester.pumpWidget(
198+
const MaterialApp(
199+
home: Scaffold(
200+
body: ThemeIcon(
201+
icon: Icons.dark_mode_outlined,
202+
isSelected: false,
203+
),
204+
),
205+
),
206+
);
207+
208+
// Find the AnimatedRotation widget
209+
var animatedRotation = tester.widget<AnimatedRotation>(
210+
find.byType(AnimatedRotation),
211+
);
212+
expect(animatedRotation.turns, 0);
213+
214+
// Update to selected
215+
await tester.pumpWidget(
216+
const MaterialApp(
217+
home: Scaffold(
218+
body: ThemeIcon(
219+
icon: Icons.dark_mode_outlined,
220+
isSelected: true,
221+
),
222+
),
223+
),
224+
);
225+
226+
// Animation should start
227+
animatedRotation = tester.widget<AnimatedRotation>(
228+
find.byType(AnimatedRotation),
229+
);
230+
expect(animatedRotation.turns, 1);
231+
232+
// Pump to complete animation
233+
await tester.pumpAndSettle();
234+
});
235+
236+
testWidgets('does not animate when isSelected is false', (tester) async {
237+
await tester.pumpWidget(
238+
const MaterialApp(
239+
home: Scaffold(
240+
body: ThemeIcon(
241+
icon: Icons.dark_mode_outlined,
242+
isSelected: false,
243+
),
244+
),
245+
),
246+
);
247+
248+
var animatedRotation = tester.widget<AnimatedRotation>(
249+
find.byType(AnimatedRotation),
250+
);
251+
expect(animatedRotation.turns, 0);
252+
253+
// Update icon but keep isSelected false
254+
await tester.pumpWidget(
255+
const MaterialApp(
256+
home: Scaffold(
257+
body: ThemeIcon(
258+
icon: Icons.light_mode_outlined,
259+
isSelected: false,
260+
),
261+
),
262+
),
263+
);
264+
265+
animatedRotation = tester.widget<AnimatedRotation>(
266+
find.byType(AnimatedRotation),
267+
);
268+
expect(animatedRotation.turns, 0);
269+
});
270+
271+
testWidgets('accumulates turns on multiple selections', (tester) async {
272+
await tester.pumpWidget(
273+
const MaterialApp(
274+
home: Scaffold(
275+
body: ThemeIcon(
276+
icon: Icons.dark_mode_outlined,
277+
isSelected: false,
278+
),
279+
),
280+
),
281+
);
282+
283+
// First selection
284+
await tester.pumpWidget(
285+
const MaterialApp(
286+
home: Scaffold(
287+
body: ThemeIcon(
288+
icon: Icons.dark_mode_outlined,
289+
isSelected: true,
290+
),
291+
),
292+
),
293+
);
294+
295+
var animatedRotation = tester.widget<AnimatedRotation>(
296+
find.byType(AnimatedRotation),
297+
);
298+
expect(animatedRotation.turns, 1);
299+
300+
// Deselect
301+
await tester.pumpWidget(
302+
const MaterialApp(
303+
home: Scaffold(
304+
body: ThemeIcon(
305+
icon: Icons.dark_mode_outlined,
306+
isSelected: false,
307+
),
308+
),
309+
),
310+
);
311+
312+
// Second selection
313+
await tester.pumpWidget(
314+
const MaterialApp(
315+
home: Scaffold(
316+
body: ThemeIcon(
317+
icon: Icons.light_mode_outlined,
318+
isSelected: true,
319+
),
320+
),
321+
),
322+
);
323+
324+
animatedRotation = tester.widget<AnimatedRotation>(
325+
find.byType(AnimatedRotation),
326+
);
327+
expect(animatedRotation.turns, 2);
328+
});
329+
330+
testWidgets('animation duration is 600ms', (tester) async {
331+
await tester.pumpWidget(
332+
const MaterialApp(
333+
home: Scaffold(
334+
body: ThemeIcon(
335+
icon: Icons.dark_mode_outlined,
336+
isSelected: false,
337+
),
338+
),
339+
),
340+
);
341+
342+
final animatedRotation = tester.widget<AnimatedRotation>(
343+
find.byType(AnimatedRotation),
344+
);
345+
346+
expect(animatedRotation.duration, const Duration(milliseconds: 600));
347+
});
348+
});
349+
350+
group('DarkThemeScreen.themeIcon', () {
351+
test('returns correct icon for off preference', () {
352+
const screen = DarkThemeScreen();
353+
expect(
354+
screen.themeIcon(DarkThemePreference.off), Icons.light_mode_outlined);
355+
});
356+
357+
test('returns correct icon for on preference', () {
358+
const screen = DarkThemeScreen();
359+
expect(
360+
screen.themeIcon(DarkThemePreference.on), Icons.dark_mode_outlined);
361+
});
362+
363+
test('returns correct icon for System default preference', () {
364+
const screen = DarkThemeScreen();
365+
expect(screen.themeIcon(DarkThemePreference.followSystem),
366+
Icons.settings_outlined);
367+
});
368+
369+
test('returns settings icon for unknown preference', () {
370+
const screen = DarkThemeScreen();
371+
expect(screen.themeIcon(999), Icons.settings_outlined);
372+
});
373+
});
374+
}

0 commit comments

Comments
 (0)