// 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-effects: // - shuffle applies immediately and spends one booster // - tapping an empty booster shows the ad dialog whose confirm grants +1 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/services/ad_service.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}, }, }); /// A real, uninitialized [AdService]: with no SDK loaded its [showRewarded] /// takes the "no ad available -> grant the reward" path and resolves true /// without touching the platform — exactly the rewarded-earn case we want. AdService _earnAd() => AdService(adsRemoved: () => true); 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 startedContainer({AdService? ad}) async { SharedPreferences.setMockInitialValues({}); final repo = SaveRepository(await SharedPreferences.getInstance()); final c = ProviderContainer(overrides: [ saveRepositoryProvider.overrideWithValue(repo), if (ad != null) adServiceProvider.overrideWithValue(ad), ]); 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(); }); testWidgets('tapping an empty booster offers an ad that grants +1', (tester) async { final c = await startedContainer(ad: _earnAd()); expect(c.read(boosterInventoryProvider)[BoosterType.hammer], 0); await tester.pumpWidget(_wrap(c)); await tester.pump(); await tester.tap(find.byKey(const ValueKey('booster_hammer'))); await tester.pumpAndSettle(); // The dialog offers the "watch an ad to get one" action; confirm it. final l10n = AppLocalizations.of( tester.element(find.byType(GameScreen)), )!; await tester.tap(find.text(l10n.boosterGetWithAd).last); await tester.pump(); await tester.pump(); expect(c.read(boosterInventoryProvider)[BoosterType.hammer], 1); c.dispose(); }); }