Skip to content

Commit

Permalink
Merge branch 'main' into feat/answer-word
Browse files Browse the repository at this point in the history
  • Loading branch information
jsgalarraga authored Apr 17, 2024
2 parents 2b204b5 + 385d9a2 commit cee4505
Show file tree
Hide file tree
Showing 12 changed files with 508 additions and 61 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: io_crossword

concurrency:
group: $-$
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
Expand Down
49 changes: 49 additions & 0 deletions lib/game_intro/bloc/game_intro_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:api_client/api_client.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:game_domain/game_domain.dart';

part 'game_intro_event.dart';
part 'game_intro_state.dart';

class GameIntroBloc extends Bloc<GameIntroEvent, GameIntroState> {
GameIntroBloc({
required LeaderboardResource leaderboardResource,
}) : _leaderboardResource = leaderboardResource,
super(const GameIntroState()) {
on<GameIntroPlayerCreated>(_onGameIntroPlayerCreated);
}

final LeaderboardResource _leaderboardResource;

Future<void> _onGameIntroPlayerCreated(
GameIntroPlayerCreated event,
Emitter<GameIntroState> emit,
) async {
try {
emit(
const GameIntroState(
status: GameIntroPlayerCreationStatus.inProgress,
),
);

await _leaderboardResource.createScore(
initials: event.initials,
mascot: event.mascot!,
);

emit(
const GameIntroState(
status: GameIntroPlayerCreationStatus.success,
),
);
} catch (error, stackTrace) {
addError(error, stackTrace);
emit(
const GameIntroState(
status: GameIntroPlayerCreationStatus.failure,
),
);
}
}
}
18 changes: 18 additions & 0 deletions lib/game_intro/bloc/game_intro_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
part of 'game_intro_bloc.dart';

abstract class GameIntroEvent extends Equatable {
const GameIntroEvent();
}

class GameIntroPlayerCreated extends GameIntroEvent {
const GameIntroPlayerCreated({
required this.initials,
required this.mascot,
});

final String initials;
final Mascots? mascot;

@override
List<Object?> get props => [initials, mascot];
}
19 changes: 19 additions & 0 deletions lib/game_intro/bloc/game_intro_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
part of 'game_intro_bloc.dart';

enum GameIntroPlayerCreationStatus {
initial,
inProgress,
success,
failure,
}

class GameIntroState extends Equatable {
const GameIntroState({
this.status = GameIntroPlayerCreationStatus.initial,
});

final GameIntroPlayerCreationStatus status;

@override
List<Object?> get props => [status];
}
63 changes: 46 additions & 17 deletions lib/game_intro/view/game_intro_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import 'package:api_client/api_client.dart';
import 'package:flow_builder/flow_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:game_domain/game_domain.dart';
import 'package:io_crossword/crossword/crossword.dart';
import 'package:io_crossword/game_intro/bloc/game_intro_bloc.dart';
import 'package:io_crossword/how_to_play/how_to_play.dart';
import 'package:io_crossword/initials/view/initials_page.dart';
import 'package:io_crossword/l10n/l10n.dart';
import 'package:io_crossword/team_selection/team_selection.dart';
import 'package:io_crossword/welcome/welcome.dart';

Expand All @@ -14,7 +15,12 @@ class GameIntroPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return const GameIntroView();
return BlocProvider<GameIntroBloc>(
create: (_) => GameIntroBloc(
leaderboardResource: context.read<LeaderboardResource>(),
),
child: const GameIntroView(),
);
}
}

Expand All @@ -25,7 +31,9 @@ enum GameIntroStatus {
howToPlay,
}

@visibleForTesting
class GameIntroView extends StatelessWidget {
@visibleForTesting
const GameIntroView({
super.key,
@visibleForTesting FlowController<GameIntroStatus>? flowController,
Expand All @@ -35,23 +43,44 @@ class GameIntroView extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Material(
child: FlowBuilder<GameIntroStatus>(
controller: _flowController,
state: _flowController == null ? GameIntroStatus.welcome : null,
onGeneratePages: onGenerateGameIntroPages,
onComplete: (state) {
// coverage:ignore-start
// TODO(alestiago): Handle this creation.
// https://very-good-ventures-team.monday.com/boards/6004820050/pulses/6422014818
context.read<LeaderboardResource>().createScore(
initials: 'AAA',
mascot: Mascots.dash,
final l10n = context.l10n;

return BlocListener<GameIntroBloc, GameIntroState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
switch (state.status) {
case GameIntroPlayerCreationStatus.initial:
case GameIntroPlayerCreationStatus.inProgress:
break;
case GameIntroPlayerCreationStatus.success:
Navigator.of(context).pushReplacement(CrosswordPage.route());
case GameIntroPlayerCreationStatus.failure:
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(l10n.errorPromptText),
behavior: SnackBarBehavior.floating,
),
);
// coverage:ignore-end
}
},
child: Material(
child: FlowBuilder<GameIntroStatus>(
controller: _flowController,
state: _flowController == null ? GameIntroStatus.welcome : null,
onGeneratePages: onGenerateGameIntroPages,
onComplete: (state) {
final crosswordBloc = context.read<CrosswordBloc>().state;

Navigator.of(context).pushReplacement(CrosswordPage.route());
},
context.read<GameIntroBloc>().add(
GameIntroPlayerCreated(
initials: crosswordBloc.initials,
mascot: crosswordBloc.mascot,
),
);
},
),
),
);
}
Expand Down
19 changes: 15 additions & 4 deletions lib/how_to_play/view/how_to_play_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flow_builder/flow_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_crossword/game_intro/bloc/game_intro_bloc.dart';
import 'package:io_crossword/game_intro/game_intro.dart';
import 'package:io_crossword/l10n/l10n.dart';

Expand All @@ -16,7 +18,9 @@ class HowToPlayPage extends StatelessWidget {
}
}

@visibleForTesting
class HowToPlayView extends StatelessWidget {
@visibleForTesting
const HowToPlayView({super.key});

void _onPlay(BuildContext context) {
Expand All @@ -27,12 +31,19 @@ class HowToPlayView extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = context.l10n;

final isCreating = context.select(
(GameIntroBloc bloc) =>
bloc.state.status == GameIntroPlayerCreationStatus.inProgress,
);

return Scaffold(
body: Center(
child: OutlinedButton(
onPressed: () => _onPlay(context),
child: Text(l10n.playNow),
),
child: isCreating
? const CircularProgressIndicator()
: OutlinedButton(
onPressed: () => _onPlay(context),
child: Text(l10n.playNow),
),
),
);
}
Expand Down
16 changes: 10 additions & 6 deletions packages/io_crossword_ui/lib/src/widgets/io_app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,18 @@ class IoAppBar extends StatelessWidget implements PreferredSizeWidget {
),
IoLayoutData.large => Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Stack(
children: [
_IoCrosswordLogo(
crossword: crossword,
Center(child: titleWidget),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_IoCrosswordLogo(
crossword: crossword,
),
if (actions != null) actions(context),
],
),
titleWidget,
if (actions != null) actions(context),
],
),
),
Expand Down
80 changes: 80 additions & 0 deletions test/game_intro/bloc/game_intro_bloc_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// ignore_for_file: prefer_const_constructors

import 'package:api_client/api_client.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_domain/game_domain.dart';
import 'package:io_crossword/game_intro/bloc/game_intro_bloc.dart';
import 'package:mocktail/mocktail.dart';

class _MockLeaderboardResource extends Mock implements LeaderboardResource {}

void main() {
group('$GameIntroBloc', () {
late LeaderboardResource leaderboardResource;
late GameIntroBloc bloc;

setUp(() {
leaderboardResource = _MockLeaderboardResource();
bloc = GameIntroBloc(
leaderboardResource: leaderboardResource,
);
});

group('$GameIntroPlayerCreated', () {
blocTest<GameIntroBloc, GameIntroState>(
'emits [inProgress, success]',
setUp: () {
when(
() => leaderboardResource.createScore(
initials: 'ABC',
mascot: Mascots.android,
),
).thenAnswer((_) async => []);
},
build: () => bloc,
act: (bloc) => bloc.add(
GameIntroPlayerCreated(
initials: 'ABC',
mascot: Mascots.android,
),
),
expect: () => [
GameIntroState(
status: GameIntroPlayerCreationStatus.inProgress,
),
GameIntroState(
status: GameIntroPlayerCreationStatus.success,
),
],
);

blocTest<GameIntroBloc, GameIntroState>(
'emits [inProgress, failure] when createScore throws exception',
setUp: () {
when(
() => leaderboardResource.createScore(
initials: 'ABC',
mascot: Mascots.android,
),
).thenThrow(Exception());
},
build: () => bloc,
act: (bloc) => bloc.add(
GameIntroPlayerCreated(
initials: 'ABC',
mascot: Mascots.android,
),
),
expect: () => [
GameIntroState(
status: GameIntroPlayerCreationStatus.inProgress,
),
GameIntroState(
status: GameIntroPlayerCreationStatus.failure,
),
],
);
});
});
}
38 changes: 38 additions & 0 deletions test/game_intro/bloc/game_intro_event_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// ignore_for_file: prefer_const_constructors

import 'package:flutter_test/flutter_test.dart';
import 'package:game_domain/game_domain.dart';
import 'package:io_crossword/game_intro/bloc/game_intro_bloc.dart';

void main() {
group('$GameIntroEvent', () {
group('$GameIntroPlayerCreated', () {
test('supports equality', () {
expect(
GameIntroPlayerCreated(mascot: Mascots.android, initials: 'ABC'),
equals(
GameIntroPlayerCreated(mascot: Mascots.android, initials: 'ABC'),
),
);

expect(
GameIntroPlayerCreated(mascot: Mascots.android, initials: 'ABC'),
isNot(
equals(
GameIntroPlayerCreated(mascot: Mascots.dash, initials: 'ABC'),
),
),
);

expect(
GameIntroPlayerCreated(mascot: Mascots.android, initials: 'ABC'),
isNot(
equals(
GameIntroPlayerCreated(mascot: Mascots.android, initials: 'CCC'),
),
),
);
});
});
});
}
Loading

0 comments on commit cee4505

Please sign in to comment.