feat(ui): booster bar targeting in the game screen

Mount BoosterBar below the tray (only while playing), guarded so legacy
GameScreen tests without a SaveRepository keep passing. Tapping a booster
arms targeting: shuffle applies immediately; hammer/line-bomb arm a board
tap (hammer clears a cell, line-bomb opens a row/column chooser). An empty
booster opens a get-one dialog (ad grant lands in Task 15).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 12:40:13 +09:00
parent a04bb3b847
commit 1ba30028b5
2 changed files with 229 additions and 4 deletions
+81
View File
@@ -0,0 +1,81 @@
// Widget tests for the booster bar mounted in the game screen.
//
// Board-geometry taps (hammer / line-bomb cell selection) are NOT covered here:
// the board is laid out inside an Expanded/Center whose pixel geometry is not
// deterministic in a widget test, so a pixel-accurate cell tap is unreliable.
// Those paths are left to manual QA (see the agent report). What we CAN verify
// deterministically is the inventory side-effect: shuffle applies immediately
// and spends one booster.
import 'package:block_seasons/core/rng.dart';
import 'package:block_seasons/data/save_repository.dart';
import 'package:block_seasons/game/engine/piece_generator.dart';
import 'package:block_seasons/game/models/booster.dart';
import 'package:block_seasons/game/models/stage.dart';
import 'package:block_seasons/l10n/gen/app_localizations.dart';
import 'package:block_seasons/state/providers.dart';
import 'package:block_seasons/ui/screens/game_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
StageConfig _stage() => StageConfig.fromJson({
'id': 'ui_b',
'seed': 1,
'moveLimit': 20,
'preset': [
{'x': 0, 'y': 0, 't': 'filled', 'c': 3},
],
'objectives': [
{'type': 'reachScore', 'target': 100000},
],
'stars': {
'two': {'movesLeft': 5},
'three': {'movesLeft': 10},
},
});
Widget _wrap(ProviderContainer c) => UncontrolledProviderScope(
container: c,
child: MaterialApp(
debugShowCheckedModeBanner: false,
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const GameScreen(),
),
);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
Future<ProviderContainer> startedContainer() async {
SharedPreferences.setMockInitialValues({});
final repo = SaveRepository(await SharedPreferences.getInstance());
final c = ProviderContainer(overrides: [
saveRepositoryProvider.overrideWithValue(repo),
]);
c.read(gameSessionProvider.notifier).startStage(
_stage(),
generator: PieceGenerator(SeededRng(1)),
);
return c;
}
testWidgets('tapping shuffle applies immediately and spends one booster',
(tester) async {
final c = await startedContainer();
await c.read(boosterInventoryProvider.notifier).grant(BoosterType.shuffle);
expect(c.read(boosterInventoryProvider)[BoosterType.shuffle], 1);
await tester.pumpWidget(_wrap(c));
await tester.pump();
await tester.tap(find.byKey(const ValueKey('booster_shuffle')));
await tester.pump();
await tester.pump();
expect(c.read(boosterInventoryProvider)[BoosterType.shuffle], 0);
c.dispose();
});
}