feat: state for showing ShareDisplay at Backbox

pull/406/head
RuiAlonso 3 years ago
parent 76041a327e
commit 94821f401f

@ -8,6 +8,7 @@ import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
/// {@template backbox} /// {@template backbox}
@ -18,14 +19,18 @@ class Backbox extends PositionComponent with ZIndex {
Backbox({ Backbox({
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository, required ShareRepository shareRepository,
}) : _bloc = BackboxBloc(leaderboardRepository: leaderboardRepository); }) : _shareRepository = shareRepository,
_bloc = BackboxBloc(leaderboardRepository: leaderboardRepository);
/// {@macro backbox} /// {@macro backbox}
@visibleForTesting @visibleForTesting
Backbox.test({ Backbox.test({
required BackboxBloc bloc, required BackboxBloc bloc,
}) : _bloc = bloc; required ShareRepository shareRepository,
}) : _bloc = bloc,
_shareRepository = shareRepository;
final ShareRepository _shareRepository;
late final Component _display; late final Component _display;
final BackboxBloc _bloc; final BackboxBloc _bloc;
late StreamSubscription<BackboxState> _subscription; late StreamSubscription<BackboxState> _subscription;
@ -77,6 +82,18 @@ class Backbox extends PositionComponent with ZIndex {
); );
} else if (state is InitialsSuccessState) { } else if (state is InitialsSuccessState) {
_display.add(InitialsSubmissionSuccessDisplay()); _display.add(InitialsSubmissionSuccessDisplay());
} else if (state is ShareState) {
_display.add(
ShareDisplay(
onShare: (platform) {
final url = _shareRepository.shareText(
value: state.score.toString(),
platform: platform,
);
openLink(url);
},
),
);
} else if (state is InitialsFailureState) { } else if (state is InitialsFailureState) {
_display.add(InitialsSubmissionFailureDisplay()); _display.add(InitialsSubmissionFailureDisplay());
} }

@ -18,6 +18,7 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
super(LoadingState()) { super(LoadingState()) {
on<PlayerInitialsRequested>(_onPlayerInitialsRequested); on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted); on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
on<ShareScoreRequested>(_onScoreShareRequested);
on<LeaderboardRequested>(_onLeaderboardRequested); on<LeaderboardRequested>(_onLeaderboardRequested);
} }
@ -55,6 +56,19 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
} }
} }
Future<void> _onScoreShareRequested(
ShareScoreRequested event,
Emitter<BackboxState> emit,
) async {
emit(
ShareState(
initials: event.initials,
score: event.score,
character: event.character,
),
);
}
Future<void> _onLeaderboardRequested( Future<void> _onLeaderboardRequested(
LeaderboardRequested event, LeaderboardRequested event,
Emitter<BackboxState> emit, Emitter<BackboxState> emit,

@ -52,6 +52,30 @@ class PlayerInitialsSubmitted extends BackboxEvent {
List<Object?> get props => [score, initials, character]; List<Object?> get props => [score, initials, character];
} }
/// {@template share_score_requested}
/// Event that request the user to share score and initials.
/// {@endtemplate}
class ShareScoreRequested extends BackboxEvent {
/// {@macro share_score_requested}
const ShareScoreRequested({
required this.score,
required this.initials,
required this.character,
});
/// Player's score.
final int score;
/// Player's initials.
final String initials;
/// Player's character.
final CharacterTheme character;
@override
List<Object?> get props => [score, initials, character];
}
/// Event that triggers the fetching of the leaderboard /// Event that triggers the fetching of the leaderboard
class LeaderboardRequested extends BackboxEvent { class LeaderboardRequested extends BackboxEvent {
@override @override

@ -65,3 +65,27 @@ class InitialsFailureState extends BackboxState {
@override @override
List<Object?> get props => []; List<Object?> get props => [];
} }
/// {@template share_state}
/// State when the user is sharing their score.
/// {@endtemplate}
class ShareState extends BackboxState {
/// {@macro share_state}
const ShareState({
required this.score,
required this.initials,
required this.character,
}) : super();
/// Player's score.
final int score;
/// Player's initials.
final String initials;
/// Player's character.
final CharacterTheme character;
@override
List<Object?> get props => [score, initials, character];
}

@ -19,6 +19,9 @@ import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:pinball_theme/pinball_theme.dart' as theme;
import 'package:pinball_ui/pinball_ui.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:share_repository/share_repository.dart';
class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents {
final character = theme.DashTheme(); final character = theme.DashTheme();
@ -33,6 +36,8 @@ class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents {
character.leaderboardIcon.keyName, character.leaderboardIcon.keyName,
Assets.images.backbox.marquee.keyName, Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName, Assets.images.backbox.displayDivider.keyName,
Assets.images.backbox.button.facebook.keyName,
Assets.images.backbox.button.twitter.keyName,
]); ]);
} }
@ -69,6 +74,14 @@ class _MockBackboxBloc extends Mock implements BackboxBloc {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
} }
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockTapDownInfo extends Mock implements TapDownInfo {}
class _MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
class _MockAppLocalizations extends Mock implements AppLocalizations { class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
String get score => ''; String get score => '';
@ -96,6 +109,15 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
String get loading => ''; String get loading => '';
@override
String get letEveryone => '';
@override
String get bySharingYourScore => '';
@override
String get socialMediaAccount => '';
} }
void main() { void main() {
@ -118,7 +140,10 @@ void main() {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
);
await game.pump(backbox); await game.pump(backbox);
expect(game.descendants(), contains(backbox)); expect(game.descendants(), contains(backbox));
}, },
@ -127,7 +152,10 @@ void main() {
flameTester.test( flameTester.test(
'adds LeaderboardRequested when loaded', 'adds LeaderboardRequested when loaded',
(game) async { (game) async {
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
);
await game.pump(backbox); await game.pump(backbox);
verify(() => bloc.add(LeaderboardRequested())).called(1); verify(() => bloc.add(LeaderboardRequested())).called(1);
@ -142,7 +170,10 @@ void main() {
..followVector2(Vector2(0, -130)) ..followVector2(Vector2(0, -130))
..zoom = 6; ..zoom = 6;
await game.pump( await game.pump(
Backbox.test(bloc: bloc), Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
),
); );
await tester.pump(); await tester.pump();
}, },
@ -161,6 +192,7 @@ void main() {
bloc: BackboxBloc( bloc: BackboxBloc(
leaderboardRepository: _MockLeaderboardRepository(), leaderboardRepository: _MockLeaderboardRepository(),
), ),
shareRepository: _MockShareRepository(),
); );
await game.pump(backbox); await game.pump(backbox);
backbox.requestInitials( backbox.requestInitials(
@ -189,7 +221,10 @@ void main() {
Stream<BackboxState>.empty(), Stream<BackboxState>.empty(),
initialState: state, initialState: state,
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
);
await game.pump(backbox); await game.pump(backbox);
game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {}); game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {});
@ -213,7 +248,10 @@ void main() {
Stream<BackboxState>.empty(), Stream<BackboxState>.empty(),
initialState: InitialsSuccessState(), initialState: InitialsSuccessState(),
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
);
await game.pump(backbox); await game.pump(backbox);
expect( expect(
@ -234,7 +272,10 @@ void main() {
Stream<BackboxState>.empty(), Stream<BackboxState>.empty(),
initialState: InitialsFailureState(), initialState: InitialsFailureState(),
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
);
await game.pump(backbox); await game.pump(backbox);
expect( expect(
@ -247,6 +288,173 @@ void main() {
}, },
); );
group('ShareDisplay', () {
late UrlLauncherPlatform urlLauncher;
setUp(() async {
urlLauncher = _MockUrlLauncher();
UrlLauncherPlatform.instance = urlLauncher;
});
flameTester.test(
'added on ShareState',
(game) async {
final state = ShareState(
score: 100,
initials: 'AAA',
character: theme.AndroidTheme(),
);
whenListen(
bloc,
const Stream<InitialsSuccessState>.empty(),
initialState: state,
);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
);
await game.pump(backbox);
expect(
game.descendants().whereType<ShareDisplay>().length,
equals(1),
);
},
);
flameTester.test(
'open Facebook link when sharing with Facebook',
(game) async {
final state = ShareState(
score: 100,
initials: 'AAA',
character: theme.AndroidTheme(),
);
whenListen(
bloc,
Stream.value(state),
initialState: state,
);
final shareRepository = _MockShareRepository();
const fakeUrl = 'fakeUrl';
when(
() => () => shareRepository.shareText(
value: any(),
platform: SharePlatform.facebook,
),
).thenReturn(() => fakeUrl);
when(() => urlLauncher.canLaunch(any()))
.thenAnswer((_) async => true);
when(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: shareRepository,
);
await game.pump(backbox);
final facebookButton =
game.descendants().whereType<FacebookButtonComponent>().first;
facebookButton.onTapDown(_MockTapDownInfo());
verify(
() => shareRepository.shareText(
value: state.score.toString(),
platform: SharePlatform.facebook,
),
).called(1);
verify(
() => urlLauncher.launch(
fakeUrl,
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
);
},
);
flameTester.test(
'open Twitter link when sharing with Twitter',
(game) async {
final state = ShareState(
score: 100,
initials: 'AAA',
character: theme.AndroidTheme(),
);
whenListen(
bloc,
Stream.value(state),
initialState: state,
);
final shareRepository = _MockShareRepository();
const fakeUrl = 'fakeUrl';
when(
() => () => shareRepository.shareText(
value: any(),
platform: SharePlatform.twitter,
),
).thenReturn(() => fakeUrl);
when(() => urlLauncher.canLaunch(any()))
.thenAnswer((_) async => true);
when(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: shareRepository,
);
await game.pump(backbox);
final facebookButton =
game.descendants().whereType<TwitterButtonComponent>().first;
facebookButton.onTapDown(_MockTapDownInfo());
verify(
() => shareRepository.shareText(
value: state.score.toString(),
platform: SharePlatform.twitter,
),
).called(1);
verify(
() => urlLauncher.launch(
fakeUrl,
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
);
},
);
});
flameTester.test( flameTester.test(
'adds LeaderboardDisplay on LeaderboardSuccessState', 'adds LeaderboardDisplay on LeaderboardSuccessState',
(game) async { (game) async {
@ -256,7 +464,10 @@ void main() {
initialState: LeaderboardSuccessState(entries: const []), initialState: LeaderboardSuccessState(entries: const []),
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
);
await game.pump(backbox); await game.pump(backbox);
expect( expect(
@ -276,7 +487,10 @@ void main() {
initialState: LoadingState(), initialState: LoadingState(),
); );
final backbox = Backbox.test(bloc: bloc); final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
);
await game.pump(backbox); await game.pump(backbox);
backbox.removeFromParent(); backbox.removeFromParent();

@ -89,6 +89,30 @@ void main() {
); );
}); });
group('ShareScoreRequested', () {
blocTest<BackboxBloc, BackboxState>(
'emits ShareState',
setUp: () {
leaderboardRepository = _MockLeaderboardRepository();
},
build: () => BackboxBloc(leaderboardRepository: leaderboardRepository),
act: (bloc) => bloc.add(
ShareScoreRequested(
score: 100,
initials: 'AAA',
character: AndroidTheme(),
),
),
expect: () => [
ShareState(
score: 100,
initials: 'AAA',
character: AndroidTheme(),
),
],
);
});
group('LeaderboardRequested', () { group('LeaderboardRequested', () {
blocTest<BackboxBloc, BackboxState>( blocTest<BackboxBloc, BackboxState>(
'adds [LoadingState, LeaderboardSuccessState] when request succeeds', 'adds [LoadingState, LeaderboardSuccessState] when request succeeds',

@ -123,6 +123,87 @@ void main() {
}); });
}); });
group('ScoreShareRequested', () {
test('can be instantiated', () {
expect(
ShareScoreRequested(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
isNotNull,
);
});
test('supports value comparison', () {
expect(
ShareScoreRequested(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
equals(
ShareScoreRequested(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
),
);
expect(
ShareScoreRequested(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
isNot(
equals(
ShareScoreRequested(
score: 1,
initials: 'AAA',
character: AndroidTheme(),
),
),
),
);
expect(
ShareScoreRequested(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
isNot(
equals(
ShareScoreRequested(
score: 0,
initials: 'AAA',
character: SparkyTheme(),
),
),
),
);
expect(
ShareScoreRequested(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
isNot(
equals(
ShareScoreRequested(
score: 0,
initials: 'BBB',
character: AndroidTheme(),
),
),
),
);
});
});
group('LeaderboardRequested', () { group('LeaderboardRequested', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(LeaderboardRequested(), isNotNull); expect(LeaderboardRequested(), isNotNull);

@ -132,5 +132,35 @@ void main() {
}); });
}); });
}); });
group('ShareState', () {
test('can be instantiated', () {
expect(
ShareState(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
isNotNull,
);
});
test('supports value comparison', () {
expect(
ShareState(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
equals(
ShareState(
score: 0,
initials: 'AAA',
character: AndroidTheme(),
),
),
);
});
});
}); });
} }

Loading…
Cancel
Save