Skip to content

Commit f4f4d47

Browse files
committed
Add more comprehensive tests
1 parent a7787cc commit f4f4d47

12 files changed

Lines changed: 3372 additions & 309 deletions
Lines changed: 238 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,10 @@
11
import 'package:aimi_app/services/anime_service.dart';
2-
import 'package:aimi_app/services/caching_service.dart';
3-
import 'package:aimi_lib/aimi_lib.dart' as lib;
42
import 'package:flutter_test/flutter_test.dart';
53

6-
class FakeMetadataProvider implements lib.IMetadataProvider {
7-
final String _name;
8-
final Map<int, lib.Media> _animeMap = {};
9-
10-
FakeMetadataProvider(this._name);
11-
12-
void addAnime(lib.Media anime) {
13-
_animeMap[anime.id] = anime;
14-
}
15-
16-
@override
17-
String get name => _name;
18-
19-
@override
20-
String get version => '1.0.0';
21-
22-
@override
23-
Future<lib.Media> fetchAnimeById(int id) async {
24-
if (_animeMap.containsKey(id)) {
25-
return _animeMap[id]!;
26-
}
27-
throw Exception('Anime $id not found in $_name');
28-
}
29-
30-
@override
31-
Future<List<lib.Media>> fetchTrending({int page = 1}) async {
32-
return _animeMap.values.toList();
33-
}
34-
35-
@override
36-
Future<List<lib.Media>> searchAnime(String query, {int page = 1}) async {
37-
return _animeMap.values
38-
.where((a) => a.title.english?.contains(query) ?? false)
39-
.toList();
40-
}
41-
42-
@override
43-
void dispose() {}
44-
}
45-
46-
class FakeCachingService extends CachingService {
47-
@override
48-
Future<void> saveData({
49-
CacheKey? cacheKey,
50-
String? dynamicKey,
51-
required dynamic data,
52-
Duration? expiresIn,
53-
String? providerName,
54-
}) async {}
55-
56-
@override
57-
Future<dynamic> getData({
58-
CacheKey? cacheKey,
59-
String? dynamicKey,
60-
String? providerName,
61-
}) async {
62-
return null;
63-
}
64-
}
4+
import 'test_helpers.dart';
655

666
void main() {
67-
group('AnimeService Multi-Provider', () {
7+
group('AnimeService', () {
688
late AnimeService service;
699
late FakeMetadataProvider providerA;
7010
late FakeMetadataProvider providerB;
@@ -75,66 +15,256 @@ void main() {
7515
providerB = FakeMetadataProvider('ProviderB');
7616
cachingService = FakeCachingService();
7717

78-
// Add dummy data
18+
// Add test data
19+
providerA.addAnime(TestAnimeFactory.createMedia(id: 1, englishTitle: 'Anime A', romajiTitle: 'Anime A Romaji'));
7920
providerA.addAnime(
80-
lib.Media(
81-
id: 1,
82-
title: lib.AnimeTitle(english: 'Anime A', native: ''),
83-
type: 'TV',
84-
status: 'FINISHED',
85-
description: '',
86-
countryOfOrigin: 'JP',
87-
updatedAt: 0,
88-
coverImage: lib.CoverImage(extraLarge: '', large: ''),
89-
siteUrl: '',
90-
),
21+
TestAnimeFactory.createMedia(id: 2, englishTitle: 'Another Anime A', romajiTitle: 'Another A Romaji'),
9122
);
9223

93-
providerB.addAnime(
94-
lib.Media(
95-
id: 1, // Same ID but different content/context
96-
title: lib.AnimeTitle(english: 'Anime B', native: ''),
97-
type: 'TV',
98-
status: 'FINISHED',
99-
description: '',
100-
countryOfOrigin: 'JP',
101-
updatedAt: 0,
102-
coverImage: lib.CoverImage(extraLarge: '', large: ''),
103-
siteUrl: '',
104-
),
105-
);
24+
providerB.addAnime(TestAnimeFactory.createMedia(id: 1, englishTitle: 'Anime B', romajiTitle: 'Anime B Romaji'));
10625

107-
service = AnimeService(
108-
[providerA, providerB],
109-
cachingService,
110-
defaultProviderName: 'ProviderA',
111-
);
26+
service = AnimeService([providerA, providerB], cachingService, defaultProviderName: 'ProviderA');
11227
});
11328

114-
test('getById uses default provider when no name specified', () async {
115-
final anime = await service.getById(1);
116-
expect(anime.title.english, 'Anime A');
29+
tearDown(() {
30+
cachingService.clear();
11731
});
11832

119-
test('getById uses specified provider', () async {
120-
final anime = await service.getById(1, providerName: 'ProviderB');
121-
expect(anime.title.english, 'Anime B');
33+
// =========================================================================
34+
// Construction Tests
35+
// =========================================================================
36+
group('Construction', () {
37+
test('throws if no providers are provided', () {
38+
expect(
39+
() => AnimeService([], cachingService),
40+
throwsA(isA<Exception>().having((e) => e.toString(), 'message', contains('At least one provider'))),
41+
);
42+
});
43+
44+
test('throws if default provider not found', () {
45+
expect(
46+
() => AnimeService([providerA], cachingService, defaultProviderName: 'NonExistent'),
47+
throwsA(isA<Exception>().having((e) => e.toString(), 'message', contains('Default provider'))),
48+
);
49+
});
50+
51+
test('uses first provider as default when not specified', () {
52+
final svc = AnimeService([providerB, providerA], cachingService);
53+
expect(svc.providerName, 'ProviderB');
54+
});
12255
});
12356

124-
test('getById throws if provider not found', () async {
125-
expect(
126-
() => service.getById(1, providerName: 'UnknownProvider'),
127-
throwsException,
128-
);
57+
// =========================================================================
58+
// Provider Selection Tests
59+
// =========================================================================
60+
group('Provider Selection', () {
61+
test('providerName returns default provider name', () {
62+
expect(service.providerName, 'ProviderA');
63+
});
64+
65+
test('getById uses default provider when no name specified', () async {
66+
final anime = await service.getById(1);
67+
expect(anime.title.english, 'Anime A');
68+
});
69+
70+
test('getById uses specified provider', () async {
71+
final anime = await service.getById(1, providerName: 'ProviderB');
72+
expect(anime.title.english, 'Anime B');
73+
});
74+
75+
test('getById throws if provider not found', () async {
76+
expect(() => service.getById(1, providerName: 'UnknownProvider'), throwsException);
77+
});
78+
});
79+
80+
// =========================================================================
81+
// Caching Tests
82+
// =========================================================================
83+
group('Caching', () {
84+
test('fetchTrending caches results', () async {
85+
// First call - should fetch from provider
86+
await service.fetchTrending();
87+
88+
// Verify data was cached
89+
expect(cachingService.containsKey('trendingAnime', providerName: 'ProviderA'), isTrue);
90+
});
91+
92+
test('fetchTrending returns cached data on subsequent calls', () async {
93+
// First call
94+
final first = await service.fetchTrending();
95+
96+
// Modify provider data
97+
providerA.addAnime(TestAnimeFactory.createMedia(id: 99, englishTitle: 'New Anime'));
98+
99+
// Second call should return cached data (no new anime)
100+
final second = await service.fetchTrending();
101+
102+
expect(first.length, second.length);
103+
});
104+
105+
test('fetchTrending forceRefresh bypasses cache', () async {
106+
// First call
107+
await service.fetchTrending();
108+
109+
// Modify provider data
110+
providerA.addAnime(TestAnimeFactory.createMedia(id: 99, englishTitle: 'New Anime'));
111+
112+
// Force refresh should get new data
113+
final refreshed = await service.fetchTrending(forceRefresh: true);
114+
expect(refreshed.any((a) => a.id == 99), isTrue);
115+
});
116+
117+
test('getById caches individual anime', () async {
118+
await service.getById(1);
119+
120+
expect(cachingService.containsKey('anime_details/1', providerName: 'ProviderA'), isTrue);
121+
});
122+
123+
test('getById forceRefresh bypasses cache', () async {
124+
// First call
125+
await service.getById(1);
126+
127+
// Second call with forceRefresh
128+
final refreshed = await service.getById(1, forceRefresh: true);
129+
expect(refreshed.title.english, 'Anime A');
130+
});
131+
});
132+
133+
// =========================================================================
134+
// Pagination Tests
135+
// =========================================================================
136+
group('Pagination', () {
137+
test('fetchTrending page 2 appends to cache', () async {
138+
// First page
139+
await service.fetchTrending(page: 1);
140+
141+
// Simulate provider returning different data for page 2
142+
providerA.setAnimeList([TestAnimeFactory.createMedia(id: 3, englishTitle: 'Page 2 Anime')]);
143+
144+
// Second page
145+
await service.fetchTrending(page: 2, forceRefresh: true);
146+
147+
// Verify page 2 data was fetched
148+
// (In real scenario, cache would be appended)
149+
});
129150
});
130151

131-
test('fetchTrending uses default provider', () async {
132-
final trending = await service.fetchTrending();
133-
expect(trending.first.title.english, 'Anime A');
152+
// =========================================================================
153+
// Search Tests
154+
// =========================================================================
155+
group('Search', () {
156+
test('search returns matching anime', () async {
157+
final results = await service.search('Anime A');
158+
expect(results, isNotEmpty);
159+
expect(results.first.title.english, contains('Anime A'));
160+
});
161+
162+
test('search returns empty list for no matches', () async {
163+
final results = await service.search('NonExistent');
164+
expect(results, isEmpty);
165+
});
166+
167+
test('search is case insensitive', () async {
168+
final results = await service.search('anime a');
169+
expect(results, isNotEmpty);
170+
});
134171
});
135172

136-
test('providerName returns default provider name', () {
137-
expect(service.providerName, 'ProviderA');
173+
// =========================================================================
174+
// Search History Tests
175+
// =========================================================================
176+
group('Search History', () {
177+
test('getSearchHistory returns empty list initially', () async {
178+
final history = await service.getSearchHistory();
179+
expect(history, isEmpty);
180+
});
181+
182+
test('addToSearchHistory adds query to history', () async {
183+
await service.addToSearchHistory('test query');
184+
185+
final history = await service.getSearchHistory();
186+
expect(history, contains('test query'));
187+
});
188+
189+
test('addToSearchHistory moves duplicate to top', () async {
190+
await service.addToSearchHistory('first');
191+
await service.addToSearchHistory('second');
192+
await service.addToSearchHistory('first'); // Should move to top
193+
194+
final history = await service.getSearchHistory();
195+
expect(history.first, 'first');
196+
expect(history.length, 2);
197+
});
198+
199+
test('addToSearchHistory limits to 20 items', () async {
200+
for (int i = 0; i < 25; i++) {
201+
await service.addToSearchHistory('query $i');
202+
}
203+
204+
final history = await service.getSearchHistory();
205+
expect(history.length, 20);
206+
expect(history.first, 'query 24'); // Most recent
207+
});
208+
209+
test('addToSearchHistory ignores empty queries', () async {
210+
await service.addToSearchHistory('');
211+
await service.addToSearchHistory(' ');
212+
213+
final history = await service.getSearchHistory();
214+
expect(history, isEmpty);
215+
});
216+
217+
test('removeFromSearchHistory removes specific query', () async {
218+
await service.addToSearchHistory('keep');
219+
await service.addToSearchHistory('remove');
220+
221+
await service.removeFromSearchHistory('remove');
222+
223+
final history = await service.getSearchHistory();
224+
expect(history, contains('keep'));
225+
expect(history, isNot(contains('remove')));
226+
});
227+
228+
test('clearSearchHistory removes all history', () async {
229+
await service.addToSearchHistory('one');
230+
await service.addToSearchHistory('two');
231+
232+
await service.clearSearchHistory();
233+
234+
final history = await service.getSearchHistory();
235+
expect(history, isEmpty);
236+
});
237+
});
238+
239+
// =========================================================================
240+
// Error Handling Tests
241+
// =========================================================================
242+
group('Error Handling', () {
243+
test('getById throws when anime not found', () async {
244+
expect(() => service.getById(999), throwsException);
245+
});
246+
247+
test('getById propagates provider errors', () async {
248+
providerA.shouldThrow = true;
249+
providerA.errorMessage = 'Provider error';
250+
251+
expect(
252+
() => service.getById(1),
253+
throwsA(isA<Exception>().having((e) => e.toString(), 'message', contains('Provider error'))),
254+
);
255+
});
256+
257+
test('fetchTrending propagates provider errors', () async {
258+
providerA.shouldThrow = true;
259+
260+
expect(() => service.fetchTrending(forceRefresh: true), throwsException);
261+
});
262+
263+
test('search propagates provider errors', () async {
264+
providerA.shouldThrow = true;
265+
266+
expect(() => service.search('test'), throwsException);
267+
});
138268
});
139269
});
140270
}

0 commit comments

Comments
 (0)