From 8dc76ac07676317ee2c2bb3625ea7d90122a24ef Mon Sep 17 00:00:00 2001 From: Sandip Date: Fri, 5 Jul 2024 10:42:11 +0530 Subject: [PATCH] feat: add view, and widgets for sudoku board, input --- lib/app/view/app.dart | 5 +- lib/sudoku/models/models.dart | 2 + lib/sudoku/models/sudoku_board_size.dart | 12 ++ lib/sudoku/models/sudoku_input_size.dart | 12 ++ lib/sudoku/sudoku.dart | 3 + lib/sudoku/view/sudoku_page.dart | 175 +++++++++++++++++++ lib/sudoku/view/view.dart | 1 + lib/sudoku/widgets/sudoku_block.dart | 93 ++++++++++ lib/sudoku/widgets/sudoku_board.dart | 72 ++++++++ lib/sudoku/widgets/sudoku_board_divider.dart | 35 ++++ lib/sudoku/widgets/sudoku_input.dart | 116 ++++++++++++ lib/sudoku/widgets/widgets.dart | 4 + test/app/view/app_test.dart | 6 +- test/helpers/pump_app.dart | 19 +- test/sudoku/view/sudoku_page_test.dart | 118 +++++++++++++ test/sudoku/widgets/sudoku_block_test.dart | 122 +++++++++++++ test/sudoku/widgets/sudoku_input_test.dart | 65 +++++++ 17 files changed, 850 insertions(+), 10 deletions(-) create mode 100644 lib/sudoku/models/models.dart create mode 100644 lib/sudoku/models/sudoku_board_size.dart create mode 100644 lib/sudoku/models/sudoku_input_size.dart create mode 100644 lib/sudoku/view/sudoku_page.dart create mode 100644 lib/sudoku/view/view.dart create mode 100644 lib/sudoku/widgets/sudoku_block.dart create mode 100644 lib/sudoku/widgets/sudoku_board.dart create mode 100644 lib/sudoku/widgets/sudoku_board_divider.dart create mode 100644 lib/sudoku/widgets/sudoku_input.dart create mode 100644 lib/sudoku/widgets/widgets.dart create mode 100644 test/sudoku/view/sudoku_page_test.dart create mode 100644 test/sudoku/widgets/sudoku_block_test.dart create mode 100644 test/sudoku/widgets/sudoku_input_test.dart diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 9665756..27812c4 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:sudoku/counter/counter.dart'; import 'package:sudoku/l10n/l10n.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; import 'package:sudoku/theme/theme.dart'; class App extends StatelessWidget { @@ -11,9 +11,10 @@ class App extends StatelessWidget { return MaterialApp( theme: SudokuTheme.light, darkTheme: SudokuTheme.dark, + themeMode: ThemeMode.light, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const CounterPage(), + home: const SudokuPage(), ); } } diff --git a/lib/sudoku/models/models.dart b/lib/sudoku/models/models.dart new file mode 100644 index 0000000..c996d5e --- /dev/null +++ b/lib/sudoku/models/models.dart @@ -0,0 +1,2 @@ +export 'sudoku_board_size.dart'; +export 'sudoku_input_size.dart'; diff --git a/lib/sudoku/models/sudoku_board_size.dart b/lib/sudoku/models/sudoku_board_size.dart new file mode 100644 index 0000000..e043623 --- /dev/null +++ b/lib/sudoku/models/sudoku_board_size.dart @@ -0,0 +1,12 @@ +/// Determines the size of the Sudoku Board depending upon the +/// screen size. +abstract class SudokuBoardSize { + /// Sudoku board size for small layout. + static const double small = 360; + + /// Sudoku board size for medium layout. + static const double medium = 495; + + /// Sudoku board size for large layout. + static const double large = 558; +} diff --git a/lib/sudoku/models/sudoku_input_size.dart b/lib/sudoku/models/sudoku_input_size.dart new file mode 100644 index 0000000..ccedf97 --- /dev/null +++ b/lib/sudoku/models/sudoku_input_size.dart @@ -0,0 +1,12 @@ +/// Determines the size of the Sudoku Input depending upon the +/// screen size. +abstract class SudokuInputSize { + /// Sudoku input size for small layout. + static const double small = 72; + + /// Sudoku input size for medium layout. + static const double medium = 84.6; + + /// Sudoku input size for large layout. + static const double large = 95.4; +} diff --git a/lib/sudoku/sudoku.dart b/lib/sudoku/sudoku.dart index f979921..8c02f88 100644 --- a/lib/sudoku/sudoku.dart +++ b/lib/sudoku/sudoku.dart @@ -1 +1,4 @@ export 'bloc/sudoku_bloc.dart'; +export 'models/models.dart'; +export 'view/view.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/sudoku/view/sudoku_page.dart b/lib/sudoku/view/sudoku_page.dart new file mode 100644 index 0000000..3134516 --- /dev/null +++ b/lib/sudoku/view/sudoku_page.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/l10n/l10n.dart'; +import 'package:sudoku/layout/layout.dart'; +import 'package:sudoku/models/models.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; +import 'package:sudoku/typography/typography.dart'; + +class SudokuPage extends StatelessWidget { + const SudokuPage({super.key}); + + static const _generated = [ + [-1, -1, -1, 8, -1, -1, -1, -1, 9], + [-1, 1, 9, -1, -1, 5, 8, 3, -1], + [-1, 4, 3, -1, 1, -1, -1, -1, 7], + [4, -1, -1, 1, 5, -1, -1, -1, 3], + [-1, -1, 2, 7, -1, 4, -1, 1, -1], + [-1, 8, -1, -1, 9, -1, 6, -1, -1], + [-1, 7, -1, -1, -1, 6, 3, -1, -1], + [-1, 3, -1, -1, 7, -1, -1, 8, -1], + [9, -1, 4, 5, -1, -1, -1, -1, 1], + ]; + + static const _answer = [ + [2, 5, 6, 8, 3, 7, 1, 4, 9], + [7, 1, 9, 4, 2, 5, 8, 3, 6], + [8, 4, 3, 6, 1, 9, 2, 5, 7], + [4, 6, 7, 1, 5, 8, 9, 2, 3], + [3, 9, 2, 7, 6, 4, 5, 1, 8], + [5, 8, 1, 3, 9, 2, 6, 7, 4], + [1, 7, 8, 2, 4, 6, 3, 9, 5], + [6, 3, 5, 9, 7, 1, 4, 8, 2], + [9, 2, 4, 5, 8, 3, 7, 6, 1], + ]; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SudokuBloc( + sudoku: Sudoku.fromRawData(_generated, _answer), + ), + child: const SudokuView(), + ); + } +} + +class SudokuView extends StatelessWidget { + const SudokuView({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final sudoku = context.select( + (SudokuBloc bloc) => bloc.state.sudoku, + ); + + return ResponsiveLayoutBuilder( + small: (_, child) => child!, + medium: (_, child) => child!, + large: (_, __) => Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + const ResponsiveGap( + large: 246, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 250, + child: Text( + l10n.sudokuAppBarTitle, + style: SudokuTextStyle.headline1.copyWith( + fontWeight: SudokuFontWeight.black, + ), + ), + ), + const SizedBox( + width: 60, + ), + const SudokuBoardView( + layoutSize: ResponsiveLayoutSize.large, + ), + const SizedBox( + width: 96, + ), + SudokuInput( + sudokuDimension: sudoku.getDimesion(), + ), + ], + ), + const ResponsiveGap( + large: 246, + ), + ], + ), + ), + ), + child: (layoutSize) { + return Scaffold( + appBar: AppBar( + title: Text(l10n.sudokuAppBarTitle), + ), + body: SingleChildScrollView( + child: Column( + children: [ + const ResponsiveGap( + small: 24, + medium: 32, + ), + Center( + child: SudokuBoardView(layoutSize: layoutSize), + ), + const ResponsiveGap( + small: 32, + medium: 56, + ), + Center( + child: SudokuInput( + sudokuDimension: sudoku.getDimesion(), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +@visibleForTesting +class SudokuBoardView extends StatelessWidget { + const SudokuBoardView({ + required this.layoutSize, + super.key, + }); + final ResponsiveLayoutSize layoutSize; + + @override + Widget build(BuildContext context) { + final dimension = context.select( + (SudokuBloc bloc) => bloc.state.sudoku.getDimesion(), + ); + + final blockSize = switch (layoutSize) { + ResponsiveLayoutSize.small => SudokuBoardSize.small / dimension, + ResponsiveLayoutSize.medium => SudokuBoardSize.medium / dimension, + ResponsiveLayoutSize.large => SudokuBoardSize.large / dimension, + }; + + return BlocBuilder( + buildWhen: (previous, current) => previous.sudoku != current.sudoku, + builder: (context, state) { + return SudokuBoard( + blocks: [ + for (final block in state.sudoku.blocks) + Positioned( + key: Key( + 'sudoku_board_view_${block.position.x}_${block.position.y}', + ), + top: block.position.x * blockSize, + left: block.position.y * blockSize, + child: SudokuBlock( + block: block, + state: state, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/sudoku/view/view.dart b/lib/sudoku/view/view.dart new file mode 100644 index 0000000..c3b3560 --- /dev/null +++ b/lib/sudoku/view/view.dart @@ -0,0 +1 @@ +export 'sudoku_page.dart'; diff --git a/lib/sudoku/widgets/sudoku_block.dart b/lib/sudoku/widgets/sudoku_block.dart new file mode 100644 index 0000000..8ae64db --- /dev/null +++ b/lib/sudoku/widgets/sudoku_block.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/layout/layout.dart'; +import 'package:sudoku/models/models.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; +import 'package:sudoku/typography/typography.dart'; + +/// {@template sudoku_block} +/// Displays the Sudoku [block] based upon the current [state]. +/// {@endtemplate} +class SudokuBlock extends StatelessWidget { + /// {@macro sudoku_block} + const SudokuBlock({ + required this.block, + required this.state, + super.key, + }); + + /// The [Block] to be displayed. + final Block block; + + /// The state of the sudoku. + final SudokuState state; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final dimension = state.sudoku.getDimesion(); + + final selectedBlock = context.select( + (SudokuBloc bloc) => bloc.state.currentSelectedBlock, + ); + final highlightedBlocks = context.select( + (SudokuBloc bloc) => bloc.state.highlightedBlocks, + ); + + // Comparing with the current block's position, otherwise + // this will return false result, when the `currentValue` is updated. + final isBlockSelected = selectedBlock?.position == block.position; + + // Checking with the current block's position, otherwise + // this will return false when the `currentValue` is updated, hence this + // block will no longer remain highlighted. + final isBlockHighlighted = highlightedBlocks + .map((block) => block.position) + .contains(block.position); + + return ResponsiveLayoutBuilder( + small: (_, child) => SizedBox.square( + dimension: SudokuBoardSize.small / dimension, + key: Key('sudoku_block_small_${block.position.x}_${block.position.y}'), + child: child, + ), + medium: (_, child) => SizedBox.square( + dimension: SudokuBoardSize.medium / dimension, + key: Key('sudoku_block_medium_${block.position.x}_${block.position.y}'), + child: child, + ), + large: (_, child) => SizedBox.square( + dimension: SudokuBoardSize.large / dimension, + key: Key('sudoku_block_large_${block.position.x}_${block.position.y}'), + child: child, + ), + child: (_) { + return GestureDetector( + onTap: () { + context.read().add(SudokuBlockSelected(block)); + }, + child: DecoratedBox( + decoration: BoxDecoration( + color: isBlockSelected + ? theme.primaryColorLight + : isBlockHighlighted + ? theme.splashColor.withOpacity(0.27) + : null, + border: Border.all( + color: theme.highlightColor, + ), + ), + child: Center( + child: Text( + block.currentValue != -1 ? '${block.currentValue}' : '', + style: SudokuTextStyle.bodyText1.copyWith( + color: block.isGenerated ? null : theme.colorScheme.secondary, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/sudoku/widgets/sudoku_board.dart b/lib/sudoku/widgets/sudoku_board.dart new file mode 100644 index 0000000..b1100af --- /dev/null +++ b/lib/sudoku/widgets/sudoku_board.dart @@ -0,0 +1,72 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:sudoku/layout/layout.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; + +/// {@template sudoku_board} +/// Displays the Sudoku board in a [Stack] containing [blocks]. +/// {@endtemplate} +class SudokuBoard extends StatelessWidget { + /// {@macro sudoku_board} + const SudokuBoard({required this.blocks, super.key}); + + /// The blocks to be displayed on the Sudoku board. + final List blocks; + + @override + Widget build(BuildContext context) { + return ResponsiveLayoutBuilder( + small: (_, child) => SizedBox.square( + key: const Key('sudoku_board_small'), + dimension: SudokuBoardSize.small, + child: child, + ), + medium: (_, child) => SizedBox.square( + key: const Key('sudoku_board_medium'), + dimension: SudokuBoardSize.medium, + child: child, + ), + large: (_, child) => SizedBox.square( + key: const Key('sudoku_board_large'), + dimension: SudokuBoardSize.large, + child: child, + ), + child: (currentSize) { + final boardSize = switch (currentSize) { + ResponsiveLayoutSize.small => SudokuBoardSize.small, + ResponsiveLayoutSize.medium => SudokuBoardSize.medium, + ResponsiveLayoutSize.large => SudokuBoardSize.large, + }; + + final boardDimension = sqrt(blocks.length).toInt(); + final subGridDimension = sqrt(boardDimension).toInt(); + + final blockSize = boardSize / boardDimension; + final subGridSize = subGridDimension * blockSize; + return Stack( + children: [ + ...blocks, + IgnorePointer( + child: SudokuBoardDivider( + dimension: boardSize, + width: 1.4, + ), + ), + for (var i = 0; i < boardDimension; i++) + Positioned( + top: (i % subGridDimension) * subGridSize, + left: (i ~/ subGridDimension) * subGridSize, + child: IgnorePointer( + child: SudokuBoardDivider( + dimension: subGridSize, + width: 0.8, + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/sudoku/widgets/sudoku_board_divider.dart b/lib/sudoku/widgets/sudoku_board_divider.dart new file mode 100644 index 0000000..1991739 --- /dev/null +++ b/lib/sudoku/widgets/sudoku_board_divider.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +/// {@template sudoku_board_divider} +/// Builds dividers for Sudoku board, and sub-grids. +/// {@endtemplate} +class SudokuBoardDivider extends StatelessWidget { + /// {@macro sudoku_board_divider} + const SudokuBoardDivider({ + required this.dimension, + required this.width, + super.key, + }); + + /// Dimension of the square where boarder will be painted. + final double dimension; + + /// Width of the border. + final double width; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SizedBox.square( + dimension: dimension, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: theme.dividerColor, + width: width, + ), + ), + ), + ); + } +} diff --git a/lib/sudoku/widgets/sudoku_input.dart b/lib/sudoku/widgets/sudoku_input.dart new file mode 100644 index 0000000..9006b85 --- /dev/null +++ b/lib/sudoku/widgets/sudoku_input.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/layout/layout.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; +import 'package:sudoku/typography/typography.dart'; + +/// {@template sudoku_input} +/// The input blocks for the Sudoku puzzle. +/// {@endtemplate} +class SudokuInput extends StatelessWidget { + /// {@macro sudoku_input} + const SudokuInput({ + required this.sudokuDimension, + super.key, + }); + + /// Dimension of the Sudoku puzzle. + final int sudokuDimension; + + @override + Widget build(BuildContext context) { + return ResponsiveLayoutBuilder( + small: (_, __) => SizedBox.fromSize( + key: const Key('sudoku_input_small'), + size: Size( + SudokuInputSize.small * (sudokuDimension / 2).ceil(), + SudokuInputSize.small * 2, + ), + child: _SudokuInputView( + sudokuDimension: sudokuDimension, + inputsPerRow: (sudokuDimension / 2).ceil(), + inputSize: SudokuInputSize.small, + ), + ), + medium: (_, __) => SizedBox.fromSize( + key: const Key('sudoku_input_medium'), + size: Size( + SudokuInputSize.medium * (sudokuDimension / 2).ceil(), + SudokuInputSize.medium * 2, + ), + child: _SudokuInputView( + sudokuDimension: sudokuDimension, + inputsPerRow: (sudokuDimension / 2).ceil(), + inputSize: SudokuInputSize.medium, + ), + ), + large: (_, __) => SizedBox.fromSize( + key: const Key('sudoku_input_large'), + size: Size( + SudokuInputSize.large * (sudokuDimension / 3).ceil(), + SudokuInputSize.large * 3, + ), + child: _SudokuInputView( + sudokuDimension: sudokuDimension, + inputsPerRow: (sudokuDimension / 3).ceil(), + inputSize: SudokuInputSize.large, + ), + ), + ); + } +} + +class _SudokuInputView extends StatelessWidget { + const _SudokuInputView({ + required this.sudokuDimension, + required this.inputsPerRow, + required this.inputSize, + }); + + final int sudokuDimension; + final int inputsPerRow; + final double inputSize; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final keySize = switch (inputSize) { + SudokuInputSize.small => 'small', + SudokuInputSize.medium => 'medium', + SudokuInputSize.large => 'large', + _ => 'other', + }; + return Stack( + children: [ + for (var i = 0; i < sudokuDimension; i++) + Positioned( + top: (i ~/ inputsPerRow) * inputSize, + left: (i % inputsPerRow) * inputSize, + child: GestureDetector( + onTap: () => context.read().add( + SudokuInputTapped(i + 1), + ), + child: Container( + key: Key('sudoku_input_${keySize}_block_${i + 1}'), + alignment: Alignment.center, + height: inputSize, + width: inputSize, + decoration: BoxDecoration( + border: Border.all( + color: theme.dividerColor, + width: 0.8, + ), + ), + child: Text( + '${i + 1}', + style: SudokuTextStyle.headline6.copyWith( + color: theme.colorScheme.secondary, + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/sudoku/widgets/widgets.dart b/lib/sudoku/widgets/widgets.dart new file mode 100644 index 0000000..c18e2e3 --- /dev/null +++ b/lib/sudoku/widgets/widgets.dart @@ -0,0 +1,4 @@ +export 'sudoku_block.dart'; +export 'sudoku_board.dart'; +export 'sudoku_board_divider.dart'; +export 'sudoku_input.dart'; diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 4145e26..96997d2 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -1,12 +1,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sudoku/app/app.dart'; -import 'package:sudoku/counter/counter.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; void main() { group('App', () { - testWidgets('renders CounterPage', (tester) async { + testWidgets('renders SudokuPage', (tester) async { await tester.pumpWidget(const App()); - expect(find.byType(CounterPage), findsOneWidget); + expect(find.byType(SudokuPage), findsOneWidget); }); }); } diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index a079998..762dd89 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -1,14 +1,23 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sudoku/l10n/l10n.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; + +class _MockSudokuBloc extends MockBloc + implements SudokuBloc {} extension PumpApp on WidgetTester { - Future pumpApp(Widget widget) { + Future pumpApp(Widget widget, {SudokuBloc? sudokuBloc}) { return pumpWidget( - MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: widget, + BlocProvider.value( + value: sudokuBloc ?? _MockSudokuBloc(), + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: widget, + ), ), ); } diff --git a/test/sudoku/view/sudoku_page_test.dart b/test/sudoku/view/sudoku_page_test.dart new file mode 100644 index 0000000..2e6b02b --- /dev/null +++ b/test/sudoku/view/sudoku_page_test.dart @@ -0,0 +1,118 @@ +// 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:mocktail/mocktail.dart'; +import 'package:sudoku/layout/responsive_layout_builder.dart'; +import 'package:sudoku/models/models.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; + +import '../../helpers/helpers.dart'; + +class _MockSudokuBloc extends MockBloc + implements SudokuBloc {} + +class _MockSudoku extends Mock implements Sudoku {} + +class _FakeBlock extends Fake implements Block {} + +void main() { + group('SudokuPage', () { + testWidgets('renders SudokuView', (tester) async { + await tester.pumpApp(const SudokuPage()); + expect(find.byType(SudokuView), findsOneWidget); + }); + }); + + group('SudokuView', () { + late SudokuBloc sudokuBloc; + late Sudoku sudoku; + + setUp(() { + sudokuBloc = _MockSudokuBloc(); + sudoku = _MockSudoku(); + when(() => sudoku.getDimesion()).thenReturn(3); + when(() => sudoku.blocks).thenReturn([]); + when(() => sudokuBloc.state).thenReturn( + SudokuState(sudoku: sudoku), + ); + }); + + testWidgets('renders appbar in small layout', (tester) async { + tester.setSmallDisplaySize(); + await tester.pumpApp( + const SudokuView(), + sudokuBloc: sudokuBloc, + ); + expect(find.byType(AppBar), findsOneWidget); + }); + + testWidgets('renders appbar in medium layout', (tester) async { + tester.setMediumDisplaySize(); + await tester.pumpApp( + const SudokuView(), + sudokuBloc: sudokuBloc, + ); + expect(find.byType(AppBar), findsOneWidget); + }); + + testWidgets('does not render appbar in large layout', (tester) async { + tester.setLargeDisplaySize(); + await tester.pumpApp( + const SudokuView(), + sudokuBloc: sudokuBloc, + ); + expect(find.byType(AppBar), findsNothing); + }); + }); + + group('SudokuBoardView', () { + late SudokuBloc sudokuBloc; + late Sudoku sudoku; + + setUp(() { + sudokuBloc = _MockSudokuBloc(); + sudoku = _MockSudoku(); + when(() => sudoku.getDimesion()).thenReturn(3); + when(() => sudoku.blocks).thenReturn([]); + when(() => sudokuBloc.state).thenReturn( + SudokuState(sudoku: sudoku), + ); + }); + + testWidgets( + 're-renders only when sudoku from [SudokuState] changes', + (tester) async { + final block1 = Block( + position: Position(x: 0, y: 0), + correctValue: 1, + currentValue: 1, + ); + final block2 = Block( + position: Position(x: 0, y: 1), + correctValue: 2, + currentValue: 2, + ); + + when(() => sudokuBloc.stream).thenAnswer( + (_) => Stream.fromIterable([ + SudokuState(sudoku: Sudoku(blocks: const [])), + SudokuState(sudoku: Sudoku(blocks: [block1, block2])), + ]), + ); + + await tester.pumpApp( + const SudokuBoardView( + layoutSize: ResponsiveLayoutSize.large, + ), + sudokuBloc: sudokuBloc, + ); + + await tester.pumpAndSettle(); + expect(find.byKey(Key('sudoku_board_view_0_0')), findsOneWidget); + expect(find.byKey(Key('sudoku_board_view_0_1')), findsOneWidget); + }, + ); + }); +} diff --git a/test/sudoku/widgets/sudoku_block_test.dart b/test/sudoku/widgets/sudoku_block_test.dart new file mode 100644 index 0000000..ea1cf6d --- /dev/null +++ b/test/sudoku/widgets/sudoku_block_test.dart @@ -0,0 +1,122 @@ +// 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:mocktail/mocktail.dart'; +import 'package:sudoku/models/models.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; + +import '../../helpers/helpers.dart'; + +class _MockSudokuBloc extends MockBloc + implements SudokuBloc {} + +class _MockSudokuState extends Mock implements SudokuState {} + +class _MockSudoku extends Mock implements Sudoku {} + +class _MockBlock extends Mock implements Block {} + +void main() { + group('SudokuBlock', () { + final block = Block( + position: Position(x: 0, y: 0), + correctValue: 1, + currentValue: 1, + ); + + const smallBlockKey = 'sudoku_block_small_0_0'; + const mediumBlockKey = 'sudoku_block_medium_0_0'; + const largeBlockKey = 'sudoku_block_large_0_0'; + + late Sudoku sudoku; + late SudokuBloc bloc; + late SudokuState state; + + setUp(() { + sudoku = _MockSudoku(); + when(() => sudoku.getDimesion()).thenReturn(3); + + state = _MockSudokuState(); + when(() => state.sudoku).thenReturn(sudoku); + when(() => state.highlightedBlocks).thenReturn({}); + when(() => state.currentSelectedBlock).thenReturn(block); + + bloc = _MockSudokuBloc(); + when(() => bloc.state).thenReturn(state); + }); + + testWidgets( + 'adds [SudokuBlockSelected] when tapped on a block', + (tester) async { + await tester.pumpApp( + SudokuBlock(block: block, state: state), + sudokuBloc: bloc, + ); + + await tester.tap(find.byType(GestureDetector)); + await tester.pumpAndSettle(); + + verify(() => bloc.add(SudokuBlockSelected(block))).called(1); + }, + ); + + testWidgets('renders large block on a large display', (tester) async { + tester.setLargeDisplaySize(); + + await tester.pumpApp( + SudokuBlock(block: block, state: state), + sudokuBloc: bloc, + ); + + expect(find.byKey(Key(largeBlockKey)), findsOneWidget); + }); + + testWidgets('renders medium block on a medium display', (tester) async { + tester.setMediumDisplaySize(); + + await tester.pumpApp( + SudokuBlock(block: block, state: state), + sudokuBloc: bloc, + ); + + expect(find.byKey(Key(mediumBlockKey)), findsOneWidget); + }); + + testWidgets('renders small block on a small display', (tester) async { + tester.setSmallDisplaySize(); + + await tester.pumpApp( + SudokuBlock(block: block, state: state), + sudokuBloc: bloc, + ); + + expect(find.byKey(Key(smallBlockKey)), findsOneWidget); + }); + + testWidgets( + 'renders block when block is part of highlighted, but not selcted', + (tester) async { + final otherBlock = _MockBlock(); + when(() => otherBlock.position).thenReturn(Position(x: 0, y: 1)); + + when(() => state.highlightedBlocks).thenReturn({block}); + when(() => state.currentSelectedBlock).thenReturn(otherBlock); + + await tester.pumpApp( + SudokuBlock( + block: block, + state: state, + ), + sudokuBloc: bloc, + ); + + expect( + find.byWidgetPredicate((widget) => widget is DecoratedBox), + findsOneWidget, + ); + }, + ); + }); +} diff --git a/test/sudoku/widgets/sudoku_input_test.dart b/test/sudoku/widgets/sudoku_input_test.dart new file mode 100644 index 0000000..0fa8920 --- /dev/null +++ b/test/sudoku/widgets/sudoku_input_test.dart @@ -0,0 +1,65 @@ +// 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:mocktail/mocktail.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; + +import '../../helpers/helpers.dart'; + +class _MockSudokuBloc extends MockBloc + implements SudokuBloc {} + +void main() { + group('SudokuInput', () { + const largeInputKey = 'sudoku_input_large'; + const mediumInputKey = 'sudoku_input_medium'; + const smallInputKey = 'sudoku_input_small'; + + late SudokuBloc sudokuBloc; + + setUp(() { + sudokuBloc = _MockSudokuBloc(); + }); + + testWidgets( + 'adds [SudokuInputTapped] when tapped on an input block', + (tester) async { + await tester.pumpApp( + SudokuInput(sudokuDimension: 1), + sudokuBloc: sudokuBloc, + ); + + await tester.tap(find.byType(GestureDetector)); + await tester.pumpAndSettle(); + + verify(() => sudokuBloc.add(SudokuInputTapped(1))).called(1); + }, + ); + + testWidgets('renders large input on large display', (tester) async { + tester.setLargeDisplaySize(); + await tester.pumpApp( + SudokuInput(sudokuDimension: 3), + ); + expect(find.byKey(Key(largeInputKey)), findsOneWidget); + }); + + testWidgets('renders medium input on medium display', (tester) async { + tester.setMediumDisplaySize(); + await tester.pumpApp( + SudokuInput(sudokuDimension: 3), + ); + expect(find.byKey(Key(mediumInputKey)), findsOneWidget); + }); + + testWidgets('renders small input on small display', (tester) async { + tester.setSmallDisplaySize(); + await tester.pumpApp( + SudokuInput(sudokuDimension: 3), + ); + expect(find.byKey(Key(smallInputKey)), findsOneWidget); + }); + }); +}