refactor: reusable blinking behavior

refactor/blinking-behavior
Allison Ryan 2 years ago
parent fdb9075738
commit f0428d74bc

@ -0,0 +1,111 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
/// {@template blinking_behavior}
/// Looping behavior that performs an action at the end of each loop.
/// {@endtemplate}
class BlinkingBehavior<T> extends Component {
/// {@macro blinking_behavior}
BlinkingBehavior({
required double loopDuration,
int loops = 1,
VoidCallback? onLoop,
VoidCallback? onFinished,
Stream<T>? stream,
bool Function(T? previousState, T newState)? listenWhen,
}) : _loopDuration = loopDuration,
_loops = loops,
_onLoop = onLoop,
_onFinished = onFinished,
_stream = stream,
_listenWhen = listenWhen;
final double _loopDuration;
final int _loops;
final VoidCallback? _onLoop;
final VoidCallback? _onFinished;
final Stream<T>? _stream;
final bool Function(T? previousState, T newState)? _listenWhen;
T? _previousState;
StreamSubscription<T>? _subscription;
@override
Future<void> onLoad() async {
await super.onLoad();
if (_stream == null && _listenWhen == null) {
await add(
LoopableTimerComponent(
period: _loopDuration,
loops: _loops,
onLoop: _onLoop,
onFinished: _onFinished,
)..start(),
);
return;
}
_subscription = _stream!.listen((newState) async {
if (_listenWhen!(_previousState, newState)) {
await add(
LoopableTimerComponent(
period: _loopDuration,
loops: _loops,
onLoop: _onLoop,
onFinished: _onFinished,
)..start(),
);
}
_previousState = newState;
});
}
@override
void onRemove() {
_subscription?.cancel();
super.onRemove();
}
}
/// {@template loopable_timer_component}
/// Timer that performs an action at the end of each loop.
/// {@endtemplate}
@visibleForTesting
class LoopableTimerComponent extends TimerComponent {
/// {@macro loopable_timer_component}
LoopableTimerComponent({
required double period,
int loops = 1,
VoidCallback? onLoop,
VoidCallback? onFinished,
}) : _periods = loops,
_onFinished = onFinished,
super(
period: period,
repeat: true,
onTick: onLoop,
autoStart: false,
);
final int _periods;
final VoidCallback? _onFinished;
int _currentLoop = 0;
/// Begin the looping.
void start() => timer.start();
@override
void onTick() {
super.onTick();
_currentLoop++;
if (_currentLoop == _periods) {
timer.stop();
_onFinished?.call();
shouldRemove = true;
}
}
}

@ -29,7 +29,13 @@ class AndroidBumper extends BodyComponent with InitialPosition, ZIndex {
renderBody: false,
children: [
AndroidBumperBallContactBehavior(),
AndroidBumperBlinkingBehavior(),
BlinkingBehavior<AndroidBumperState>(
loopDuration: 0.05,
onFinished: bloc.onBlinked,
stream: bloc.stream,
listenWhen: (previousState, newState) =>
previousState != newState,
),
_AndroidBumperSpriteGroupComponent(
dimmedAssetPath: dimmedAssetPath,
litAssetPath: litAssetPath,

@ -1,39 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template android_bumper_blinking_behavior}
/// Makes an [AndroidBumper] blink back to [AndroidBumperState.lit] when
/// [AndroidBumperState.dimmed].
/// {@endtemplate}
class AndroidBumperBlinkingBehavior extends TimerComponent
with ParentIsA<AndroidBumper> {
/// {@macro android_bumper_blinking_behavior}
AndroidBumperBlinkingBehavior() : super(period: 0.05);
void _onNewState(AndroidBumperState state) {
switch (state) {
case AndroidBumperState.lit:
break;
case AndroidBumperState.dimmed:
timer
..reset()
..start();
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
timer.stop();
parent.bloc.onBlinked();
}
}

@ -1,2 +1 @@
export 'android_bumper_ball_contact_behavior.dart';
export 'android_bumper_blinking_behavior.dart';

@ -1,2 +1 @@
export 'kicker_ball_contact_behavior.dart';
export 'kicker_blinking_behavior.dart';

@ -1,37 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template kicker_blinking_behavior}
/// Makes a [Kicker] blink back to [KickerState.lit] when [KickerState.dimmed].
/// {@endtemplate}
class KickerBlinkingBehavior extends TimerComponent with ParentIsA<Kicker> {
/// {@macro kicker_blinking_behavior}
KickerBlinkingBehavior() : super(period: 0.05);
void _onNewState(KickerState state) {
switch (state) {
case KickerState.lit:
break;
case KickerState.dimmed:
timer
..reset()
..start();
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
timer.stop();
parent.bloc.onBlinked();
}
}

@ -38,7 +38,13 @@ class Kicker extends BodyComponent with InitialPosition {
children: [
BumpingBehavior(strength: 25)..applyTo(['bouncy_edge']),
KickerBallContactBehavior()..applyTo(['bouncy_edge']),
KickerBlinkingBehavior(),
BlinkingBehavior<KickerState>(
loopDuration: 0.05,
onFinished: bloc.onBlinked,
stream: bloc.stream,
listenWhen: (previousState, newState) =>
previousState != newState,
),
_KickerSpriteGroupComponent(
side: side,
state: bloc.state,

@ -1,78 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template multiball_blinking_behavior}
/// Makes a [Multiball] blink back to [MultiballLightState.lit] when
/// [MultiballLightState.dimmed].
/// {@endtemplate}
class MultiballBlinkingBehavior extends TimerComponent
with ParentIsA<Multiball> {
/// {@macro multiball_blinking_behavior}
MultiballBlinkingBehavior() : super(period: 0.1);
final _maxBlinks = 10;
int _blinksCounter = 0;
bool _isAnimating = false;
void _onNewState(MultiballState state) {
final animationEnabled =
state.animationState == MultiballAnimationState.blinking;
final canBlink = _blinksCounter < _maxBlinks;
if (animationEnabled && canBlink) {
_start();
} else {
_stop();
}
}
void _start() {
if (!_isAnimating) {
_isAnimating = true;
timer
..reset()
..start();
_animate();
}
}
void _animate() {
parent.bloc.onBlink();
_blinksCounter++;
}
void _stop() {
if (_isAnimating) {
_isAnimating = false;
timer.stop();
_blinksCounter = 0;
parent.bloc.onStop();
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
if (!_isAnimating) {
timer.stop();
} else {
if (_blinksCounter < _maxBlinks) {
_animate();
timer
..reset()
..start();
} else {
timer.stop();
}
}
}
}

@ -7,27 +7,23 @@ class MultiballCubit extends Cubit<MultiballState> {
MultiballCubit() : super(const MultiballState.initial());
void onAnimate() {
emit(
state.copyWith(animationState: MultiballAnimationState.blinking),
);
emit(state.copyWith(isAnimating: true));
}
void onStop() {
emit(
state.copyWith(animationState: MultiballAnimationState.idle),
);
emit(state.copyWith(isAnimating: false));
}
void onBlink() {
switch (state.lightState) {
case MultiballLightState.lit:
void onBlinked() {
switch (state.spriteState) {
case MultiballSpriteState.lit:
emit(
state.copyWith(lightState: MultiballLightState.dimmed),
state.copyWith(lightState: MultiballSpriteState.dimmed),
);
break;
case MultiballLightState.dimmed:
case MultiballSpriteState.dimmed:
emit(
state.copyWith(lightState: MultiballLightState.lit),
state.copyWith(lightState: MultiballSpriteState.lit),
);
break;
}

@ -1,41 +1,35 @@
part of 'multiball_cubit.dart';
enum MultiballLightState {
enum MultiballSpriteState {
lit,
dimmed,
}
// Indicates if the blinking animation is running.
enum MultiballAnimationState {
idle,
blinking,
}
class MultiballState extends Equatable {
const MultiballState({
required this.lightState,
required this.animationState,
required this.spriteState,
required this.isAnimating,
});
const MultiballState.initial()
: this(
lightState: MultiballLightState.dimmed,
animationState: MultiballAnimationState.idle,
spriteState: MultiballSpriteState.dimmed,
isAnimating: false,
);
final MultiballLightState lightState;
final MultiballAnimationState animationState;
final MultiballSpriteState spriteState;
final bool isAnimating;
MultiballState copyWith({
MultiballLightState? lightState,
MultiballAnimationState? animationState,
MultiballSpriteState? lightState,
bool? isAnimating,
}) {
return MultiballState(
lightState: lightState ?? this.lightState,
animationState: animationState ?? this.animationState,
spriteState: lightState ?? this.spriteState,
isAnimating: isAnimating ?? this.isAnimating,
);
}
@override
List<Object> get props => [lightState, animationState];
List<Object> get props => [spriteState, isAnimating];
}

@ -2,7 +2,6 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import 'package:pinball_components/src/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -20,13 +19,23 @@ class Multiball extends Component {
required this.bloc,
}) : super(
children: [
MultiballBlinkingBehavior(),
BlinkingBehavior<MultiballState>(
loopDuration: 0.1,
loops: 20,
onLoop: bloc.onBlinked,
onFinished: bloc.onStop,
stream: bloc.stream,
listenWhen: (previousState, newState) {
final isAlreadyAnimating = previousState?.isAnimating ?? false;
return !isAlreadyAnimating && newState.isAnimating;
},
),
MultiballSpriteGroupComponent(
position: position,
litAssetPath: Assets.images.multiball.lit.keyName,
dimmedAssetPath: Assets.images.multiball.dimmed.keyName,
rotation: rotation,
state: bloc.state.lightState,
state: bloc.state.spriteState,
),
...?children,
],
@ -94,7 +103,7 @@ class Multiball extends Component {
/// {@endtemplate}
@visibleForTesting
class MultiballSpriteGroupComponent
extends SpriteGroupComponent<MultiballLightState>
extends SpriteGroupComponent<MultiballSpriteState>
with HasGameRef, ParentIsA<Multiball> {
/// {@macro multiball_sprite_group_component}
MultiballSpriteGroupComponent({
@ -102,7 +111,7 @@ class MultiballSpriteGroupComponent
required String litAssetPath,
required String dimmedAssetPath,
required double rotation,
required MultiballLightState state,
required MultiballSpriteState state,
}) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath,
super(
@ -118,13 +127,13 @@ class MultiballSpriteGroupComponent
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state.lightState);
parent.bloc.stream.listen((state) => current = state.spriteState);
final sprites = {
MultiballLightState.lit: Sprite(
MultiballSpriteState.lit: Sprite(
gameRef.images.fromCache(_litAssetPath),
),
MultiballLightState.dimmed:
MultiballSpriteState.dimmed:
Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
};
this.sprites = sprites;

@ -1,2 +1 @@
export 'skill_shot_ball_contact_behavior.dart';
export 'skill_shot_blinking_behavior.dart';

@ -1,44 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template skill_shot_blinking_behavior}
/// Makes a [SkillShot] blink between [SkillShotSpriteState.lit] and
/// [SkillShotSpriteState.dimmed] for a set amount of blinks.
/// {@endtemplate}
class SkillShotBlinkingBehavior extends TimerComponent
with ParentIsA<SkillShot> {
/// {@macro skill_shot_blinking_behavior}
SkillShotBlinkingBehavior() : super(period: 0.15);
final _maxBlinks = 4;
int _blinks = 0;
void _onNewState(SkillShotState state) {
if (state.isBlinking) {
timer
..reset()
..start();
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
if (_blinks != _maxBlinks * 2) {
parent.bloc.switched();
_blinks++;
} else {
_blinks = 0;
timer.stop();
parent.bloc.onBlinkingFinished();
}
}
}

@ -15,6 +15,15 @@ class SkillShotCubit extends Cubit<SkillShotState> {
);
}
void finishedBlinking() {
emit(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
);
}
void switched() {
switch (state.spriteState) {
case SkillShotSpriteState.lit:
@ -25,13 +34,4 @@ class SkillShotCubit extends Cubit<SkillShotState> {
break;
}
}
void onBlinkingFinished() {
emit(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
);
}
}

@ -25,7 +25,17 @@ class SkillShot extends BodyComponent with ZIndex {
renderBody: false,
children: [
SkillShotBallContactBehavior(),
SkillShotBlinkingBehavior(),
BlinkingBehavior<SkillShotState>(
loopDuration: 0.15,
loops: 8,
onLoop: bloc.switched,
onFinished: bloc.finishedBlinking,
stream: bloc.stream,
listenWhen: (previousState, newState) {
final isAlreadyBlinking = previousState?.isBlinking ?? false;
return !isAlreadyBlinking && newState.isBlinking;
},
),
_RolloverDecalSpriteComponent(),
PinSpriteAnimationComponent(),
_TextDecalSpriteGroupComponent(state: bloc.state.spriteState),

@ -1,2 +1 @@
export 'sparky_bumper_ball_contact_behavior.dart';
export 'sparky_bumper_blinking_behavior.dart';

@ -1,39 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_bumper_blinking_behavior}
/// Makes a [SparkyBumper] blink back to [SparkyBumperState.lit] when
/// [SparkyBumperState.dimmed].
/// {@endtemplate}
class SparkyBumperBlinkingBehavior extends TimerComponent
with ParentIsA<SparkyBumper> {
/// {@macro sparky_bumper_blinking_behavior}
SparkyBumperBlinkingBehavior() : super(period: 0.05);
void _onNewState(SparkyBumperState state) {
switch (state) {
case SparkyBumperState.lit:
break;
case SparkyBumperState.dimmed:
timer
..reset()
..start();
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
timer.stop();
parent.bloc.onBlinked();
}
}

@ -29,7 +29,13 @@ class SparkyBumper extends BodyComponent with InitialPosition, ZIndex {
renderBody: false,
children: [
SparkyBumperBallContactBehavior(),
SparkyBumperBlinkingBehavior(),
BlinkingBehavior<SparkyBumperState>(
loopDuration: 0.05,
onFinished: bloc.onBlinked,
stream: bloc.stream,
listenWhen: (previousState, newState) =>
previousState != newState,
),
_SparkyBumperSpriteGroupComponent(
litAssetPath: litAssetPath,
dimmedAssetPath: dimmedAssetPath,

@ -1,2 +1,3 @@
export 'behaviors/behaviors.dart';
export 'components/components.dart';
export 'extensions/extensions.dart';

@ -45,7 +45,7 @@ class MultiballGame extends BallGame with KeyboardEvents {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) {
for (final multiball in multiballs) {
multiball.bloc.onBlink();
multiball.bloc.onBlinked();
}
return KeyEventResult.handled;

@ -0,0 +1,163 @@
// ignore_for_file: prefer_const_constructors, cascade_invocations
import 'dart:async';
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';
class _MockStream extends Mock implements Stream<void> {}
class _MockStreamSubscription extends Mock implements StreamSubscription<void> {
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('BlinkingBehavior', () {
flameTester.testGameWidget(
'loops for the given loops and loopDuration '
'and calls onLoop after each loop',
setUp: (game, tester) async {
const loopDuration = 0.1;
var loopCount = 0;
final behavior = BlinkingBehavior<void>(
loopDuration: loopDuration,
loops: 2,
onLoop: () => loopCount++,
);
final component = Component();
await component.add(behavior);
await game.ensureAdd(component);
await tester.pump();
game.update(loopDuration);
expect(loopCount, equals(1));
await tester.pump();
game.update(loopDuration);
expect(loopCount, equals(2));
},
);
flameTester.testGameWidget(
'calls onFinished after all loops',
setUp: (game, tester) async {
const loopDuration = 0.1;
var onFinishedCalled = false;
final behavior = BlinkingBehavior<void>(
loopDuration: loopDuration,
loops: 2,
onFinished: () => onFinishedCalled = true,
);
final component = Component();
await component.add(behavior);
await game.ensureAdd(component);
await tester.pump();
game.update(loopDuration);
await tester.pump();
game.update(loopDuration);
expect(onFinishedCalled, isTrue);
},
);
flameTester.test(
'adds a LoopableTimerComponent when stream and listenWhen '
'are not provided',
(game) async {
const loopDuration = 0.1;
final behavior = BlinkingBehavior<void>(
loopDuration: loopDuration,
);
final component = Component();
await component.add(behavior);
await game.ensureAdd(component);
expect(
behavior.firstChild<LoopableTimerComponent>(),
isA<LoopableTimerComponent>(),
);
},
);
flameTester.testGameWidget(
'adds a LoopableTimerComponent only when the stream emits '
'a state satisfying listenWhen',
setUp: (game, tester) async {
const loopDuration = 0.1;
final streamController = StreamController<bool>();
final behavior = BlinkingBehavior<bool>(
loopDuration: loopDuration,
stream: streamController.stream,
listenWhen: (previousState, newState) => previousState != newState,
);
final component = Component();
await component.add(behavior);
await game.ensureAdd(component);
streamController.add(true);
await game.ready();
await tester.pump();
expect(
behavior.firstChild<LoopableTimerComponent>(),
isA<LoopableTimerComponent>(),
);
game.update(loopDuration);
streamController.add(true);
await game.ready();
await tester.pump();
expect(
behavior.firstChild<LoopableTimerComponent>(),
isNull,
);
streamController.add(false);
await game.ready();
await tester.pump();
expect(
behavior.firstChild<LoopableTimerComponent>(),
isA<LoopableTimerComponent>(),
);
},
);
flameTester.testGameWidget(
'closes stream subscription when behavior is removed',
setUp: (game, tester) async {
const loopDuration = 0.1;
final stream = _MockStream();
final streamSubscription = _MockStreamSubscription();
when(() => stream.listen(any())).thenReturn(streamSubscription);
when(streamSubscription.cancel).thenAnswer((_) async {});
final behavior = BlinkingBehavior<void>(
loopDuration: loopDuration,
stream: stream,
listenWhen: (_, __) => false,
);
final component = Component();
await component.add(behavior);
await game.ensureAdd(component);
behavior.shouldRemove = true;
await tester.pump();
expect(component.contains(behavior), isFalse);
verify(streamSubscription.cancel).called(1);
},
);
});
}

@ -1,5 +1,7 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
@ -73,18 +75,29 @@ void main() {
);
});
flameTester.test('an AndroidBumperBlinkingBehavior', (game) async {
flameTester.test('a BlinkingBehavior', (game) async {
final androidBumper = AndroidBumper.a();
await game.ensureAdd(androidBumper);
expect(
androidBumper.children
.whereType<AndroidBumperBlinkingBehavior>()
.single,
androidBumper.children.whereType<BlinkingBehavior>().single,
isNotNull,
);
});
});
flameTester.test('BlinkingBehavior triggers when the state changes',
(game) async {
final bloc = _MockAndroidBumperCubit();
final androidBumper = AndroidBumper.a();
await game.ensureAdd(androidBumper);
expect(androidBumper.descendants()., matcher)
});
group("'a' adds", () {
flameTester.test('new children', (game) async {
final component = Component();

@ -1,47 +0,0 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.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 'package:pinball_components/src/components/android_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'AndroidBumperBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlinked after 0.05 seconds when dimmed',
setUp: (game, tester) async {
final behavior = AndroidBumperBlinkingBehavior();
final bloc = _MockAndroidBumperCubit();
final streamController = StreamController<AndroidBumperState>();
whenListen(
bloc,
streamController.stream,
initialState: AndroidBumperState.lit,
);
final androidBumper = AndroidBumper.test(bloc: bloc);
await androidBumper.add(behavior);
await game.ensureAdd(androidBumper);
streamController.add(AndroidBumperState.dimmed);
await tester.pump();
game.update(0.05);
await streamController.close();
verify(bloc.onBlinked).called(1);
},
);
},
);
}

@ -1,50 +0,0 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.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 'package:pinball_components/src/components/kicker/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockKickerCubit extends Mock implements KickerCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'KickerBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlinked after 0.05 seconds when dimmed',
setUp: (game, tester) async {
final behavior = KickerBlinkingBehavior();
final bloc = _MockKickerCubit();
final streamController = StreamController<KickerState>();
whenListen(
bloc,
streamController.stream,
initialState: KickerState.lit,
);
final kicker = Kicker.test(
side: BoardSide.left,
bloc: bloc,
);
await kicker.add(behavior);
await game.ensureAdd(kicker);
streamController.add(KickerState.dimmed);
await tester.pump();
game.update(0.05);
await streamController.close();
verify(bloc.onBlinked).called(1);
},
);
},
);
}

@ -9,7 +9,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/bumping_behavior.dart';
import 'package:pinball_components/src/components/kicker/behaviors/behaviors.dart';
import '../../helpers/helpers.dart';
import '../../../helpers/helpers.dart';
class _MockKickerCubit extends Mock implements KickerCubit {}
@ -43,7 +43,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/kickers.png'),
matchesGoldenFile('../golden/kickers.png'),
);
},
);
@ -61,6 +61,9 @@ void main() {
},
);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockKickerCubit();
whenListen(
@ -114,13 +117,13 @@ void main() {
);
});
flameTester.test('a KickerBlinkingBehavior', (game) async {
flameTester.test('a BlinkingBehavior', (game) async {
final kicker = Kicker(
side: BoardSide.left,
);
await game.ensureAdd(kicker);
expect(
kicker.children.whereType<KickerBlinkingBehavior>().single,
kicker.children.whereType<BlinkingBehavior>().single,
isNotNull,
);
});

@ -1,160 +0,0 @@
// ignore_for_file: prefer_const_constructors, cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.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 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockMultiballCubit extends Mock implements MultiballCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'MultiballBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlink every 0.1 seconds when animation state is animated',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = _MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
game.update(0);
verify(bloc.onBlink).called(1);
await tester.pump();
game.update(0.1);
await streamController.close();
verify(bloc.onBlink).called(1);
},
);
flameTester.testGameWidget(
'calls onStop when animation state is stopped',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = _MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
streamController.add(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.lit,
),
);
await streamController.close();
verify(bloc.onStop).called(1);
},
);
flameTester.testGameWidget(
'onTick stops when there is no animation',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = _MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.lit,
),
);
await tester.pump();
behavior.onTick();
expect(behavior.timer.isRunning(), false);
},
);
flameTester.testGameWidget(
'onTick stops after 10 blinks repetitions',
setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior();
final bloc = _MockMultiballCubit();
final streamController = StreamController<MultiballState>();
whenListen(
bloc,
streamController.stream,
initialState: MultiballState.initial(),
);
when(bloc.onBlink).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
await multiball.add(behavior);
await game.ensureAdd(multiball);
streamController.add(
MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.dimmed,
),
);
await tester.pump();
for (var i = 0; i < 10; i++) {
behavior.onTick();
}
expect(behavior.timer.isRunning(), false);
},
);
},
);
}

@ -7,58 +7,58 @@ void main() {
'MultiballCubit',
() {
blocTest<MultiballCubit, MultiballState>(
'onAnimate emits animationState [animate]',
'onAnimate emits isAnimating true',
build: MultiballCubit.new,
act: (bloc) => bloc.onAnimate(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.animationState,
'animationState',
MultiballAnimationState.blinking,
(state) => state.isAnimating,
'isAnimating',
true,
)
],
);
blocTest<MultiballCubit, MultiballState>(
'onStop emits animationState [stopped]',
'onStop emits isAnimating false',
build: MultiballCubit.new,
act: (bloc) => bloc.onStop(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.animationState,
'animationState',
MultiballAnimationState.idle,
(state) => state.isAnimating,
'isAnimating',
false,
)
],
);
blocTest<MultiballCubit, MultiballState>(
'onBlink emits lightState [lit, dimmed, lit]',
'onBlinked emits lightState [lit, dimmed, lit]',
build: MultiballCubit.new,
act: (bloc) => bloc
..onBlink()
..onBlink()
..onBlink(),
..onBlinked()
..onBlinked()
..onBlinked(),
expect: () => [
isA<MultiballState>()
..having(
(state) => state.lightState,
(state) => state.spriteState,
'lightState',
MultiballLightState.lit,
MultiballSpriteState.lit,
),
isA<MultiballState>()
..having(
(state) => state.lightState,
(state) => state.spriteState,
'lightState',
MultiballLightState.dimmed,
MultiballSpriteState.dimmed,
),
isA<MultiballState>()
..having(
(state) => state.lightState,
(state) => state.spriteState,
'lightState',
MultiballLightState.lit,
MultiballSpriteState.lit,
)
],
);

@ -8,13 +8,13 @@ void main() {
test('supports value equality', () {
expect(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
isAnimating: false,
spriteState: MultiballSpriteState.dimmed,
),
equals(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
isAnimating: false,
spriteState: MultiballSpriteState.dimmed,
),
),
);
@ -24,8 +24,8 @@ void main() {
test('can be instantiated', () {
expect(
MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
isAnimating: false,
spriteState: MultiballSpriteState.dimmed,
),
isNotNull,
);
@ -38,8 +38,8 @@ void main() {
'when no argument specified',
() {
final multiballState = MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
isAnimating: false,
spriteState: MultiballSpriteState.dimmed,
);
expect(
multiballState.copyWith(),
@ -53,19 +53,19 @@ void main() {
'when all arguments specified',
() {
final multiballState = MultiballState(
animationState: MultiballAnimationState.idle,
lightState: MultiballLightState.dimmed,
isAnimating: false,
spriteState: MultiballSpriteState.dimmed,
);
final otherMultiballState = MultiballState(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
isAnimating: true,
spriteState: MultiballSpriteState.lit,
);
expect(multiballState, isNot(equals(otherMultiballState)));
expect(
multiballState.copyWith(
animationState: MultiballAnimationState.blinking,
lightState: MultiballLightState.lit,
isAnimating: true,
lightState: MultiballSpriteState.lit,
),
equals(otherMultiballState),
);

@ -6,7 +6,6 @@ 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 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
@ -55,8 +54,8 @@ void main() {
final bloc = _MockMultiballCubit();
whenListen(
bloc,
const Stream<MultiballLightState>.empty(),
initialState: MultiballLightState.dimmed,
const Stream<MultiballSpriteState>.empty(),
initialState: MultiballSpriteState.dimmed,
);
when(bloc.close).thenAnswer((_) async {});
final multiball = Multiball.test(bloc: bloc);
@ -79,11 +78,11 @@ void main() {
expect(multiball.children, contains(component));
});
flameTester.test('a MultiballBlinkingBehavior', (game) async {
flameTester.test('a BlinkingBehavior', (game) async {
final multiball = Multiball.a();
await game.ensureAdd(multiball);
expect(
multiball.children.whereType<MultiballBlinkingBehavior>().single,
multiball.children.whereType<BlinkingBehavior>().single,
isNotNull,
);
});

@ -1,125 +0,0 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.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 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockSkillShotCubit extends Mock implements SkillShotCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SkillShotBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls switched after 0.15 seconds when isBlinking and lit',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
await tester.pump();
game.update(0.15);
await streamController.close();
verify(bloc.switched).called(1);
},
);
flameTester.testGameWidget(
'calls switched after 0.15 seconds when isBlinking and dimmed',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: true,
),
);
await tester.pump();
game.update(0.15);
await streamController.close();
verify(bloc.switched).called(1);
},
);
flameTester.testGameWidget(
'calls onBlinkingFinished after all blinks complete',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
for (var i = 0; i <= 8; i++) {
if (i.isEven) {
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
} else {
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: true,
),
);
}
await tester.pump();
game.update(0.15);
}
await streamController.close();
verify(bloc.onBlinkingFinished).called(1);
},
);
},
);
}

@ -51,9 +51,9 @@ void main() {
);
blocTest<SkillShotCubit, SkillShotState>(
'onBlinkingFinished emits dimmed and false',
'finishedBlinking emits dimmed and false',
build: SkillShotCubit.new,
act: (bloc) => bloc.onBlinkingFinished(),
act: (bloc) => bloc.finishedBlinking(),
expect: () => [
SkillShotState(
spriteState: SkillShotSpriteState.dimmed,

@ -65,11 +65,11 @@ void main() {
);
});
flameTester.test('a SkillShotBlinkingBehavior', (game) async {
flameTester.test('a BlinkingBehavior', (game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
expect(
skillShot.children.whereType<SkillShotBlinkingBehavior>().single,
skillShot.children.whereType<BlinkingBehavior>().single,
isNotNull,
);
});

@ -1,47 +0,0 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.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 'package:pinball_components/src/components/sparky_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SparkyBumperBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlinked after 0.05 seconds when dimmed',
setUp: (game, tester) async {
final behavior = SparkyBumperBlinkingBehavior();
final bloc = _MockSparkyBumperCubit();
final streamController = StreamController<SparkyBumperState>();
whenListen(
bloc,
streamController.stream,
initialState: SparkyBumperState.lit,
);
final sparkyBumper = SparkyBumper.test(bloc: bloc);
await sparkyBumper.add(behavior);
await game.ensureAdd(sparkyBumper);
streamController.add(SparkyBumperState.dimmed);
await tester.pump();
game.update(0.05);
await streamController.close();
verify(bloc.onBlinked).called(1);
},
);
},
);
}

@ -73,13 +73,11 @@ void main() {
);
});
flameTester.test('a SparkyBumperBlinkingBehavior', (game) async {
flameTester.test('a BlinkingBehavior', (game) async {
final sparkyBumper = SparkyBumper.a();
await game.ensureAdd(sparkyBumper);
expect(
sparkyBumper.children
.whereType<SparkyBumperBlinkingBehavior>()
.single,
sparkyBumper.children.whereType<BlinkingBehavior>().single,
isNotNull,
);
});

Loading…
Cancel
Save