Add daily streak system and normalize bundle id to com.airkjw.blockseasons
Pure advanceStreak (1-day grace none, milestone flags at 3/7/14/30), persisted in the save blob; StreakNotifier advances on every finished attempt; home screen flame chip and milestone snackbar. Fix flutter create's camelCased iOS bundle id and underscored Android application id to the agreed lowercase form before any store registration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import 'package:block_seasons/data/save_repository.dart';
|
||||
import 'package:block_seasons/data/streak.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('advanceStreak', () {
|
||||
test('first ever play starts the streak at 1', () {
|
||||
final next = advanceStreak(StreakState.initial, DateTime(2026, 6, 11));
|
||||
expect(next.current, 1);
|
||||
expect(next.best, 1);
|
||||
expect(next.lastYmd, '2026-06-11');
|
||||
});
|
||||
|
||||
test('same-day repeat play changes nothing', () {
|
||||
final day1 = advanceStreak(StreakState.initial, DateTime(2026, 6, 11));
|
||||
final again = advanceStreak(day1, DateTime(2026, 6, 11, 23, 59));
|
||||
expect(again.current, 1);
|
||||
expect(again.best, 1);
|
||||
});
|
||||
|
||||
test('consecutive days grow the streak and best follows', () {
|
||||
var s = advanceStreak(StreakState.initial, DateTime(2026, 6, 11));
|
||||
s = advanceStreak(s, DateTime(2026, 6, 12));
|
||||
s = advanceStreak(s, DateTime(2026, 6, 13));
|
||||
expect(s.current, 3);
|
||||
expect(s.best, 3);
|
||||
});
|
||||
|
||||
test('missing a day resets current but keeps best', () {
|
||||
var s = advanceStreak(StreakState.initial, DateTime(2026, 6, 11));
|
||||
s = advanceStreak(s, DateTime(2026, 6, 12));
|
||||
s = advanceStreak(s, DateTime(2026, 6, 15));
|
||||
expect(s.current, 1);
|
||||
expect(s.best, 2);
|
||||
});
|
||||
|
||||
test('month boundaries count as consecutive days', () {
|
||||
var s = advanceStreak(StreakState.initial, DateTime(2026, 6, 30));
|
||||
s = advanceStreak(s, DateTime(2026, 7, 1));
|
||||
expect(s.current, 2);
|
||||
});
|
||||
|
||||
test('milestone crossings are flagged once', () {
|
||||
var s = StreakState(current: 2, best: 6, lastYmd: '2026-06-10');
|
||||
final next = advanceStreak(s, DateTime(2026, 6, 11));
|
||||
expect(next.current, 3);
|
||||
expect(next.hitMilestone, 3);
|
||||
|
||||
s = StreakState(current: 3, best: 6, lastYmd: '2026-06-11');
|
||||
final after = advanceStreak(s, DateTime(2026, 6, 12));
|
||||
expect(after.hitMilestone, isNull, reason: '4 is not a milestone');
|
||||
});
|
||||
});
|
||||
|
||||
group('SaveRepository streak persistence', () {
|
||||
test('round-trips streak state', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final repo = SaveRepository(prefs);
|
||||
expect(repo.streak.current, 0);
|
||||
|
||||
await repo.saveStreak(
|
||||
const StreakState(current: 5, best: 9, lastYmd: '2026-06-11'),
|
||||
);
|
||||
final reloaded = SaveRepository(prefs);
|
||||
expect(reloaded.streak.current, 5);
|
||||
expect(reloaded.streak.best, 9);
|
||||
expect(reloaded.streak.lastYmd, '2026-06-11');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:block_seasons/data/save_repository.dart';
|
||||
import 'package:block_seasons/state/providers.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('onStagePlayed advances, persists, and surfaces milestones', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final repo = SaveRepository(prefs);
|
||||
final container = ProviderContainer(
|
||||
overrides: [saveRepositoryProvider.overrideWithValue(repo)],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final notifier = container.read(streakProvider.notifier);
|
||||
expect(container.read(streakProvider).current, 0);
|
||||
|
||||
await notifier.onStagePlayed(DateTime(2026, 6, 11));
|
||||
await notifier.onStagePlayed(DateTime(2026, 6, 12));
|
||||
await notifier.onStagePlayed(DateTime(2026, 6, 13));
|
||||
|
||||
final state = container.read(streakProvider);
|
||||
expect(state.current, 3);
|
||||
expect(state.hitMilestone, 3);
|
||||
|
||||
// Persisted: a fresh repository over the same prefs sees the streak.
|
||||
expect(SaveRepository(prefs).streak.current, 3);
|
||||
|
||||
// Same-day replays neither grow nor re-flag.
|
||||
await notifier.onStagePlayed(DateTime(2026, 6, 13, 22));
|
||||
expect(container.read(streakProvider).current, 3);
|
||||
expect(container.read(streakProvider).hitMilestone, isNull);
|
||||
});
|
||||
}
|
||||
+14
-3
@@ -1,10 +1,21 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:block_seasons/app.dart';
|
||||
import 'package:block_seasons/data/save_repository.dart';
|
||||
import 'package:block_seasons/state/providers.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('home screen shows title and play button', (tester) async {
|
||||
await tester.pumpWidget(const BlockSeasonsApp());
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final repo = SaveRepository(await SharedPreferences.getInstance());
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [saveRepositoryProvider.overrideWithValue(repo)],
|
||||
child: const BlockSeasonsApp(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Block Seasons'), findsOneWidget);
|
||||
|
||||
Reference in New Issue
Block a user