mirror of https://github.com/flutter/pinball.git
commit
ea993bac07
@ -1,2 +1,3 @@
|
||||
export 'bloc/leaderboard_bloc.dart';
|
||||
export 'models/leader_board_entry.dart';
|
||||
export 'view/leaderboard_page.dart';
|
||||
|
@ -0,0 +1,306 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball/leaderboard/leaderboard.dart';
|
||||
import 'package:pinball/theme/theme.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
class LeaderboardPage extends StatelessWidget {
|
||||
const LeaderboardPage({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
static Route route({required CharacterTheme theme}) {
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => LeaderboardPage(theme: theme),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LeaderboardBloc(
|
||||
context.read<LeaderboardRepository>(),
|
||||
)..add(const Top10Fetched()),
|
||||
child: LeaderboardView(theme: theme),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LeaderboardView extends StatelessWidget {
|
||||
const LeaderboardView({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
Text(
|
||||
l10n.leaderboard,
|
||||
style: Theme.of(context).textTheme.headline3,
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
BlocBuilder<LeaderboardBloc, LeaderboardState>(
|
||||
builder: (context, state) {
|
||||
switch (state.status) {
|
||||
case LeaderboardStatus.loading:
|
||||
return _LeaderboardLoading(theme: theme);
|
||||
case LeaderboardStatus.success:
|
||||
return _LeaderboardRanking(
|
||||
ranking: state.leaderboard,
|
||||
theme: theme,
|
||||
);
|
||||
case LeaderboardStatus.error:
|
||||
return _LeaderboardError(theme: theme);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).push<void>(
|
||||
CharacterSelectionPage.route(),
|
||||
),
|
||||
child: Text(l10n.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardLoading extends StatelessWidget {
|
||||
const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardError extends StatelessWidget {
|
||||
const _LeaderboardError({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'There was en error loading data!',
|
||||
style:
|
||||
Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardRanking extends StatelessWidget {
|
||||
const _LeaderboardRanking({
|
||||
Key? key,
|
||||
required this.ranking,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<LeaderboardEntry> ranking;
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_LeaderboardHeaders(theme: theme),
|
||||
_LeaderboardList(
|
||||
ranking: ranking,
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardHeaders extends StatelessWidget {
|
||||
const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_LeaderboardHeaderItem(title: l10n.rank, theme: theme),
|
||||
_LeaderboardHeaderItem(title: l10n.character, theme: theme),
|
||||
_LeaderboardHeaderItem(title: l10n.username, theme: theme),
|
||||
_LeaderboardHeaderItem(title: l10n.score, theme: theme),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardHeaderItem extends StatelessWidget {
|
||||
const _LeaderboardHeaderItem({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.ballColor,
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardList extends StatelessWidget {
|
||||
const _LeaderboardList({
|
||||
Key? key,
|
||||
required this.ranking,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<LeaderboardEntry> ranking;
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (_, index) => _LeaderBoardCompetitor(
|
||||
entry: ranking[index],
|
||||
theme: theme,
|
||||
),
|
||||
itemCount: ranking.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderBoardCompetitor extends StatelessWidget {
|
||||
const _LeaderBoardCompetitor({
|
||||
Key? key,
|
||||
required this.entry,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
final LeaderboardEntry entry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_LeaderboardCompetitorField(
|
||||
text: entry.rank,
|
||||
theme: theme,
|
||||
),
|
||||
_LeaderboardCompetitorCharacter(
|
||||
characterAsset: entry.character,
|
||||
theme: theme,
|
||||
),
|
||||
_LeaderboardCompetitorField(
|
||||
text: entry.playerInitials,
|
||||
theme: theme,
|
||||
),
|
||||
_LeaderboardCompetitorField(
|
||||
text: entry.score.toString(),
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardCompetitorField extends StatelessWidget {
|
||||
const _LeaderboardCompetitorField({
|
||||
Key? key,
|
||||
required this.text,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.ballColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(text),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardCompetitorCharacter extends StatelessWidget {
|
||||
const _LeaderboardCompetitorCharacter({
|
||||
Key? key,
|
||||
required this.characterAsset,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
final AssetGenImage characterAsset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.ballColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
child: characterAsset.image(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export 'ball.dart';
|
||||
export 'fire_effect.dart';
|
||||
export 'initial_position.dart';
|
||||
export 'layer.dart';
|
||||
|
@ -0,0 +1,113 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/particles.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart' hide Particle;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _particleRadius = 0.25;
|
||||
|
||||
// TODO(erickzanardo): This component could just be a ParticleComponet,
|
||||
/// unfortunately there is a Particle Component is not a PositionComponent,
|
||||
/// which makes it hard to be used since we have camera transformations and on
|
||||
// top of that, PositionComponent has a bug inside forge 2d games
|
||||
///
|
||||
/// https://github.com/flame-engine/flame/issues/1484
|
||||
/// https://github.com/flame-engine/flame/issues/1484
|
||||
|
||||
/// {@template fire_effect}
|
||||
/// A [BodyComponent] which creates a fire trail effect using the given
|
||||
/// parameters
|
||||
/// {@endtemplate}
|
||||
class FireEffect extends BodyComponent {
|
||||
/// {@macro fire_effect}
|
||||
FireEffect({
|
||||
required this.burstPower,
|
||||
required this.position,
|
||||
required this.direction,
|
||||
});
|
||||
|
||||
/// A [double] value that will define how "strong" the burst of particles
|
||||
/// will be
|
||||
final double burstPower;
|
||||
|
||||
/// The position of the burst
|
||||
final Vector2 position;
|
||||
|
||||
/// Which direction the burst will aim
|
||||
final Vector2 direction;
|
||||
late Particle _particle;
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final bodyDef = BodyDef()..position = position;
|
||||
|
||||
final fixtureDef = FixtureDef(CircleShape()..radius = 0)..isSensor = true;
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final children = [
|
||||
...List.generate(4, (index) {
|
||||
return CircleParticle(
|
||||
radius: _particleRadius,
|
||||
paint: Paint()..color = Colors.yellow.darken((index + 1) / 4),
|
||||
);
|
||||
}),
|
||||
...List.generate(4, (index) {
|
||||
return CircleParticle(
|
||||
radius: _particleRadius,
|
||||
paint: Paint()..color = Colors.red.darken((index + 1) / 4),
|
||||
);
|
||||
}),
|
||||
...List.generate(4, (index) {
|
||||
return CircleParticle(
|
||||
radius: _particleRadius,
|
||||
paint: Paint()..color = Colors.orange.darken((index + 1) / 4),
|
||||
);
|
||||
}),
|
||||
];
|
||||
final rng = math.Random();
|
||||
final spreadTween = Tween<double>(begin: -0.2, end: 0.2);
|
||||
|
||||
_particle = Particle.generate(
|
||||
count: (rng.nextDouble() * (burstPower * 10)).toInt(),
|
||||
generator: (_) {
|
||||
final spread = Vector2(
|
||||
spreadTween.transform(rng.nextDouble()),
|
||||
spreadTween.transform(rng.nextDouble()),
|
||||
);
|
||||
final finalDirection = Vector2(direction.x, -direction.y) + spread;
|
||||
final speed = finalDirection * (burstPower * 20);
|
||||
|
||||
return AcceleratedParticle(
|
||||
lifespan: 5 / burstPower,
|
||||
position: Vector2.zero(),
|
||||
speed: speed,
|
||||
child: children[rng.nextInt(children.length)],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
_particle.update(dt);
|
||||
|
||||
if (_particle.shouldRemove) {
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
super.render(canvas);
|
||||
|
||||
_particle.render(canvas);
|
||||
}
|
||||
}
|
@ -1,11 +1,2 @@
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
|
||||
String buildSourceLink(String path) {
|
||||
return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path';
|
||||
}
|
||||
|
||||
class BasicGame extends Forge2DGame {
|
||||
BasicGame() {
|
||||
images.prefix = '';
|
||||
}
|
||||
}
|
||||
export 'games.dart';
|
||||
export 'methods.dart';
|
||||
|
@ -0,0 +1,74 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BasicGame extends Forge2DGame {
|
||||
BasicGame() {
|
||||
images.prefix = '';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class LineGame extends BasicGame with PanDetector {
|
||||
Vector2? _lineEnd;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
camera.followVector2(Vector2.zero());
|
||||
unawaited(add(_PreviewLine()));
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanStart(DragStartInfo info) {
|
||||
_lineEnd = info.eventPosition.game;
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanUpdate(DragUpdateInfo info) {
|
||||
_lineEnd = info.eventPosition.game;
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanEnd(DragEndInfo info) {
|
||||
if (_lineEnd != null) {
|
||||
final line = _lineEnd! - Vector2.zero();
|
||||
onLine(line);
|
||||
_lineEnd = null;
|
||||
}
|
||||
}
|
||||
|
||||
void onLine(Vector2 line);
|
||||
}
|
||||
|
||||
class _PreviewLine extends PositionComponent with HasGameRef<LineGame> {
|
||||
static final _previewLinePaint = Paint()
|
||||
..color = Colors.pink
|
||||
..strokeWidth = 0.2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
Vector2? lineEnd;
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
|
||||
lineEnd = gameRef._lineEnd?.clone()?..multiply(Vector2(1, -1));
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
super.render(canvas);
|
||||
|
||||
if (lineEnd != null) {
|
||||
canvas.drawLine(
|
||||
Vector2.zero().toOffset(),
|
||||
lineEnd!.toOffset(),
|
||||
_previewLinePaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
String buildSourceLink(String path) {
|
||||
return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path';
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
|
||||
class BallBoosterExample extends LineGame {
|
||||
static const info = '';
|
||||
|
||||
@override
|
||||
void onLine(Vector2 line) {
|
||||
final ball = Ball(baseColor: Colors.transparent);
|
||||
add(ball);
|
||||
|
||||
ball.mounted.then((value) => ball.boost(line * -1 * 20));
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/effects/fire_effect.dart';
|
||||
|
||||
void addEffectsStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Effects').add(
|
||||
'Fire Effect',
|
||||
(context) => GameWidget(game: FireEffectExample()),
|
||||
codeLink: buildSourceLink('effects/fire_effect.dart'),
|
||||
info: FireEffectExample.info,
|
||||
);
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
|
||||
class FireEffectExample extends LineGame {
|
||||
static const info = 'Demonstrate the fire trail effect '
|
||||
'drag a line to define the trail direction';
|
||||
|
||||
@override
|
||||
void onLine(Vector2 line) {
|
||||
add(_EffectEmitter(line));
|
||||
}
|
||||
}
|
||||
|
||||
class _EffectEmitter extends Component {
|
||||
_EffectEmitter(this.line) {
|
||||
_direction = line.normalized();
|
||||
_force = line.length;
|
||||
}
|
||||
|
||||
static const _timerLimit = 2.0;
|
||||
var _timer = _timerLimit;
|
||||
|
||||
final Vector2 line;
|
||||
|
||||
late Vector2 _direction;
|
||||
late double _force;
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
|
||||
if (_timer > 0) {
|
||||
add(
|
||||
FireEffect(
|
||||
burstPower: (_timer / _timerLimit) * _force,
|
||||
position: Vector2.zero(),
|
||||
direction: _direction,
|
||||
),
|
||||
);
|
||||
_timer -= dt;
|
||||
} else {
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export 'mocks.dart';
|
||||
export 'test_game.dart';
|
||||
|
@ -0,0 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockCanvas extends Mock implements Canvas {}
|
@ -0,0 +1,55 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final flameTester = FlameTester(TestGame.new);
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Offset.zero);
|
||||
registerFallbackValue(Paint());
|
||||
});
|
||||
|
||||
group('FireEffect', () {
|
||||
flameTester.test('is removed once its particles are done', (game) async {
|
||||
await game.ensureAdd(
|
||||
FireEffect(
|
||||
burstPower: 1,
|
||||
position: Vector2.zero(),
|
||||
direction: Vector2.all(2),
|
||||
),
|
||||
);
|
||||
await game.ready();
|
||||
expect(game.children.whereType<FireEffect>().length, equals(1));
|
||||
game.update(5);
|
||||
|
||||
await game.ready();
|
||||
expect(game.children.whereType<FireEffect>().length, equals(0));
|
||||
});
|
||||
|
||||
flameTester.test('render circles on the canvas', (game) async {
|
||||
final effect = FireEffect(
|
||||
burstPower: 1,
|
||||
position: Vector2.zero(),
|
||||
direction: Vector2.all(2),
|
||||
);
|
||||
await game.ensureAdd(effect);
|
||||
await game.ready();
|
||||
|
||||
final canvas = MockCanvas();
|
||||
effect.render(canvas);
|
||||
|
||||
verify(() => canvas.drawCircle(any(), any(), any()))
|
||||
.called(greaterThan(0));
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.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 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
import '../../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('GameOverDialog', () {
|
||||
testWidgets('renders correctly', (tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
await tester.pumpApp(
|
||||
const GameOverDialog(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text(l10n.gameOver), findsOneWidget);
|
||||
expect(find.text(l10n.leaderboard), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tapping on leaderboard button navigates to LeaderBoardPage',
|
||||
(tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
final navigator = MockNavigator();
|
||||
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
|
||||
|
||||
await tester.pumpApp(
|
||||
const GameOverDialog(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
navigator: navigator,
|
||||
);
|
||||
|
||||
await tester.tap(find.widgetWithText(TextButton, l10n.leaderboard));
|
||||
|
||||
verify(() => navigator.push<void>(any())).called(1);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'helpers.dart';
|
||||
|
||||
Future<void> expectNavigatesToRoute<Type>(
|
||||
WidgetTester tester,
|
||||
Route route, {
|
||||
bool hasFlameGameInside = false,
|
||||
}) async {
|
||||
// ignore: avoid_dynamic_calls
|
||||
await tester.pumpApp(
|
||||
Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push<void>(route);
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Tap me'));
|
||||
if (hasFlameGameInside) {
|
||||
// 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
|
||||
} else {
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
expect(find.byType(Type), findsOneWidget);
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
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/l10n/l10n.dart';
|
||||
import 'package:pinball/leaderboard/leaderboard.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('LeaderboardPage', () {
|
||||
testWidgets('renders LeaderboardView', (tester) async {
|
||||
await tester.pumpApp(
|
||||
LeaderboardPage(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(LeaderboardView), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('route returns a valid navigation route', (tester) async {
|
||||
await expectNavigatesToRoute<LeaderboardPage>(
|
||||
tester,
|
||||
LeaderboardPage.route(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('LeaderboardView', () {
|
||||
late LeaderboardBloc leaderboardBloc;
|
||||
|
||||
setUp(() {
|
||||
leaderboardBloc = MockLeaderboardBloc();
|
||||
});
|
||||
|
||||
testWidgets('renders correctly', (tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial());
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: LeaderboardView(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text(l10n.leaderboard), findsOneWidget);
|
||||
expect(find.text(l10n.retry), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders loading view when bloc emits [loading]',
|
||||
(tester) async {
|
||||
when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial());
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: LeaderboardView(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
expect(find.text('There was en error loading data!'), findsNothing);
|
||||
expect(find.byType(ListView), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('renders error view when bloc emits [error]', (tester) async {
|
||||
when(() => leaderboardBloc.state).thenReturn(
|
||||
LeaderboardState.initial().copyWith(status: LeaderboardStatus.error),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: LeaderboardView(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
expect(find.text('There was en error loading data!'), findsOneWidget);
|
||||
expect(find.byType(ListView), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('renders success view when bloc emits [success]',
|
||||
(tester) async {
|
||||
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||
when(() => leaderboardBloc.state).thenReturn(
|
||||
LeaderboardState(
|
||||
status: LeaderboardStatus.success,
|
||||
ranking: LeaderboardRanking(ranking: 0, outOf: 0),
|
||||
leaderboard: [
|
||||
LeaderboardEntry(
|
||||
rank: '1',
|
||||
playerInitials: 'ABC',
|
||||
score: 10000,
|
||||
character: DashTheme().characterAsset,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpApp(
|
||||
BlocProvider.value(
|
||||
value: leaderboardBloc,
|
||||
child: LeaderboardView(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
expect(find.text('There was en error loading data!'), findsNothing);
|
||||
expect(find.text(l10n.rank), findsOneWidget);
|
||||
expect(find.text(l10n.character), findsOneWidget);
|
||||
expect(find.text(l10n.username), findsOneWidget);
|
||||
expect(find.text(l10n.score), findsOneWidget);
|
||||
expect(find.byType(ListView), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('navigates to CharacterSelectionPage when retry is tapped',
|
||||
(tester) async {
|
||||
final navigator = MockNavigator();
|
||||
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
|
||||
|
||||
await tester.pumpApp(
|
||||
LeaderboardPage(
|
||||
theme: DashTheme(),
|
||||
),
|
||||
navigator: navigator,
|
||||
);
|
||||
await tester.ensureVisible(find.byType(TextButton));
|
||||
await tester.tap(find.byType(TextButton));
|
||||
|
||||
verify(() => navigator.push<void>(any())).called(1);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in new issue