Merge branch 'main' into feat/incldue-public-member-api-docs

pull/13/head
Alejandro Santiago 4 years ago committed by GitHub
commit e781b1bb38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,18 @@
name: pinball_theme
on:
push:
paths:
- "packages/pinball_theme/**"
- ".github/workflows/pinball_theme.yaml"
pull_request:
paths:
- "packages/pinball_theme/**"
- ".github/workflows/pinball_theme.yaml"
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
working_directory: packages/pinball_theme

2
.gitignore vendored

@ -125,3 +125,5 @@ app.*.map.json
!.idea/codeStyles/
!.idea/dictionaries/
!.idea/runConfigurations/
.firebase

@ -154,6 +154,33 @@ Update the `CFBundleLocalizations` array in the `Info.plist` at `ios/Runner/Info
}
```
### Deploy application to Firebase hosting
Follow the following steps to deploy the application.
## Firebase CLI
Install and authenticate with [Firebase CLI tools](https://firebase.google.com/docs/cli)
## Build the project using the desired environment
```bash
# Development
$ flutter build web --release --target lib/main_development.dart
# Staging
$ flutter build web --release --target lib/main_staging.dart
# Production
$ flutter build web --release --target lib/main_production.dart
```
## Deploy
```bash
$ firebase deploy
```
[coverage_badge]: coverage_badge.svg
[flutter_localizations_link]: https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html
[internationalization_link]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@ -0,0 +1,11 @@
{
"hosting": {
"public": "build/web",
"site": "ashehwkdkdjruejdnensjsjdne",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}

@ -11,6 +11,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc() : super(const GameState.initial()) {
on<BallLost>(_onBallLost);
on<Scored>(_onScored);
on<BonusLetterActivated>(_onBonusLetterActivated);
}
void _onBallLost(BallLost event, Emitter emit) {
@ -24,4 +25,15 @@ class GameBloc extends Bloc<GameEvent, GameState> {
emit(state.copyWith(score: state.score + event.points));
}
}
void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) {
emit(
state.copyWith(
bonusLetters: [
...state.bonusLetters,
event.letter,
],
),
);
}
}

@ -32,3 +32,12 @@ class Scored extends GameEvent {
@override
List<Object?> get props => [points];
}
class BonusLetterActivated extends GameEvent {
const BonusLetterActivated(this.letter);
final String letter;
@override
List<Object?> get props => [letter];
}

@ -10,12 +10,14 @@ class GameState extends Equatable {
const GameState({
required this.score,
required this.balls,
required this.bonusLetters,
}) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls can't be negative");
const GameState.initial()
: score = 0,
balls = 3;
balls = 3,
bonusLetters = const [];
/// The current score of the game.
final int score;
@ -25,12 +27,19 @@ class GameState extends Equatable {
/// When the number of balls is 0, the game is over.
final int balls;
/// Active bonus letters.
final List<String> bonusLetters;
/// Determines when the game is over.
bool get isGameOver => balls == 0;
/// Determines when the player has only one ball left.
bool get isLastBall => balls == 1;
GameState copyWith({
int? score,
int? balls,
List<String>? bonusLetters,
}) {
assert(
score == null || score >= this.score,
@ -40,6 +49,7 @@ class GameState extends Equatable {
return GameState(
score: score ?? this.score,
balls: balls ?? this.balls,
bonusLetters: bonusLetters ?? this.bonusLetters,
);
}
@ -47,5 +57,6 @@ class GameState extends Equatable {
List<Object?> get props => [
score,
balls,
bonusLetters,
];
}

@ -0,0 +1,32 @@
import 'package:flame_forge2d/flame_forge2d.dart';
/// {@template anchor}
/// Non visual [BodyComponent] used to hold a [BodyType.dynamic] in [Joint]s
/// with this [BodyType.static].
///
/// It is recommended to [_position] the anchor first and then use the body
/// position as the anchor point when initializing a [JointDef].
///
/// ```dart
/// initialize(
/// dynamicBody.body,
/// anchor.body,
/// anchor.body.position,
/// );
/// ```
/// {@endtemplate}
class Anchor extends BodyComponent {
/// {@macro anchor}
Anchor({
required Vector2 position,
}) : _position = position;
final Vector2 _position;
@override
Body createBody() {
final bodyDef = BodyDef()..position = _position;
return world.createBody(bodyDef);
}
}

@ -1,28 +1,36 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/body_component.dart';
import 'package:flutter/material.dart';
import 'package:forge2d/forge2d.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the
/// [PinballGame].
/// {@endtemplate}
class Ball extends BodyComponent<PinballGame>
class Ball extends PositionBodyComponent<PinballGame, SpriteComponent>
with BlocComponent<GameBloc, GameState> {
/// {@macro ball}
Ball({
required Vector2 position,
}) : _position = position {
// TODO(alestiago): Use asset instead of color when provided.
paint = Paint()..color = const Color(0xFFFFFFFF);
}
}) : _position = position,
super(size: ballSize);
static final ballSize = Vector2.all(2);
final Vector2 _position;
static const spritePath = 'components/ball.png';
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent(sprite: sprite, size: ballSize);
}
@override
Body createBody() {
final shape = CircleShape()..radius = 2;
final shape = CircleShape()..radius = ballSize.x / 2;
final fixtureDef = FixtureDef(shape)..density = 1;
@ -33,4 +41,15 @@ class Ball extends BodyComponent<PinballGame>
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
void lost() {
shouldRemove = true;
final bloc = gameRef.read<GameBloc>()..add(const BallLost());
final shouldBallRespwan = !bloc.state.isLastBall;
if (shouldBallRespwan) {
gameRef.spawnBall();
}
}
}

@ -1,2 +1,5 @@
export 'anchor.dart';
export 'ball.dart';
export 'plunger.dart';
export 'score_points.dart';
export 'wall.dart';

@ -0,0 +1,72 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
/// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the
/// playfield.
///
/// [Plunger] ignores gravity so the player controls its downward [pull].
/// {@endtemplate}
class Plunger extends BodyComponent {
/// {@macro plunger}
Plunger({required Vector2 position}) : _position = position;
final Vector2 _position;
@override
Body createBody() {
final shape = PolygonShape()..setAsBoxXY(2.5, 1.5);
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..userData = this
..position = _position
..type = BodyType.dynamic
..gravityScale = 0;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
/// Set a constant downward velocity on the [Plunger].
void pull() {
body.linearVelocity = Vector2(0, -7);
}
/// Set an upward velocity on the [Plunger].
///
/// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [_position].
void release() {
final velocity = (_position.y - body.position.y) * 9;
body.linearVelocity = Vector2(0, velocity);
}
}
/// {@template plunger_anchor_prismatic_joint_def}
/// [PrismaticJointDef] between a [Plunger] and an [Anchor] with motion on
/// the vertical axis.
///
/// The [Plunger] is constrained vertically between its starting position and
/// the [Anchor]. The [Anchor] must be below the [Plunger].
/// {@endtemplate}
class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
/// {@macro plunger_anchor_prismatic_joint_def}
PlungerAnchorPrismaticJointDef({
required Plunger plunger,
required Anchor anchor,
}) : assert(
anchor.body.position.y < plunger.body.position.y,
'Anchor must be below the Plunger',
) {
initialize(
plunger.body,
anchor.body,
anchor.body.position,
Vector2(0, -1),
);
enableLimit = true;
lowerTranslation = double.negativeInfinity;
collideConnected = true;
}
}

@ -0,0 +1,63 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/components.dart';
/// {@template wall}
/// A continuos generic and [BodyType.static] barrier that divides a game area.
/// {@endtemplate}
class Wall extends BodyComponent {
Wall({
required this.start,
required this.end,
});
final Vector2 start;
final Vector2 end;
@override
Body createBody() {
final shape = EdgeShape()..set(start, end);
final fixtureDef = FixtureDef(shape)
..restitution = 0.0
..friction = 0.3;
final bodyDef = BodyDef()
..userData = this
..position = Vector2.zero()
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@template bottom_wall}
/// [Wall] located at the bottom of the board.
///
/// Collisions with [BottomWall] are listened by
/// [BottomWallBallContactCallback].
/// {@endtemplate}
class BottomWall extends Wall {
BottomWall(Forge2DGame game)
: super(
start: game.screenToWorld(game.camera.viewport.effectiveSize),
end: Vector2(
0,
game.screenToWorld(game.camera.viewport.effectiveSize).y,
),
);
}
/// {@template bottom_wall_ball_contact_callback}
/// Listens when a [Ball] falls into a [BottomWall].
/// {@endtemplate}
class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> {
@override
void begin(Ball ball, BottomWall wall, Contact contact) {
ball.lost();
}
@override
void end(_, __, ___) {}
}

@ -1,4 +1,5 @@
export 'bloc/game_bloc.dart';
export 'components/components.dart';
export 'game_assets.dart';
export 'pinball_game.dart';
export 'view/pinball_game_page.dart';
export 'view/view.dart';

@ -0,0 +1,11 @@
import 'package:pinball/game/game.dart';
/// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame {
/// Pre load the initial assets of the game.
Future<void> preLoadAssets() async {
await Future.wait([
images.load(Ball.spritePath),
]);
}
}

@ -1,12 +1,38 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
class PinballGame extends Forge2DGame with FlameBloc {
void spawnBall() {
add(
Ball(position: ballStartingPosition),
);
}
// TODO(erickzanardo): Change to the plumber position
late final ballStartingPosition = screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y - 20,
),
) -
Vector2(0, -20);
@override
Future<void> onLoad() async {
addContactCallback(BallScorePointsCallback());
await add(BottomWall(this));
addContactCallback(BottomWallBallContactCallback());
}
@override
void onAttach() {
super.onAttach();
spawnBall();
}
}

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
/// {@template game_hud}
/// Overlay of a [PinballGame] that displays the current [GameState.score] and
/// [GameState.balls].
/// {@endtemplate}
class GameHud extends StatelessWidget {
/// {@macro game_hud}
const GameHud({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final state = context.watch<GameBloc>().state;
return Container(
color: Colors.redAccent,
width: 200,
height: 100,
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${state.score}',
style: Theme.of(context).textTheme.headline3,
),
Wrap(
direction: Axis.vertical,
children: [
for (var i = 0; i < state.balls; i++)
const Padding(
padding: EdgeInsets.only(top: 6, right: 6),
child: CircleAvatar(
radius: 8,
backgroundColor: Colors.black,
),
),
],
),
],
),
);
}
}

@ -2,17 +2,74 @@
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
class PinballGamePage extends StatelessWidget {
const PinballGamePage({Key? key}) : super(key: key);
static Route route() {
return MaterialPageRoute<void>(builder: (_) => const PinballGamePage());
return MaterialPageRoute<void>(
builder: (_) {
return BlocProvider(
create: (_) => GameBloc(),
child: const PinballGamePage(),
);
},
);
}
@override
Widget build(BuildContext context) {
return GameWidget(game: PinballGame());
return const PinballGameView();
}
}
class PinballGameView extends StatefulWidget {
const PinballGameView({Key? key}) : super(key: key);
@override
State<PinballGameView> createState() => _PinballGameViewState();
}
class _PinballGameViewState extends State<PinballGameView> {
late PinballGame _game;
@override
void initState() {
super.initState();
// TODO(erickzanardo): Revisit this when we start to have more assets
// this could expose a Stream (maybe even a cubit?) so we could show the
// the loading progress with some fancy widgets.
_game = PinballGame()..preLoadAssets();
}
@override
Widget build(BuildContext context) {
return BlocListener<GameBloc, GameState>(
listener: (context, state) {
if (state.isGameOver) {
showDialog<void>(
context: context,
builder: (_) {
return const GameOverDialog();
},
);
}
},
child: Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(game: _game),
),
const Positioned(
top: 8,
left: 8,
child: GameHud(),
),
],
),
);
}
}

@ -0,0 +1,3 @@
export 'game_hud.dart';
export 'pinball_game_page.dart';
export 'widgets/widgets.dart';

@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class GameOverDialog extends StatelessWidget {
const GameOverDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Dialog(
child: SizedBox(
width: 200,
height: 200,
child: Center(
child: Text('Game Over'),
),
),
);
}
}

@ -0,0 +1 @@
export 'game_over_dialog.dart';

@ -0,0 +1,13 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball_theme/pinball_theme.dart';
part 'theme_state.dart';
class ThemeCubit extends Cubit<ThemeState> {
ThemeCubit() : super(const ThemeState.initial());
void characterSelected(CharacterTheme characterTheme) {
emit(ThemeState(PinballTheme(characterTheme: characterTheme)));
}
}

@ -0,0 +1,13 @@
part of 'theme_cubit.dart';
class ThemeState extends Equatable {
const ThemeState(this.theme);
const ThemeState.initial()
: theme = const PinballTheme(characterTheme: DashTheme());
final PinballTheme theme;
@override
List<Object> get props => [theme];
}

@ -0,0 +1 @@
export 'cubit/theme_cubit.dart';

@ -0,0 +1,39 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VSCode related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json

@ -0,0 +1,11 @@
# pinball_theme
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
Package containing themes for pinball game.
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis

@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml

@ -0,0 +1,4 @@
library pinball_theme;
export 'src/pinball_theme.dart';
export 'src/themes/themes.dart';

@ -0,0 +1,23 @@
import 'package:equatable/equatable.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template pinball_theme}
/// Defines all theme assets and attributes.
///
/// Game components should have a getter specified here to load their
/// corresponding assets for the game.
/// {@endtemplate}
class PinballTheme extends Equatable {
/// {@macro pinball_theme}
const PinballTheme({
required CharacterTheme characterTheme,
}) : _characterTheme = characterTheme;
final CharacterTheme _characterTheme;
/// [CharacterTheme] for the chosen character.
CharacterTheme get characterTheme => _characterTheme;
@override
List<Object?> get props => [_characterTheme];
}

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template android_theme}
/// Defines Android character theme assets and attributes.
/// {@endtemplate}
class AndroidTheme extends CharacterTheme {
/// {@macro android_theme}
const AndroidTheme();
@override
Color get ballColor => Colors.green;
}

@ -0,0 +1,19 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
/// {@template character_theme}
/// Base class for creating character themes.
///
/// Character specific game components should have a getter specified here to
/// load their corresponding assets for the game.
/// {@endtemplate}
abstract class CharacterTheme extends Equatable {
/// {@macro character_theme}
const CharacterTheme();
/// Ball color for this theme.
Color get ballColor;
@override
List<Object?> get props => [ballColor];
}

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template dash_theme}
/// Defines Dash character theme assets and attributes.
/// {@endtemplate}
class DashTheme extends CharacterTheme {
/// {@macro dash_theme}
const DashTheme();
@override
Color get ballColor => Colors.blue;
}

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template dino_theme}
/// Defines Dino character theme assets and attributes.
/// {@endtemplate}
class DinoTheme extends CharacterTheme {
/// {@macro dino_theme}
const DinoTheme();
@override
Color get ballColor => Colors.grey;
}

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template sparky_theme}
/// Defines Sparky character theme assets and attributes.
/// {@endtemplate}
class SparkyTheme extends CharacterTheme {
/// {@macro sparky_theme}
const SparkyTheme();
@override
Color get ballColor => Colors.orange;
}

@ -0,0 +1,5 @@
export 'android_theme.dart';
export 'character_theme.dart';
export 'dash_theme.dart';
export 'dino_theme.dart';
export 'sparky_theme.dart';

@ -0,0 +1,17 @@
name: pinball_theme
description: Package containing themes for pinball game.
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
equatable: ^2.0.3
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
very_good_analysis: ^2.4.0

@ -0,0 +1,28 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('PinballTheme', () {
const characterTheme = SparkyTheme();
test('can be instantiated', () {
expect(PinballTheme(characterTheme: characterTheme), isNotNull);
});
test('supports value equality', () {
expect(
PinballTheme(characterTheme: characterTheme),
equals(PinballTheme(characterTheme: characterTheme)),
);
});
test('characterTheme is correct', () {
expect(
PinballTheme(characterTheme: characterTheme).characterTheme,
equals(characterTheme),
);
});
});
}

@ -0,0 +1,21 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('AndroidTheme', () {
test('can be instantiated', () {
expect(AndroidTheme(), isNotNull);
});
test('supports value equality', () {
expect(AndroidTheme(), equals(AndroidTheme()));
});
test('ballColor is correct', () {
expect(AndroidTheme().ballColor, equals(Colors.green));
});
});
}

@ -0,0 +1,21 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('DashTheme', () {
test('can be instantiated', () {
expect(DashTheme(), isNotNull);
});
test('supports value equality', () {
expect(DashTheme(), equals(DashTheme()));
});
test('ballColor is correct', () {
expect(DashTheme().ballColor, equals(Colors.blue));
});
});
}

@ -0,0 +1,21 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('DinoTheme', () {
test('can be instantiated', () {
expect(DinoTheme(), isNotNull);
});
test('supports value equality', () {
expect(DinoTheme(), equals(DinoTheme()));
});
test('ballColor is correct', () {
expect(DinoTheme().ballColor, equals(Colors.grey));
});
});
}

@ -0,0 +1,21 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('SparkyTheme', () {
test('can be instantiated', () {
expect(SparkyTheme(), isNotNull);
});
test('supports value equality', () {
expect(SparkyTheme(), equals(SparkyTheme()));
});
test('ballColor is correct', () {
expect(SparkyTheme().ballColor, equals(Colors.orange));
});
});
}

@ -140,21 +140,21 @@ packages:
name: flame
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-releasecandidate.1"
version: "1.1.0-releasecandidate.2"
flame_bloc:
dependency: "direct main"
description:
name: flame_bloc
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-releasecandidate.1"
version: "1.2.0-releasecandidate.2"
flame_forge2d:
dependency: "direct main"
description:
name: flame_forge2d
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.0-releasecandidate.1"
version: "0.9.0-releasecandidate.2"
flame_test:
dependency: "direct dev"
description:
@ -324,6 +324,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
pinball_theme:
dependency: "direct main"
description:
path: "packages/pinball_theme"
relative: true
source: path
version: "1.0.0+1"
pool:
dependency: transitive
description:

@ -9,15 +9,17 @@ environment:
dependencies:
bloc: ^8.0.2
equatable: ^2.0.3
flame: ^1.1.0-releasecandidate.1
flame_bloc: ^1.2.0-releasecandidate.1
flame_forge2d: ^0.9.0-releasecandidate.1
flame: ^1.1.0-releasecandidate.2
flame_bloc: ^1.2.0-releasecandidate.2
flame_forge2d: ^0.9.0-releasecandidate.2
flutter:
sdk: flutter
flutter_bloc: ^8.0.1
flutter_localizations:
sdk: flutter
intl: ^0.17.0
pinball_theme:
path: packages/pinball_theme
dev_dependencies:
bloc_test: ^9.0.2
@ -31,3 +33,6 @@ dev_dependencies:
flutter:
uses-material-design: true
generate: true
assets:
- assets/images/components/

@ -21,9 +21,9 @@ void main() {
}
},
expect: () => [
const GameState(score: 0, balls: 2),
const GameState(score: 0, balls: 1),
const GameState(score: 0, balls: 0),
const GameState(score: 0, balls: 2, bonusLetters: []),
const GameState(score: 0, balls: 1, bonusLetters: []),
const GameState(score: 0, balls: 0, bonusLetters: []),
],
);
});
@ -37,8 +37,8 @@ void main() {
..add(const Scored(points: 2))
..add(const Scored(points: 3)),
expect: () => [
const GameState(score: 2, balls: 3),
const GameState(score: 5, balls: 3),
const GameState(score: 2, balls: 3, bonusLetters: []),
const GameState(score: 5, balls: 3, bonusLetters: []),
],
);
@ -53,9 +53,55 @@ void main() {
bloc.add(const Scored(points: 2));
},
expect: () => [
const GameState(score: 0, balls: 2),
const GameState(score: 0, balls: 1),
const GameState(score: 0, balls: 0),
const GameState(score: 0, balls: 2, bonusLetters: []),
const GameState(score: 0, balls: 1, bonusLetters: []),
const GameState(score: 0, balls: 0, bonusLetters: []),
],
);
});
group('BonusLetterActivated', () {
blocTest<GameBloc, GameState>(
'adds the letter to the state',
build: GameBloc.new,
act: (bloc) => bloc
..add(const BonusLetterActivated('G'))
..add(const BonusLetterActivated('O'))
..add(const BonusLetterActivated('O'))
..add(const BonusLetterActivated('G'))
..add(const BonusLetterActivated('L'))
..add(const BonusLetterActivated('E')),
expect: () => [
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L', 'E'],
),
],
);
});

@ -40,5 +40,22 @@ void main() {
expect(() => Scored(points: 0), throwsAssertionError);
});
});
group('BonusLetterActivated', () {
test('can be instantiated', () {
expect(const BonusLetterActivated('A'), isNotNull);
});
test('supports value equality', () {
expect(
BonusLetterActivated('A'),
equals(BonusLetterActivated('A')),
);
expect(
BonusLetterActivated('B'),
isNot(equals(BonusLetterActivated('A'))),
);
});
});
});
}

@ -7,14 +7,27 @@ void main() {
group('GameState', () {
test('supports value equality', () {
expect(
GameState(score: 0, balls: 0),
equals(const GameState(score: 0, balls: 0)),
GameState(
score: 0,
balls: 0,
bonusLetters: const [],
),
equals(
const GameState(
score: 0,
balls: 0,
bonusLetters: [],
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(const GameState(score: 0, balls: 0), isNotNull);
expect(
const GameState(score: 0, balls: 0, bonusLetters: []),
isNotNull,
);
});
});
@ -23,7 +36,7 @@ void main() {
'when balls are negative',
() {
expect(
() => GameState(balls: -1, score: 0),
() => GameState(balls: -1, score: 0, bonusLetters: const []),
throwsAssertionError,
);
},
@ -34,7 +47,7 @@ void main() {
'when score is negative',
() {
expect(
() => GameState(balls: 0, score: -1),
() => GameState(balls: 0, score: -1, bonusLetters: const []),
throwsAssertionError,
);
},
@ -47,6 +60,7 @@ void main() {
const gameState = GameState(
balls: 0,
score: 0,
bonusLetters: [],
);
expect(gameState.isGameOver, isTrue);
});
@ -57,11 +71,40 @@ void main() {
const gameState = GameState(
balls: 1,
score: 0,
bonusLetters: [],
);
expect(gameState.isGameOver, isFalse);
});
});
group('isLastBall', () {
test(
'is true '
'when there is only one ball left',
() {
const gameState = GameState(
balls: 1,
score: 0,
bonusLetters: [],
);
expect(gameState.isLastBall, isTrue);
},
);
test(
'is false '
'when there are more balls left',
() {
const gameState = GameState(
balls: 2,
score: 0,
bonusLetters: [],
);
expect(gameState.isLastBall, isFalse);
},
);
});
group('copyWith', () {
test(
'throws AssertionError '
@ -70,6 +113,7 @@ void main() {
const gameState = GameState(
balls: 0,
score: 2,
bonusLetters: [],
);
expect(
() => gameState.copyWith(score: gameState.score - 1),
@ -85,6 +129,7 @@ void main() {
const gameState = GameState(
balls: 0,
score: 2,
bonusLetters: [],
);
expect(
gameState.copyWith(),
@ -100,10 +145,12 @@ void main() {
const gameState = GameState(
score: 2,
balls: 0,
bonusLetters: [],
);
final otherGameState = GameState(
score: gameState.score + 1,
balls: gameState.balls + 1,
bonusLetters: const ['A'],
);
expect(gameState, isNot(equals(otherGameState)));
@ -111,6 +158,7 @@ void main() {
gameState.copyWith(
score: otherGameState.score,
balls: otherGameState.balls,
bonusLetters: otherGameState.bonusLetters,
),
equals(otherGameState),
);

@ -0,0 +1,60 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Anchor', () {
final flameTester = FlameTester(PinballGame.new);
flameTester.test(
'loads correctly',
(game) async {
final anchor = Anchor(position: Vector2.zero());
await game.ensureAdd(anchor);
expect(game.contains(anchor), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final anchor = Anchor(position: position);
await game.ensureAdd(anchor);
game.contains(anchor);
expect(anchor.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
final anchor = Anchor(position: Vector2.zero());
await game.ensureAdd(anchor);
expect(anchor.body.bodyType, equals(BodyType.static));
},
);
});
group('fixtures', () {
flameTester.test(
'has none',
(game) async {
final anchor = Anchor(position: Vector2.zero());
await game.ensureAdd(anchor);
expect(anchor.body.fixtures, isEmpty);
},
);
});
});
}

@ -1,10 +1,14 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -75,7 +79,77 @@ void main() {
final fixture = ball.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(2));
expect(fixture.shape.radius, equals(1));
},
);
});
group('resetting a ball', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final tester = flameBlocTester(
gameBlocBuilder: () {
return gameBloc;
},
);
tester.widgetTest(
'adds BallLost to GameBloc',
(game, tester) async {
await game.ready();
game.children.whereType<Ball>().first.lost();
await tester.pump();
verify(() => gameBloc.add(const BallLost())).called(1);
},
);
tester.widgetTest(
'resets the ball if the game is not over',
(game, tester) async {
await game.ready();
game.children.whereType<Ball>().first.removeFromParent();
await game.ready(); // Making sure that all additions are done
expect(
game.children.whereType<Ball>().length,
equals(1),
);
},
);
tester.widgetTest(
'no ball is added on game over',
(game, tester) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState(
score: 10,
balls: 1,
bonusLetters: [],
),
);
await game.ready();
game.children.whereType<Ball>().first.removeFromParent();
await tester.pump();
expect(
game.children.whereType<Ball>().length,
equals(0),
);
},
);
});

@ -0,0 +1,302 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
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(PinballGame.new);
group('Plunger', () {
flameTester.test(
'loads correctly',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
expect(game.contains(plunger), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final plunger = Plunger(position: position);
await game.ensureAdd(plunger);
game.contains(plunger);
expect(plunger.body.position, position);
},
);
flameTester.test(
'is dynamic',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
expect(plunger.body.bodyType, equals(BodyType.dynamic));
},
);
flameTester.test(
'ignores gravity',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
expect(plunger.body.gravityScale, isZero);
},
);
});
group('first fixture', () {
flameTester.test(
'exists',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
expect(plunger.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'shape is a polygon',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.polygon));
},
);
});
flameTester.test(
'pull sets a negative linear velocity',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
plunger.pull();
expect(plunger.body.linearVelocity.y, isNegative);
expect(plunger.body.linearVelocity.x, isZero);
},
);
group('release', () {
flameTester.test(
'does not set a linear velocity '
'when plunger is in starting position',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
plunger.release();
expect(plunger.body.linearVelocity.y, isZero);
expect(plunger.body.linearVelocity.x, isZero);
},
);
flameTester.test(
'sets a positive linear velocity '
'when plunger is below starting position',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
plunger.body.setTransform(Vector2(0, -1), 0);
plunger.release();
expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
});
group('PlungerAnchorPrismaticJointDef', () {
late GameBloc gameBloc;
late Plunger plunger;
late Anchor anchor;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
plunger = Plunger(position: Vector2.zero());
anchor = Anchor(position: Vector2(0, -1));
});
final flameTester = flameBlocTester(
gameBlocBuilder: () {
return gameBloc;
},
);
flameTester.test(
'throws AssertionError '
'when anchor is above plunger',
(game) async {
final anchor = Anchor(position: Vector2(0, 1));
await game.ensureAddAll([plunger, anchor]);
expect(
() => PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
),
throwsAssertionError,
);
},
);
flameTester.test(
'throws AssertionError '
'when anchor is in same position as plunger',
(game) async {
final anchor = Anchor(position: Vector2.zero());
await game.ensureAddAll([plunger, anchor]);
expect(
() => PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
),
throwsAssertionError,
);
},
);
group('initializes with', () {
flameTester.test(
'plunger body as bodyA',
(game) async {
await game.ensureAddAll([plunger, anchor]);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
expect(jointDef.bodyA, equals(plunger.body));
},
);
flameTester.test(
'anchor body as bodyB',
(game) async {
await game.ensureAddAll([plunger, anchor]);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
expect(jointDef.bodyB, equals(anchor.body));
},
);
flameTester.test(
'limits enabled',
(game) async {
await game.ensureAddAll([plunger, anchor]);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
expect(jointDef.enableLimit, isTrue);
},
);
flameTester.test(
'lower translation limit as negative infinity',
(game) async {
await game.ensureAddAll([plunger, anchor]);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
expect(jointDef.lowerTranslation, equals(double.negativeInfinity));
},
);
flameTester.test(
'connected body collison enabled',
(game) async {
await game.ensureAddAll([plunger, anchor]);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
expect(jointDef.collideConnected, isTrue);
},
);
});
flameTester.widgetTest(
'plunger cannot go below anchor',
(game, tester) async {
await game.ensureAddAll([plunger, anchor]);
// Giving anchor a shape for the plunger to collide with.
anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1));
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
plunger.pull();
await tester.pump(const Duration(seconds: 1));
expect(plunger.body.position.y > anchor.body.position.y, isTrue);
},
);
flameTester.widgetTest(
'plunger cannot excessively exceed starting position',
(game, tester) async {
await game.ensureAddAll([plunger, anchor]);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
plunger.pull();
await tester.pump(const Duration(seconds: 1));
plunger.release();
await tester.pump(const Duration(seconds: 1));
expect(plunger.body.position.y < 1, isTrue);
},
);
});
}

@ -0,0 +1,122 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Wall', () {
group('BottomWallBallContactCallback', () {
test(
'removes the ball on begin contact when the wall is a bottom one',
() {
final game = MockPinballGame();
final wall = MockBottomWall();
final ball = MockBall();
when(() => ball.gameRef).thenReturn(game);
BottomWallBallContactCallback()
// Remove once https://github.com/flame-engine/flame/pull/1415
// is merged
..end(MockBall(), MockBottomWall(), MockContact())
..begin(ball, wall, MockContact());
verify(ball.lost).called(1);
},
);
});
final flameTester = FlameTester(PinballGame.new);
flameTester.test(
'loads correctly',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
expect(game.contains(wall), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
game.contains(wall);
expect(wall.body.position, Vector2.zero());
},
);
flameTester.test(
'is static',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
expect(wall.body.bodyType, equals(BodyType.static));
},
);
});
group('first fixture', () {
flameTester.test(
'exists',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
expect(wall.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'has restitution equals 0',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
final fixture = wall.body.fixtures[0];
expect(fixture.restitution, equals(0));
},
);
flameTester.test(
'has friction',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
final fixture = wall.body.fixtures[0];
expect(fixture.friction, greaterThan(0));
},
);
});
});
}

@ -0,0 +1,79 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
group('GameHud', () {
late GameBloc gameBloc;
const initialState = GameState(score: 10, balls: 2, bonusLetters: []);
void _mockState(GameState state) {
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
);
}
Future<void> _pumpHud(WidgetTester tester) async {
await tester.pumpApp(
GameHud(),
gameBloc: gameBloc,
);
}
setUp(() {
gameBloc = MockGameBloc();
_mockState(initialState);
});
testWidgets(
'renders the current score',
(tester) async {
await _pumpHud(tester);
expect(find.text(initialState.score.toString()), findsOneWidget);
},
);
testWidgets(
'renders the current ball number',
(tester) async {
await _pumpHud(tester);
expect(
find.byType(CircleAvatar),
findsNWidgets(initialState.balls),
);
},
);
testWidgets('updates the score', (tester) async {
await _pumpHud(tester);
expect(find.text(initialState.score.toString()), findsOneWidget);
_mockState(initialState.copyWith(score: 20));
await tester.pump();
expect(find.text('20'), findsOneWidget);
});
testWidgets('updates the ball number', (tester) async {
await _pumpHud(tester);
expect(
find.byType(CircleAvatar),
findsNWidgets(initialState.balls),
);
_mockState(initialState.copyWith(balls: 1));
await tester.pump();
expect(
find.byType(CircleAvatar),
findsNWidgets(1),
);
});
});
}

@ -1,4 +1,6 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
@ -6,9 +8,84 @@ import '../../helpers/helpers.dart';
void main() {
group('PinballGamePage', () {
testWidgets('renders single GameWidget with PinballGame', (tester) async {
await tester.pumpApp(const PinballGamePage());
expect(find.byType(GameWidget<PinballGame>), findsOneWidget);
testWidgets('renders PinballGameView', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
await tester.pumpApp(const PinballGamePage(), gameBloc: gameBloc);
expect(find.byType(PinballGameView), findsOneWidget);
});
testWidgets('route returns a valid navigation route', (tester) async {
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(PinballGamePage.route());
},
child: const Text('Tap me'),
);
},
),
),
);
await tester.tap(find.text('Tap me'));
// We can't use pumpAndSettle here because the page renders a Flame game
// which is an infinity animation, so it will timeout
await tester.pump(); // Runs the button action
await tester.pump(); // Runs the navigation
expect(find.byType(PinballGamePage), findsOneWidget);
});
});
group('PinballGameView', () {
testWidgets('renders game and a hud', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc);
expect(
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
findsOneWidget,
);
expect(
find.byType(GameHud),
findsOneWidget,
);
});
testWidgets(
'renders a game over dialog when the user has lost',
(tester) async {
final gameBloc = MockGameBloc();
const state = GameState(score: 0, balls: 0, bonusLetters: []);
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
);
await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc);
await tester.pump();
expect(
find.text('Game Over'),
findsOneWidget,
);
},
);
});
}

@ -0,0 +1,19 @@
import 'package:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
FlameTester<PinballGame> flameBlocTester({
required GameBloc Function() gameBlocBuilder,
}) {
return FlameTester<PinballGame>(
PinballGame.new,
pumpWidget: (gameWidget, tester) async {
await tester.pumpWidget(
BlocProvider.value(
value: gameBlocBuilder(),
child: gameWidget,
),
);
},
);
}

@ -5,4 +5,6 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
export 'builders.dart';
export 'mocks.dart';
export 'pump_app.dart';

@ -0,0 +1,15 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
class MockPinballGame extends Mock implements PinballGame {}
class MockWall extends Mock implements Wall {}
class MockBottomWall extends Mock implements BottomWall {}
class MockBall extends Mock implements Ball {}
class MockContact extends Mock implements Contact {}
class MockGameBloc extends Mock implements GameBloc {}

@ -6,15 +6,20 @@
// https://opensource.org/licenses/MIT.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'helpers.dart';
extension PumpApp on WidgetTester {
Future<void> pumpApp(
Widget widget, {
MockNavigator? navigator,
GameBloc? gameBloc,
}) {
return pumpWidget(
MaterialApp(
@ -23,10 +28,13 @@ extension PumpApp on WidgetTester {
GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: navigator != null
home: BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
child: navigator != null
? MockNavigatorProvider(navigator: navigator, child: widget)
: widget,
),
),
);
}
}

@ -0,0 +1,22 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('ThemeCubit', () {
test('initial state has Dash character theme', () {
final themeCubit = ThemeCubit();
expect(themeCubit.state.theme.characterTheme, equals(const DashTheme()));
});
blocTest<ThemeCubit, ThemeState>(
'charcterSelected emits selected character theme',
build: ThemeCubit.new,
act: (bloc) => bloc.characterSelected(const SparkyTheme()),
expect: () => [
const ThemeState(PinballTheme(characterTheme: SparkyTheme())),
],
);
});
}

@ -0,0 +1,19 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/theme/theme.dart';
void main() {
group('ThemeState', () {
test('can be instantiated', () {
expect(const ThemeState.initial(), isNotNull);
});
test('supports value equality', () {
expect(
ThemeState.initial(),
equals(const ThemeState.initial()),
);
});
});
}
Loading…
Cancel
Save