feat: tutorial step state machine with persistence

Adds TutorialNotifier (dragPiece → clearLine → explainHud → null) backed
by SaveRepository.markTutorialDone(); out-of-order events are silently
ignored. Registers tutorialProvider in providers.dart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 22:50:49 +09:00
parent f97b4faad7
commit 3d1f3b30c7
3 changed files with 105 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
import 'package:block_seasons/data/save_repository.dart';
import 'package:block_seasons/state/providers.dart';
import 'package:block_seasons/state/tutorial_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
late ProviderContainer container;
late SaveRepository repo;
setUp(() async {
SharedPreferences.setMockInitialValues({});
repo = SaveRepository(await SharedPreferences.getInstance());
container = ProviderContainer(
overrides: [saveRepositoryProvider.overrideWithValue(repo)],
);
});
tearDown(() => container.dispose());
test('inactive by default', () {
expect(container.read(tutorialProvider), isNull);
});
test('happy path: drag -> clear -> hud -> done, persists flag', () async {
final n = container.read(tutorialProvider.notifier);
n.start();
expect(container.read(tutorialProvider), TutorialStep.dragPiece);
n.onPlaced();
expect(container.read(tutorialProvider), TutorialStep.clearLine);
n.onLineCleared();
expect(container.read(tutorialProvider), TutorialStep.explainHud);
await n.dismissHud();
expect(container.read(tutorialProvider), isNull);
expect(repo.tutorialDone, isTrue);
});
test('out-of-order events are ignored', () {
final n = container.read(tutorialProvider.notifier);
n.start();
n.onLineCleared(); // not in clearLine step yet
expect(container.read(tutorialProvider), TutorialStep.dragPiece);
});
test('skip finishes from any step and persists', () async {
final n = container.read(tutorialProvider.notifier);
n.start();
await n.skip();
expect(container.read(tutorialProvider), isNull);
expect(repo.tutorialDone, isTrue);
});
test('start is a no-op when tutorial already done', () async {
await repo.markTutorialDone();
final n = container.read(tutorialProvider.notifier);
n.start();
expect(container.read(tutorialProvider), isNull);
});
}