e722fe2ce1
Extends ContentRepository with optional cacheDir: cached packs in <cacheDir>/seasons/*/pack.json merge with bundled ones (cached wins for same id), corrupt/future-schema packs silently ignored, refresh() fires ContentDownloader.sync() once per session. main() wires the real cache+downloader instance; default constructor stays bundled-only for tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
104 lines
3.5 KiB
Dart
104 lines
3.5 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:block_seasons/data/content_repository.dart';
|
|
import 'package:block_seasons/game/models/season.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
test('loads the bundled Season 1 pack with 60 calibrated stages', () async {
|
|
final repo = ContentRepository();
|
|
final pack = await repo.loadBundledSeason('season_001');
|
|
expect(pack.seasonId, 'season_001');
|
|
expect(pack.stages, hasLength(60));
|
|
expect(pack.titleFor('ko'), '첫 개화');
|
|
|
|
// Sanity: every stage has a positive budget and at least one objective.
|
|
for (final stage in pack.stages) {
|
|
expect(stage.moveLimit, greaterThan(0), reason: stage.id);
|
|
expect(stage.objectives, isNotEmpty, reason: stage.id);
|
|
expect(stage.stars.threeMovesLeft, greaterThan(stage.stars.twoMovesLeft),
|
|
reason: stage.id);
|
|
}
|
|
});
|
|
|
|
test('availableSeasons returns the bundled season', () async {
|
|
final repo = ContentRepository();
|
|
final seasons = await repo.availableSeasons();
|
|
expect(seasons.map((s) => s.seasonId), contains('season_001'));
|
|
});
|
|
|
|
group('cache merge and fallback', () {
|
|
late Directory tmp;
|
|
|
|
setUp(() async {
|
|
tmp = await Directory.systemTemp.createTemp('bs_repo_');
|
|
});
|
|
|
|
tearDown(() async {
|
|
if (await tmp.exists()) await tmp.delete(recursive: true);
|
|
});
|
|
|
|
Map<String, dynamic> packJson(String id) => {
|
|
'schemaVersion': 1,
|
|
'seasonId': id,
|
|
'version': 1,
|
|
'title': {'en': id},
|
|
'theme': const SeasonTheme().toJson(),
|
|
'stages': [
|
|
{
|
|
'id': '${id}_001',
|
|
'seed': 1,
|
|
'moveLimit': 10,
|
|
'preset': [],
|
|
'objectives': [
|
|
{'type': 'reachScore', 'target': 100}
|
|
],
|
|
'stars': {
|
|
'two': {'movesLeft': 2},
|
|
'three': {'movesLeft': 4}
|
|
},
|
|
'generatorProfile': 'mid',
|
|
}
|
|
],
|
|
};
|
|
|
|
Future<void> putCachedSeason(String id) async {
|
|
final dir = Directory('${tmp.path}/seasons/$id');
|
|
await dir.create(recursive: true);
|
|
await File('${dir.path}/pack.json')
|
|
.writeAsString(jsonEncode(packJson(id)));
|
|
}
|
|
|
|
test('merges cached seasons after bundled ones, sorted by id', () async {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
await putCachedSeason('season_002');
|
|
final repo = ContentRepository(cacheDir: tmp);
|
|
final seasons = await repo.availableSeasons();
|
|
expect(seasons.map((s) => s.seasonId).toList(),
|
|
['season_001', 'season_002']);
|
|
});
|
|
|
|
test('cached copy of a bundled season wins over the bundle', () async {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
await putCachedSeason('season_001');
|
|
final repo = ContentRepository(cacheDir: tmp);
|
|
final seasons = await repo.availableSeasons();
|
|
// Cached fake has exactly 1 stage; the bundled real one has 60.
|
|
expect(seasons.single.stages, hasLength(1));
|
|
});
|
|
|
|
test('corrupt cached pack falls back to bundle silently', () async {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
final dir = Directory('${tmp.path}/seasons/season_001');
|
|
await dir.create(recursive: true);
|
|
await File('${dir.path}/pack.json').writeAsString('{not json');
|
|
final repo = ContentRepository(cacheDir: tmp);
|
|
final seasons = await repo.availableSeasons();
|
|
expect(seasons.single.stages, hasLength(60));
|
|
});
|
|
});
|
|
}
|