mirror of https://github.com/flutter/pinball.git
commit
3e2fab00d3
@ -0,0 +1,27 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'assets_manager_state.dart';
|
||||
|
||||
/// {@template assets_manager_cubit}
|
||||
/// Cubit responsable for pre loading any game assets
|
||||
/// {@endtemplate}
|
||||
class AssetsManagerCubit extends Cubit<AssetsManagerState> {
|
||||
/// {@macro assets_manager_cubit}
|
||||
AssetsManagerCubit(List<Future> loadables)
|
||||
: super(
|
||||
AssetsManagerState.initial(
|
||||
loadables: loadables,
|
||||
),
|
||||
);
|
||||
|
||||
/// Loads the assets
|
||||
Future<void> load() async {
|
||||
final all = state.loadables.map((loadable) async {
|
||||
await loadable;
|
||||
emit(state.copyWith(loaded: [...state.loaded, loadable]));
|
||||
}).toList();
|
||||
|
||||
await Future.wait(all);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
part of 'assets_manager_cubit.dart';
|
||||
|
||||
/// {@template assets_manager_state}
|
||||
/// State used to load the game assets
|
||||
/// {@endtemplate}
|
||||
class AssetsManagerState extends Equatable {
|
||||
/// {@macro assets_manager_state}
|
||||
const AssetsManagerState({
|
||||
required this.loadables,
|
||||
required this.loaded,
|
||||
});
|
||||
|
||||
/// {@macro assets_manager_state}
|
||||
const AssetsManagerState.initial({
|
||||
required List<Future> loadables,
|
||||
}) : this(loadables: loadables, loaded: const []);
|
||||
|
||||
/// List of futures to load
|
||||
final List<Future> loadables;
|
||||
|
||||
/// List of loaded futures
|
||||
final List<Future> loaded;
|
||||
|
||||
/// Returns a value between 0 and 1 to indicate the loading progress
|
||||
double get progress => loaded.length / loadables.length;
|
||||
|
||||
/// Returns a copy of this instance with the given parameters
|
||||
/// updated
|
||||
AssetsManagerState copyWith({
|
||||
List<Future>? loadables,
|
||||
List<Future>? loaded,
|
||||
}) {
|
||||
return AssetsManagerState(
|
||||
loadables: loadables ?? this.loadables,
|
||||
loaded: loaded ?? this.loaded,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [loaded, loadables];
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:pinball/flame/flame.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// Adds helpers methods to Flame's [Camera]
|
||||
extension CameraX on Camera {
|
||||
/// Instantly apply the point of focus to the [Camera]
|
||||
void snapToFocus(FocusData data) {
|
||||
followVector2(data.position);
|
||||
zoom = data.zoom;
|
||||
}
|
||||
|
||||
/// Returns a [CameraZoom] that can be added to a [FlameGame]
|
||||
CameraZoom focusToCameraZoom(FocusData data) {
|
||||
final zoom = CameraZoom(value: data.zoom);
|
||||
zoom.completed.then((_) {
|
||||
moveTo(data.position);
|
||||
});
|
||||
return zoom;
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template focus_data}
|
||||
/// Model class that defines a focus point of the camera
|
||||
/// {@endtemplate}
|
||||
class FocusData {
|
||||
/// {@template focus_data}
|
||||
FocusData({
|
||||
required this.zoom,
|
||||
required this.position,
|
||||
});
|
||||
|
||||
/// The amount of zoom
|
||||
final double zoom;
|
||||
|
||||
/// The position of the camera
|
||||
final Vector2 position;
|
||||
}
|
||||
|
||||
/// {@template camera_controller}
|
||||
/// A [Component] that controls its game camera focus
|
||||
/// {@endtemplate}
|
||||
class CameraController extends ComponentController<FlameGame> {
|
||||
/// {@macro camera_controller}
|
||||
CameraController(FlameGame component) : super(component) {
|
||||
final gameZoom = component.size.y / 16;
|
||||
final backboardZoom = component.size.y / 18;
|
||||
|
||||
gameFocus = FocusData(
|
||||
zoom: gameZoom,
|
||||
position: Vector2(0, -7.8),
|
||||
);
|
||||
backboardFocus = FocusData(
|
||||
zoom: backboardZoom,
|
||||
position: Vector2(0, -100.8),
|
||||
);
|
||||
|
||||
// Game starts with the camera focused on the panel
|
||||
component.camera
|
||||
..speed = 100
|
||||
..snapToFocus(backboardFocus);
|
||||
}
|
||||
|
||||
/// Holds the data for the game focus point
|
||||
late final FocusData gameFocus;
|
||||
|
||||
/// Holds the data for the backboard focus point
|
||||
late final FocusData backboardFocus;
|
||||
|
||||
/// Move the camera focus to the game board
|
||||
void focusOnGame() {
|
||||
component.add(component.camera.focusToCameraZoom(gameFocus));
|
||||
}
|
||||
|
||||
/// Move the camera focus to the backboard
|
||||
void focusOnBackboard() {
|
||||
component.add(component.camera.focusToCameraZoom(backboardFocus));
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
export 'board.dart';
|
||||
export 'bonus_word.dart';
|
||||
export 'camera_controller.dart';
|
||||
export 'controlled_ball.dart';
|
||||
export 'controlled_flipper.dart';
|
||||
export 'flutter_forest.dart';
|
||||
export 'game_flow_controller.dart';
|
||||
export 'plunger.dart';
|
||||
export 'score_points.dart';
|
||||
export 'sparky_fire_zone.dart';
|
||||
export 'wall.dart';
|
||||
|
@ -0,0 +1,41 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_bloc/flame_bloc.dart';
|
||||
import 'package:pinball/flame/flame.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// {@template game_flow_controller}
|
||||
/// A [Component] that controls the game over and game restart logic
|
||||
/// {@endtemplate}
|
||||
class GameFlowController extends ComponentController<PinballGame>
|
||||
with BlocComponent<GameBloc, GameState> {
|
||||
/// {@macro game_flow_controller}
|
||||
GameFlowController(PinballGame component) : super(component);
|
||||
|
||||
@override
|
||||
bool listenWhen(GameState? previousState, GameState newState) {
|
||||
return previousState?.isGameOver != newState.isGameOver;
|
||||
}
|
||||
|
||||
@override
|
||||
void onNewState(GameState state) {
|
||||
if (state.isGameOver) {
|
||||
gameOver();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts the game on a game over state
|
||||
void gameOver() {
|
||||
component.firstChild<Backboard>()?.gameOverMode();
|
||||
component.firstChild<CameraController>()?.focusOnBackboard();
|
||||
}
|
||||
|
||||
/// Puts the game on a playing state
|
||||
void start() {
|
||||
component.firstChild<Backboard>()?.waitingMode();
|
||||
component.firstChild<CameraController>()?.focusOnGame();
|
||||
component.overlays.remove(PinballGame.playButtonOverlay);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// ignore_for_file: avoid_renaming_method_parameters
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball/flame/flame.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
// TODO(ruimiguel): create and add SparkyFireZone component here in other PR.
|
||||
|
||||
// TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done
|
||||
// ignore: public_member_api_docs
|
||||
class ControlledSparkyBumper extends SparkyBumper
|
||||
with Controls<_SparkyBumperController> {
|
||||
// TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done
|
||||
// ignore: public_member_api_docs
|
||||
ControlledSparkyBumper() : super.a() {
|
||||
controller = _SparkyBumperController(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template sparky_bumper_controller}
|
||||
/// Controls a [SparkyBumper].
|
||||
/// {@endtemplate}
|
||||
class _SparkyBumperController extends ComponentController<SparkyBumper>
|
||||
with HasGameRef<PinballGame> {
|
||||
/// {@macro sparky_bumper_controller}
|
||||
_SparkyBumperController(ControlledSparkyBumper controlledSparkyBumper)
|
||||
: super(controlledSparkyBumper);
|
||||
|
||||
/// Flag for activated state of the [SparkyBumper].
|
||||
///
|
||||
/// Used to toggle [SparkyBumper]s' state between activated and deactivated.
|
||||
bool isActivated = false;
|
||||
|
||||
/// Registers when a [SparkyBumper] is hit by a [Ball].
|
||||
void hit() {
|
||||
if (isActivated) {
|
||||
component.deactivate();
|
||||
} else {
|
||||
component.activate();
|
||||
}
|
||||
isActivated = !isActivated;
|
||||
}
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball/leaderboard/leaderboard.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
/// {@template game_over_dialog}
|
||||
/// [Dialog] displayed when the [PinballGame] is over.
|
||||
/// {@endtemplate}
|
||||
class GameOverDialog extends StatelessWidget {
|
||||
/// {@macro game_over_dialog}
|
||||
const GameOverDialog({Key? key, required this.score, required this.theme})
|
||||
: super(key: key);
|
||||
|
||||
/// Score achieved by the current user.
|
||||
final int score;
|
||||
|
||||
/// Theme of the current user.
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LeaderboardBloc(
|
||||
context.read<LeaderboardRepository>(),
|
||||
),
|
||||
child: GameOverDialogView(score: score, theme: theme),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template game_over_dialog_view}
|
||||
/// View for showing final score when the game is finished.
|
||||
/// {@endtemplate}
|
||||
@visibleForTesting
|
||||
class GameOverDialogView extends StatefulWidget {
|
||||
/// {@macro game_over_dialog_view}
|
||||
const GameOverDialogView({
|
||||
Key? key,
|
||||
required this.score,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Score achieved by the current user.
|
||||
final int score;
|
||||
|
||||
/// Theme of the current user.
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
State<GameOverDialogView> createState() => _GameOverDialogViewState();
|
||||
}
|
||||
|
||||
class _GameOverDialogViewState extends State<GameOverDialogView> {
|
||||
final playerInitialsInputController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
playerInitialsInputController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
// TODO(ruimiguel): refactor this view once UI design finished.
|
||||
return Dialog(
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
height: 250,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
l10n.gameOver,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
'${l10n.yourScore} ${widget.score}',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
TextField(
|
||||
key: const Key('player_initials_text_field'),
|
||||
controller: playerInitialsInputController,
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: l10n.enterInitials,
|
||||
),
|
||||
maxLength: 3,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
_GameOverDialogActions(
|
||||
score: widget.score,
|
||||
theme: widget.theme,
|
||||
playerInitialsInputController:
|
||||
playerInitialsInputController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GameOverDialogActions extends StatelessWidget {
|
||||
const _GameOverDialogActions({
|
||||
Key? key,
|
||||
required this.score,
|
||||
required this.theme,
|
||||
required this.playerInitialsInputController,
|
||||
}) : super(key: key);
|
||||
|
||||
final int score;
|
||||
final CharacterTheme theme;
|
||||
final TextEditingController playerInitialsInputController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return BlocBuilder<LeaderboardBloc, LeaderboardState>(
|
||||
builder: (context, state) {
|
||||
switch (state.status) {
|
||||
case LeaderboardStatus.loading:
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
context.read<LeaderboardBloc>().add(
|
||||
LeaderboardEntryAdded(
|
||||
entry: LeaderboardEntryData(
|
||||
playerInitials:
|
||||
playerInitialsInputController.text.toUpperCase(),
|
||||
score: score,
|
||||
character: theme.toType,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.addUser),
|
||||
);
|
||||
case LeaderboardStatus.success:
|
||||
return TextButton(
|
||||
onPressed: () => Navigator.of(context).push<void>(
|
||||
LeaderboardPage.route(theme: theme),
|
||||
),
|
||||
child: Text(l10n.leaderboard),
|
||||
);
|
||||
case LeaderboardStatus.error:
|
||||
return Text(l10n.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball/game/pinball_game.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
|
||||
/// {@template play_button_overlay}
|
||||
/// [Widget] that renders the button responsible to starting the game
|
||||
/// {@endtemplate}
|
||||
class PlayButtonOverlay extends StatelessWidget {
|
||||
/// {@macro play_button_overlay}
|
||||
const PlayButtonOverlay({
|
||||
Key? key,
|
||||
required PinballGame game,
|
||||
}) : _game = game,
|
||||
super(key: key);
|
||||
|
||||
final PinballGame _game;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: _game.gameFlowController.start,
|
||||
child: Text(l10n.play),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
export 'game_hud.dart';
|
||||
export 'game_over_dialog.dart';
|
||||
export 'play_button_overlay.dart';
|
||||
|
After Width: | Height: | Size: 955 KiB |
After Width: | Height: | Size: 2.4 MiB |
@ -0,0 +1,40 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// {@template backboard}
|
||||
/// The [Backboard] of the pinball machine.
|
||||
/// {@endtemplate}
|
||||
class Backboard extends SpriteComponent with HasGameRef {
|
||||
/// {@macro backboard}
|
||||
Backboard({
|
||||
required Vector2 position,
|
||||
}) : super(
|
||||
// TODO(erickzanardo): remove multiply after
|
||||
// https://github.com/flame-engine/flame/pull/1506 is merged
|
||||
position: position..clone().multiply(Vector2(1, -1)),
|
||||
anchor: Anchor.bottomCenter,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await waitingMode();
|
||||
}
|
||||
|
||||
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
|
||||
Future<void> waitingMode() async {
|
||||
final sprite = await gameRef.loadSprite(
|
||||
Assets.images.backboard.backboardScores.keyName,
|
||||
);
|
||||
size = sprite.originalSize / 10;
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
/// Puts the Backboard in game over mode, where the score input is shown.
|
||||
Future<void> gameOverMode() async {
|
||||
final sprite = await gameRef.loadSprite(
|
||||
Assets.images.backboard.backboardGameOver.keyName,
|
||||
);
|
||||
size = sprite.originalSize / 10;
|
||||
this.sprite = sprite;
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||
import 'package:sandbox/stories/dash_nest_bumper/big_dash_nest_bumper_game.dart';
|
||||
|
||||
void addDashNestBumperStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Dash Nest Bumpers').add(
|
||||
'Big',
|
||||
(context) => GameWidget(
|
||||
game: BigDashNestBumperGame()
|
||||
..trace = context.boolProperty('Trace', true),
|
||||
),
|
||||
codeLink: buildSourceLink('dash_nest_bumper/big.dart'),
|
||||
info: BasicBallGame.info,
|
||||
);
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||
|
||||
class FlutterSignPostGame extends BasicBallGame with Traceable {
|
||||
FlutterSignPostGame() : super(color: const Color(0xFF0000FF));
|
||||
|
||||
static const info = '''
|
||||
Shows how a FlutterSignPost is rendered.
|
||||
|
||||
- Activate the "trace" parameter to overlay the body.
|
||||
''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
camera.followVector2(Vector2.zero());
|
||||
await add(FlutterSignPost()..priority = 1);
|
||||
await traceAllBodies();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||
|
||||
class SmallDashNestBumperAGame extends BasicBallGame with Traceable {
|
||||
SmallDashNestBumperAGame() : super(color: const Color(0xFF0000FF));
|
||||
|
||||
static const info = '''
|
||||
Shows how a SmallDashNestBumper ("a") is rendered.
|
||||
|
||||
- Activate the "trace" parameter to overlay the body.
|
||||
''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
camera.followVector2(Vector2.zero());
|
||||
await add(SmallDashNestBumper.a()..priority = 1);
|
||||
await traceAllBodies();
|
||||
await traceAllBodies();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||
|
||||
class SmallDashNestBumperBGame extends BasicBallGame with Traceable {
|
||||
SmallDashNestBumperBGame() : super(color: const Color(0xFF0000FF));
|
||||
|
||||
static const info = '''
|
||||
Shows how a SmallDashNestBumper ("b") is rendered.
|
||||
|
||||
- Activate the "trace" parameter to overlay the body.
|
||||
''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
camera.followVector2(Vector2.zero());
|
||||
await add(SmallDashNestBumper.b()..priority = 1);
|
||||
await traceAllBodies();
|
||||
await traceAllBodies();
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/flutter_forest/big_dash_nest_bumper_game.dart';
|
||||
import 'package:sandbox/stories/flutter_forest/flutter_sign_post_game.dart';
|
||||
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart';
|
||||
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart';
|
||||
|
||||
void addDashNestBumperStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Flutter Forest')
|
||||
..add(
|
||||
'Flutter Sign Post',
|
||||
(context) => GameWidget(
|
||||
game: FlutterSignPostGame()
|
||||
..trace = context.boolProperty('Trace', true),
|
||||
),
|
||||
codeLink: buildSourceLink('flutter_forest/flutter_sign_post.dart'),
|
||||
info: FlutterSignPostGame.info,
|
||||
)
|
||||
..add(
|
||||
'Big Dash Nest Bumper',
|
||||
(context) => GameWidget(
|
||||
game: BigDashNestBumperGame()
|
||||
..trace = context.boolProperty('Trace', true),
|
||||
),
|
||||
codeLink: buildSourceLink('flutter_forest/big_dash_nest_bumper.dart'),
|
||||
info: BigDashNestBumperGame.info,
|
||||
)
|
||||
..add(
|
||||
'Small Dash Nest Bumper A',
|
||||
(context) => GameWidget(
|
||||
game: SmallDashNestBumperAGame()
|
||||
..trace = context.boolProperty('Trace', true),
|
||||
),
|
||||
codeLink: buildSourceLink('flutter_forest/small_dash_nest_bumper_a.dart'),
|
||||
info: SmallDashNestBumperAGame.info,
|
||||
)
|
||||
..add(
|
||||
'Small Dash Nest Bumper B',
|
||||
(context) => GameWidget(
|
||||
game: SmallDashNestBumperBGame()
|
||||
..trace = context.boolProperty('Trace', true),
|
||||
),
|
||||
codeLink: buildSourceLink('flutter_forest/small_dash_nest_bumper_b.dart'),
|
||||
info: SmallDashNestBumperBGame.info,
|
||||
);
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
// ignore_for_file: unawaited_futures
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('Backboard', () {
|
||||
final tester = FlameTester(TestGame.new);
|
||||
|
||||
group('on waitingMode', () {
|
||||
tester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
game.camera.zoom = 2;
|
||||
game.camera.followVector2(Vector2.zero());
|
||||
await game.ensureAdd(Backboard(position: Vector2(0, 15)));
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/backboard/waiting.png'),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('on gameOverMode', () {
|
||||
tester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
game.camera.zoom = 2;
|
||||
game.camera.followVector2(Vector2.zero());
|
||||
final backboard = Backboard(position: Vector2(0, 15));
|
||||
await game.ensureAdd(backboard);
|
||||
|
||||
await backboard.gameOverMode();
|
||||
await game.ready();
|
||||
await tester.pump();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/backboard/game_over.png'),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
After Width: | Height: | Size: 427 KiB |
After Width: | Height: | Size: 894 KiB |
@ -0,0 +1,35 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
|
||||
void main() {
|
||||
group('AssetsManagerCubit', () {
|
||||
final completer1 = Completer<void>();
|
||||
final completer2 = Completer<void>();
|
||||
|
||||
final future1 = completer1.future;
|
||||
final future2 = completer2.future;
|
||||
|
||||
blocTest<AssetsManagerCubit, AssetsManagerState>(
|
||||
'emits the loaded on the order that they load',
|
||||
build: () => AssetsManagerCubit([future1, future2]),
|
||||
act: (cubit) {
|
||||
cubit.load();
|
||||
completer2.complete();
|
||||
completer1.complete();
|
||||
},
|
||||
expect: () => [
|
||||
AssetsManagerState(
|
||||
loadables: [future1, future2],
|
||||
loaded: [future2],
|
||||
),
|
||||
AssetsManagerState(
|
||||
loadables: [future1, future2],
|
||||
loaded: [future2, future1],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
|
||||
void main() {
|
||||
group('AssetsManagerState', () {
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
AssetsManagerState(loadables: const [], loaded: const []),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('has the correct initial state', () {
|
||||
final future = Future<void>.value();
|
||||
expect(
|
||||
AssetsManagerState.initial(loadables: [future]),
|
||||
equals(
|
||||
AssetsManagerState(
|
||||
loadables: [future],
|
||||
loaded: const [],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
group('progress', () {
|
||||
final future1 = Future<void>.value();
|
||||
final future2 = Future<void>.value();
|
||||
|
||||
test('returns 0 when no future is loaded', () {
|
||||
expect(
|
||||
AssetsManagerState(
|
||||
loadables: [future1, future2],
|
||||
loaded: const [],
|
||||
).progress,
|
||||
equals(0),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns the correct value when some of the futures are loaded', () {
|
||||
expect(
|
||||
AssetsManagerState(
|
||||
loadables: [future1, future2],
|
||||
loaded: [future1],
|
||||
).progress,
|
||||
equals(0.5),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns the 1 when all futures are loaded', () {
|
||||
expect(
|
||||
AssetsManagerState(
|
||||
loadables: [future1, future2],
|
||||
loaded: [future1, future2],
|
||||
).progress,
|
||||
equals(1),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('copyWith', () {
|
||||
final future = Future<void>.value();
|
||||
|
||||
test('returns a copy with the updated loadables', () {
|
||||
expect(
|
||||
AssetsManagerState(
|
||||
loadables: const [],
|
||||
loaded: const [],
|
||||
).copyWith(loadables: [future]),
|
||||
equals(
|
||||
AssetsManagerState(
|
||||
loadables: [future],
|
||||
loaded: const [],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns a copy with the updated loaded', () {
|
||||
expect(
|
||||
AssetsManagerState(
|
||||
loadables: const [],
|
||||
loaded: const [],
|
||||
).copyWith(loaded: [future]),
|
||||
equals(
|
||||
AssetsManagerState(
|
||||
loadables: const [],
|
||||
loaded: [future],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('supports value comparison', () {
|
||||
final future1 = Future<void>.value();
|
||||
final future2 = Future<void>.value();
|
||||
|
||||
expect(
|
||||
AssetsManagerState(
|
||||
loadables: const [],
|
||||
loaded: const [],
|
||||
),
|
||||
equals(
|
||||
AssetsManagerState(
|
||||
loadables: const [],
|
||||
loaded: const [],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
AssetsManagerState(
|
||||
loadables: [future1],
|
||||
loaded: const [],
|
||||
),
|
||||
isNot(
|
||||
equals(
|
||||
AssetsManagerState(
|
||||
loadables: [future2],
|
||||
loaded: const [],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
AssetsManagerState(
|
||||
loadables: const [],
|
||||
loaded: [future1],
|
||||
),
|
||||
isNot(
|
||||
equals(
|
||||
AssetsManagerState(
|
||||
loadables: const [],
|
||||
loaded: [future2],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball/game/components/camera_controller.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
void main() {
|
||||
group('CameraController', () {
|
||||
late FlameGame game;
|
||||
late CameraController controller;
|
||||
|
||||
setUp(() async {
|
||||
game = FlameGame()..onGameResize(Vector2(100, 200));
|
||||
|
||||
controller = CameraController(game);
|
||||
await game.ensureAdd(controller);
|
||||
});
|
||||
|
||||
test('loads correctly', () async {
|
||||
expect(game.firstChild<CameraController>(), isNotNull);
|
||||
});
|
||||
|
||||
test('correctly calculates the zooms', () async {
|
||||
expect(controller.gameFocus.zoom.toInt(), equals(12));
|
||||
expect(controller.backboardFocus.zoom.toInt(), equals(11));
|
||||
});
|
||||
|
||||
test('correctly sets the initial zoom and position', () async {
|
||||
expect(game.camera.zoom, equals(controller.backboardFocus.zoom));
|
||||
expect(game.camera.follow, equals(controller.backboardFocus.position));
|
||||
});
|
||||
|
||||
group('focusOnBoard', () {
|
||||
test('changes the zoom', () async {
|
||||
controller.focusOnGame();
|
||||
|
||||
await game.ready();
|
||||
final zoom = game.firstChild<CameraZoom>();
|
||||
expect(zoom, isNotNull);
|
||||
expect(zoom?.value, equals(controller.gameFocus.zoom));
|
||||
});
|
||||
|
||||
test('moves the camera after the zoom is completed', () async {
|
||||
controller.focusOnGame();
|
||||
await game.ready();
|
||||
final cameraZoom = game.firstChild<CameraZoom>()!;
|
||||
final future = cameraZoom.completed;
|
||||
|
||||
game.update(10);
|
||||
game.update(0); // Ensure that the component was removed
|
||||
|
||||
await future;
|
||||
|
||||
expect(game.camera.position, Vector2(-4, -108.8));
|
||||
});
|
||||
});
|
||||
|
||||
group('focusOnBackboard', () {
|
||||
test('changes the zoom', () async {
|
||||
controller.focusOnBackboard();
|
||||
|
||||
await game.ready();
|
||||
final zoom = game.firstChild<CameraZoom>();
|
||||
expect(zoom, isNotNull);
|
||||
expect(zoom?.value, equals(controller.backboardFocus.zoom));
|
||||
});
|
||||
|
||||
test('moves the camera after the zoom is completed', () async {
|
||||
controller.focusOnBackboard();
|
||||
await game.ready();
|
||||
final cameraZoom = game.firstChild<CameraZoom>()!;
|
||||
final future = cameraZoom.completed;
|
||||
|
||||
game.update(10);
|
||||
game.update(0); // Ensure that the component was removed
|
||||
|
||||
await future;
|
||||
|
||||
expect(game.camera.position, Vector2(-4.5, -109.8));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
// ignore_for_file: type_annotate_public_apis, prefer_const_constructors
|
||||
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('GameFlowController', () {
|
||||
group('listenWhen', () {
|
||||
test('is true when the game over state has changed', () {
|
||||
final state = GameState(
|
||||
score: 10,
|
||||
balls: 0,
|
||||
activatedBonusLetters: const [],
|
||||
bonusHistory: const [],
|
||||
activatedDashNests: const {},
|
||||
);
|
||||
|
||||
final previous = GameState.initial();
|
||||
expect(
|
||||
GameFlowController(MockPinballGame()).listenWhen(previous, state),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('onNewState', () {
|
||||
late PinballGame game;
|
||||
late Backboard backboard;
|
||||
late CameraController cameraController;
|
||||
late GameFlowController gameFlowController;
|
||||
late ActiveOverlaysNotifier overlays;
|
||||
|
||||
setUp(() {
|
||||
game = MockPinballGame();
|
||||
backboard = MockBackboard();
|
||||
cameraController = MockCameraController();
|
||||
gameFlowController = GameFlowController(game);
|
||||
overlays = MockActiveOverlaysNotifier();
|
||||
|
||||
when(backboard.gameOverMode).thenAnswer((_) async {});
|
||||
when(backboard.waitingMode).thenAnswer((_) async {});
|
||||
when(cameraController.focusOnBackboard).thenAnswer((_) async {});
|
||||
when(cameraController.focusOnGame).thenAnswer((_) async {});
|
||||
|
||||
when(() => overlays.remove(any())).thenAnswer((_) => true);
|
||||
|
||||
when(game.firstChild<Backboard>).thenReturn(backboard);
|
||||
when(game.firstChild<CameraController>).thenReturn(cameraController);
|
||||
when(() => game.overlays).thenReturn(overlays);
|
||||
});
|
||||
|
||||
test(
|
||||
'changes the backboard and camera correctly when it is a game over',
|
||||
() {
|
||||
gameFlowController.onNewState(
|
||||
GameState(
|
||||
score: 10,
|
||||
balls: 0,
|
||||
activatedBonusLetters: const [],
|
||||
bonusHistory: const [],
|
||||
activatedDashNests: const {},
|
||||
),
|
||||
);
|
||||
|
||||
verify(backboard.gameOverMode).called(1);
|
||||
verify(cameraController.focusOnBackboard).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'changes the backboard and camera correctly when it is not a game over',
|
||||
() {
|
||||
gameFlowController.onNewState(GameState.initial());
|
||||
|
||||
verify(backboard.waitingMode).called(1);
|
||||
verify(cameraController.focusOnGame).called(1);
|
||||
verify(() => overlays.remove(PinballGame.playButtonOverlay))
|
||||
.called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final flameTester = FlameTester(EmptyPinballGameTest.new);
|
||||
|
||||
group('SparkyFireZone', () {
|
||||
group('bumpers', () {
|
||||
late ControlledSparkyBumper controlledSparkyBumper;
|
||||
|
||||
flameTester.testGameWidget(
|
||||
'activate when deactivated bumper is hit',
|
||||
setUp: (game, tester) async {
|
||||
controlledSparkyBumper = ControlledSparkyBumper();
|
||||
await game.ensureAdd(controlledSparkyBumper);
|
||||
|
||||
controlledSparkyBumper.controller.hit();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
expect(controlledSparkyBumper.controller.isActivated, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.testGameWidget(
|
||||
'deactivate when activated bumper is hit',
|
||||
setUp: (game, tester) async {
|
||||
controlledSparkyBumper = ControlledSparkyBumper();
|
||||
await game.ensureAdd(controlledSparkyBumper);
|
||||
|
||||
controlledSparkyBumper.controller.hit();
|
||||
controlledSparkyBumper.controller.hit();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
expect(controlledSparkyBumper.controller.isActivated, isFalse);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('PlayButtonOverlay', () {
|
||||
late PinballGame game;
|
||||
late GameFlowController gameFlowController;
|
||||
|
||||
setUp(() {
|
||||
game = MockPinballGame();
|
||||
gameFlowController = MockGameFlowController();
|
||||
|
||||
when(() => game.gameFlowController).thenReturn(gameFlowController);
|
||||
when(gameFlowController.start).thenAnswer((_) {});
|
||||
});
|
||||
|
||||
testWidgets('renders correctly', (tester) async {
|
||||
await tester.pumpApp(PlayButtonOverlay(game: game));
|
||||
|
||||
expect(find.text('Play'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('calls gameFlowController.start when taped', (tester) async {
|
||||
await tester.pumpApp(PlayButtonOverlay(game: game));
|
||||
|
||||
await tester.tap(find.text('Play'));
|
||||
await tester.pump();
|
||||
|
||||
verify(gameFlowController.start).called(1);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
import 'package:mockingjay/mockingjay.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball/leaderboard/leaderboard.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
import '../../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('GameOverDialog', () {
|
||||
testWidgets('renders GameOverDialogView', (tester) async {
|
||||
await tester.pumpApp(
|
||||
GameOverDialog(
|
||||
score: 1000,
|
||||
theme: DashTheme(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(GameOverDialogView), findsOneWidget);
|
||||
});
|
||||
|
||||
group('GameOverDialogView', () {
|
||||
late LeaderboardBloc leaderboardBloc;
|
||||
|
||||
final leaderboard = [
|
||||
LeaderboardEntry(
|
||||
rank: '1',
|
||||
playerInitials: 'ABC',
|
||||
score: 5000,
|
||||
character: DashTheme().characterAsset,
|
||||
),
|
||||
];
|
||||
final entryData = LeaderboardEntryData(
|
||||
playerInitials: 'VGV',
|
||||
score: 10000,
|
||||
character: CharacterType.dash,
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
leaderboardBloc = MockLeaderboardBloc();
|
||||
whenListen(
|
||||
leaderboardBloc,
|
||||
const Stream<LeaderboardState>.empty(),
|
||||
initialState: const LeaderboardState.initial(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('renders input text view when bloc emits [loading]',
|
||||
(tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: GameOverDialogView(
|
||||
score: entryData.score,
|
||||
theme: entryData.character.toTheme,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.widgetWithText(TextButton, l10n.addUser), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders error view when bloc emits [error]', (tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
|
||||
whenListen(
|
||||
leaderboardBloc,
|
||||
const Stream<LeaderboardState>.empty(),
|
||||
initialState: LeaderboardState.initial()
|
||||
.copyWith(status: LeaderboardStatus.error),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: GameOverDialogView(
|
||||
score: entryData.score,
|
||||
theme: entryData.character.toTheme,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text(l10n.error), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders success view when bloc emits [success]',
|
||||
(tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
|
||||
whenListen(
|
||||
leaderboardBloc,
|
||||
const Stream<LeaderboardState>.empty(),
|
||||
initialState: LeaderboardState(
|
||||
status: LeaderboardStatus.success,
|
||||
ranking: LeaderboardRanking(ranking: 1, outOf: 2),
|
||||
leaderboard: leaderboard,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: GameOverDialogView(
|
||||
score: entryData.score,
|
||||
theme: entryData.character.toTheme,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.widgetWithText(TextButton, l10n.leaderboard),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('adds LeaderboardEntryAdded when tap on add user button',
|
||||
(tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
|
||||
whenListen(
|
||||
leaderboardBloc,
|
||||
const Stream<LeaderboardState>.empty(),
|
||||
initialState: LeaderboardState.initial(),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: GameOverDialogView(
|
||||
score: entryData.score,
|
||||
theme: entryData.character.toTheme,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('player_initials_text_field')),
|
||||
entryData.playerInitials,
|
||||
);
|
||||
|
||||
final button = find.widgetWithText(TextButton, l10n.addUser);
|
||||
await tester.ensureVisible(button);
|
||||
await tester.tap(button);
|
||||
|
||||
verify(
|
||||
() => leaderboardBloc.add(LeaderboardEntryAdded(entry: entryData)),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('navigates to LeaderboardPage when tap on leaderboard button',
|
||||
(tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
final navigator = MockNavigator();
|
||||
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
|
||||
whenListen(
|
||||
leaderboardBloc,
|
||||
const Stream<LeaderboardState>.empty(),
|
||||
initialState: LeaderboardState(
|
||||
status: LeaderboardStatus.success,
|
||||
ranking: LeaderboardRanking(ranking: 1, outOf: 2),
|
||||
leaderboard: leaderboard,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: GameOverDialogView(
|
||||
score: entryData.score,
|
||||
theme: entryData.character.toTheme,
|
||||
),
|
||||
),
|
||||
navigator: navigator,
|
||||
);
|
||||
|
||||
final button = find.widgetWithText(TextButton, l10n.leaderboard);
|
||||
await tester.ensureVisible(button);
|
||||
await tester.tap(button);
|
||||
|
||||
verify(() => navigator.push<void>(any())).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in new issue