diff --git a/README.md b/README.md index c6a39bb..66f07e8 100644 --- a/README.md +++ b/README.md @@ -252,3 +252,9 @@ ### Test reports and refactoring session2 [Pull request](https://github.com/nickovchinnikov/minesweeper/pull/43) + +### Refactoring useGame + +### Refactoring useGame 2 + +[Pull request](https://github.com/nickovchinnikov/minesweeper/pull/46/files) diff --git a/src/components/Game/Game.stories.tsx b/src/components/Game/Game.stories.tsx index c96fdc9..7696292 100644 --- a/src/components/Game/Game.stories.tsx +++ b/src/components/Game/Game.stories.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Story, Meta } from '@storybook/react'; -import { Field } from '@/helpers/Field'; -import { fieldGenerator } from '@/helpers/__mocks__/Field'; +import { Field } from '@/core/Field'; +import { fieldGenerator } from '@/core/__mocks__/Field'; import { Grid } from '@/components/Grid'; import { Top } from '@/components/Top'; diff --git a/src/components/Grid/Cell.test.tsx b/src/components/Grid/Cell.test.tsx index 894a773..8e4419b 100644 --- a/src/components/Grid/Cell.test.tsx +++ b/src/components/Grid/Cell.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, fireEvent, createEvent } from '@testing-library/react'; -import { CellState, Coords } from '@/helpers/Field'; +import { CellState, Coords } from '@/core/Field'; import { Cell, ClosedFrame, isActiveCell } from './Cell'; diff --git a/src/components/Grid/Cell.tsx b/src/components/Grid/Cell.tsx index c28beb5..6c38800 100644 --- a/src/components/Grid/Cell.tsx +++ b/src/components/Grid/Cell.tsx @@ -1,9 +1,9 @@ import React, { FC } from 'react'; import styled from '@emotion/styled'; -import { Cell as CellType, Coords, CellState } from '@/helpers/Field'; +import { Cell as CellType, Coords, CellState } from '@/core/Field'; -import { useMouseDown } from '@/hooks/useMouseDown'; +import { useMouseDown } from '@/components/hooks/useMouseDown'; export interface CellProps { /** diff --git a/src/components/Grid/Grid.test.tsx b/src/components/Grid/Grid.test.tsx index cce9785..c021ed2 100644 --- a/src/components/Grid/Grid.test.tsx +++ b/src/components/Grid/Grid.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { Field } from '@/helpers/Field'; +import { Field } from '@/core/Field'; import { Grid } from './Grid'; diff --git a/src/components/Grid/Grid.tsx b/src/components/Grid/Grid.tsx index 474364a..bf52663 100644 --- a/src/components/Grid/Grid.tsx +++ b/src/components/Grid/Grid.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import styled from '@emotion/styled'; -import { Coords, Field } from '@/helpers/Field'; +import { Coords, Field } from '@/core/Field'; import { Cell } from './Cell'; diff --git a/src/components/Scoreboard/Reset.tsx b/src/components/Scoreboard/Reset.tsx index 44a9f84..fabaf74 100644 --- a/src/components/Scoreboard/Reset.tsx +++ b/src/components/Scoreboard/Reset.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import styled from '@emotion/styled'; -import { useMouseDown } from '@/hooks/useMouseDown'; +import { useMouseDown } from '@/components/hooks/useMouseDown'; export interface ResetProps { /** diff --git a/src/hooks/useMouseDown.test.ts b/src/components/hooks/useMouseDown.test.ts similarity index 100% rename from src/hooks/useMouseDown.test.ts rename to src/components/hooks/useMouseDown.test.ts diff --git a/src/hooks/useMouseDown.ts b/src/components/hooks/useMouseDown.ts similarity index 100% rename from src/hooks/useMouseDown.ts rename to src/components/hooks/useMouseDown.ts diff --git a/src/helpers/CellsManipulator.test.ts b/src/core/CellsManipulator.test.ts similarity index 100% rename from src/helpers/CellsManipulator.test.ts rename to src/core/CellsManipulator.test.ts diff --git a/src/helpers/CellsManipulator.ts b/src/core/CellsManipulator.ts similarity index 100% rename from src/helpers/CellsManipulator.ts rename to src/core/CellsManipulator.ts diff --git a/src/helpers/Field.ts b/src/core/Field.ts similarity index 100% rename from src/helpers/Field.ts rename to src/core/Field.ts diff --git a/src/helpers/Fileld.test.ts b/src/core/Fileld.test.ts similarity index 100% rename from src/helpers/Fileld.test.ts rename to src/core/Fileld.test.ts diff --git a/src/helpers/__mocks__/Field.ts b/src/core/__mocks__/Field.ts similarity index 100% rename from src/helpers/__mocks__/Field.ts rename to src/core/__mocks__/Field.ts diff --git a/src/helpers/detectSolvedPullze.ts b/src/core/detectSolvedPullze.ts similarity index 100% rename from src/helpers/detectSolvedPullze.ts rename to src/core/detectSolvedPullze.ts diff --git a/src/helpers/detectSolvedPuzzle.test.ts b/src/core/detectSolvedPuzzle.test.ts similarity index 100% rename from src/helpers/detectSolvedPuzzle.test.ts rename to src/core/detectSolvedPuzzle.test.ts diff --git a/src/helpers/openCell.test.ts b/src/core/openCell.test.ts similarity index 100% rename from src/helpers/openCell.test.ts rename to src/core/openCell.test.ts diff --git a/src/helpers/openCell.ts b/src/core/openCell.ts similarity index 100% rename from src/helpers/openCell.ts rename to src/core/openCell.ts diff --git a/src/helpers/setFlag.test.ts b/src/core/setFlag.test.ts similarity index 100% rename from src/helpers/setFlag.test.ts rename to src/core/setFlag.test.ts diff --git a/src/helpers/setFlag.ts b/src/core/setFlag.ts similarity index 100% rename from src/helpers/setFlag.ts rename to src/core/setFlag.ts diff --git a/src/modules/GameWithHooks/useGame.test.ts b/src/modules/GameWithHooks/useGame.test.ts index bf95a64..394fde0 100644 --- a/src/modules/GameWithHooks/useGame.test.ts +++ b/src/modules/GameWithHooks/useGame.test.ts @@ -1,11 +1,11 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { CellState, Field } from '@/helpers/Field'; +import { CellState, Field } from '@/core/Field'; import { GameLevels, GameSettings } from '@/modules/GameSettings'; import { useGame } from './useGame'; -jest.mock('@/helpers/Field'); +jest.mock('@/core/Field'); const { empty: e, hidden: h, bomb: b, flag: f, weakFlag: w } = CellState; diff --git a/src/modules/GameWithHooks/useGame.ts b/src/modules/GameWithHooks/useGame.ts index e0eb5eb..70ef4a4 100644 --- a/src/modules/GameWithHooks/useGame.ts +++ b/src/modules/GameWithHooks/useGame.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Field, @@ -6,11 +6,15 @@ import { generateFieldWithDefaultState, fieldGenerator, Coords, -} from '@/helpers/Field'; -import { openCell } from '@/helpers/openCell'; -import { setFlag } from '@/helpers/setFlag'; +} from '@/core/Field'; +import { openCell } from '@/core/openCell'; +import { setFlag } from '@/core/setFlag'; -import { LevelNames, GameSettings } from '@/modules/GameSettings'; +import { LevelNames } from '@/modules/GameSettings'; + +import { useStatus } from './useStatus'; +import { useSettings } from './useSettings'; +import { useTime } from './useTime'; interface ReturnType { level: LevelNames; @@ -29,21 +33,25 @@ interface ReturnType { } export const useGame = (): ReturnType => { - const [level, setLevel] = useState('beginner'); - - const [isGameOver, setIsGameOver] = useState(false); - const [isWin, setIsWin] = useState(false); - const [isGameStarted, setIsGameStarted] = useState(false); + const { + settings: [size, bombs], + level, + setLevel, + } = useSettings(); - const [time, setTime] = useState(0); - const [flagCounter, setFlagCounter] = useState(0); + const { + isGameStarted, + isWin, + isGameOver, + setNewGame, + setInProgress, + setGameWin, + setGameLoose, + } = useStatus(); - const setGameOver = (isSolved = false) => { - setIsGameOver(true); - setIsWin(isSolved); - }; + const [time, resetTime] = useTime(isGameStarted, isGameOver); - const [size, bombs] = GameSettings[level]; + const [flagCounter, setFlagCounter] = useState(0); const [playerField, setPlayerField] = useState( generateFieldWithDefaultState(size, CellState.hidden) @@ -53,26 +61,8 @@ export const useGame = (): ReturnType => { fieldGenerator(size, bombs / (size * size)) ); - useEffect(() => { - let interval: NodeJS.Timeout; - - if (isGameStarted) { - interval = setInterval(() => { - setTime(time + 1); - }, 1000); - - if (isGameOver) { - clearInterval(interval); - } - } - - return () => { - clearInterval(interval); - }; - }, [isGameOver, isGameStarted, time]); - const onClick = (coords: Coords) => { - !isGameStarted && setIsGameStarted(true); + !isGameStarted && setInProgress(); try { const [newPlayerField, isSolved] = openCell( coords, @@ -80,17 +70,17 @@ export const useGame = (): ReturnType => { gameField ); if (isSolved) { - setGameOver(isSolved); + setGameWin(); } setPlayerField([...newPlayerField]); } catch (e) { setPlayerField([...gameField]); - setGameOver(); + setGameLoose(); } }; const onContextMenu = (coords: Coords) => { - !isGameStarted && setIsGameStarted(true); + !isGameStarted && setInProgress(); const [newPlayerField, isSolved, newFlagCounter] = setFlag( coords, playerField, @@ -100,7 +90,7 @@ export const useGame = (): ReturnType => { ); setFlagCounter(newFlagCounter); if (isSolved) { - setGameOver(isSolved); + setGameWin(); } setPlayerField([...newPlayerField]); }; @@ -114,16 +104,13 @@ export const useGame = (): ReturnType => { setGameField([...newGameField]); setPlayerField([...newPlayerField]); - setIsGameOver(false); - setIsWin(false); - setIsGameStarted(false); - setTime(0); + setNewGame(); + resetTime(); setFlagCounter(0); }; const onChangeLevel = (level: LevelNames) => { - setLevel(level); - const newSettings = GameSettings[level]; + const newSettings = setLevel(level); resetHandler(newSettings); }; diff --git a/src/modules/GameWithHooks/useGameCheckGameField.test.ts b/src/modules/GameWithHooks/useGameCheckGameField.test.ts index 6d1e4e5..1dadc9c 100644 --- a/src/modules/GameWithHooks/useGameCheckGameField.test.ts +++ b/src/modules/GameWithHooks/useGameCheckGameField.test.ts @@ -1,6 +1,6 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { CellState, Field } from '@/helpers/Field'; +import { CellState, Field } from '@/core/Field'; import { useGame } from './useGame'; diff --git a/src/modules/GameWithHooks/useSettings.test.ts b/src/modules/GameWithHooks/useSettings.test.ts new file mode 100644 index 0000000..0c58609 --- /dev/null +++ b/src/modules/GameWithHooks/useSettings.test.ts @@ -0,0 +1,25 @@ +import { renderHook, act } from '@testing-library/react-hooks'; + +import { GameSettings } from '@/modules/GameSettings'; + +import { useSettings } from './useSettings'; + +describe('useGameSettings test cases', () => { + it('Check default settings', () => { + const { result } = renderHook(useSettings); + + expect(result.current.settings).toEqual(GameSettings.beginner); + expect(result.current.level).toBe('beginner'); + }); + it('Check setLevel to intermediate', () => { + const { result } = renderHook(useSettings); + + act(() => { + const newSettings = result.current.setLevel('intermediate'); + expect(newSettings).toEqual(GameSettings.intermediate); + }); + + expect(result.current.settings).toEqual(GameSettings.intermediate); + expect(result.current.level).toBe('intermediate'); + }); +}); diff --git a/src/modules/GameWithHooks/useSettings.ts b/src/modules/GameWithHooks/useSettings.ts new file mode 100644 index 0000000..baa891b --- /dev/null +++ b/src/modules/GameWithHooks/useSettings.ts @@ -0,0 +1,25 @@ +import { useState } from 'react'; + +import { LevelNames, GameSettings, Settings } from '@/modules/GameSettings'; + +interface Return { + settings: Settings; + level: LevelNames; + setLevel: (level: LevelNames) => Settings; +} + +export const useSettings = ( + defaultLevel = 'beginner' as LevelNames +): Return => { + const [level, setLevel] = useState(defaultLevel); + const settings = GameSettings[level]; + + return { + settings, + level, + setLevel: (level) => { + setLevel(level); + return GameSettings[level]; + }, + }; +}; diff --git a/src/modules/GameWithHooks/useStatus.test.ts b/src/modules/GameWithHooks/useStatus.test.ts new file mode 100644 index 0000000..a93f2a8 --- /dev/null +++ b/src/modules/GameWithHooks/useStatus.test.ts @@ -0,0 +1,100 @@ +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useStatus, Return } from './useStatus'; + +const getDataFromUseStatusReturn = ({ + isGameStarted, + isWin, + isGameOver, +}: Return) => ({ isGameStarted, isWin, isGameOver }); + +describe('useGameStatus test cases', () => { + it('Check default state', () => { + const { result } = renderHook(useStatus); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: false, + isWin: false, + isGameOver: false, + }); + }); + it('Check setNewGame handler', () => { + const { result } = renderHook(useStatus); + + act(result.current.setNewGame); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: false, + isWin: false, + isGameOver: false, + }); + }); + it('Check setInProgress handler', () => { + const { result } = renderHook(useStatus); + + act(result.current.setInProgress); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: true, + isWin: false, + isGameOver: false, + }); + }); + it('Check setGameWin handler', () => { + const { result } = renderHook(useStatus); + + act(result.current.setGameWin); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: false, + isWin: true, + isGameOver: true, + }); + }); + it('Check setGameLoose handler', () => { + const { result } = renderHook(useStatus); + + act(result.current.setGameLoose); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: false, + isWin: false, + isGameOver: true, + }); + }); + it('Full game statuses flow', () => { + const { result } = renderHook(useStatus); + + act(result.current.setInProgress); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: true, + isWin: false, + isGameOver: false, + }); + + act(result.current.setGameWin); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: false, + isWin: true, + isGameOver: true, + }); + + act(result.current.setGameLoose); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: false, + isWin: false, + isGameOver: true, + }); + + act(result.current.setNewGame); + + expect(getDataFromUseStatusReturn(result.current)).toEqual({ + isGameStarted: false, + isWin: false, + isGameOver: false, + }); + }); +}); diff --git a/src/modules/GameWithHooks/useStatus.ts b/src/modules/GameWithHooks/useStatus.ts new file mode 100644 index 0000000..b8ac7d8 --- /dev/null +++ b/src/modules/GameWithHooks/useStatus.ts @@ -0,0 +1,50 @@ +import { useState } from 'react'; + +export interface Return { + isGameOver: boolean; + isGameStarted: boolean; + isWin: boolean; + setGameWin: () => void; + setGameLoose: () => void; + setInProgress: () => void; + setNewGame: () => void; +} + +export enum GameStatuses { + NewGame, + InProgress, + Win, + Loose, +} + +export const useStatus = (): Return => { + const { NewGame, InProgress, Win, Loose } = GameStatuses; + + const [isGameOver, setIsGameOver] = useState(false); + const [isWin, setIsWin] = useState(false); + const [isGameStarted, setIsGameStarted] = useState(false); + + const setGameStatus = (status: GameStatuses) => { + setIsGameStarted(status === InProgress); + setIsWin(status === Win); + setIsGameOver([Win, Loose].includes(status)); + }; + + const setNewGame = () => setGameStatus(NewGame); + + const setInProgress = () => setGameStatus(InProgress); + + const setGameWin = () => setGameStatus(Win); + + const setGameLoose = () => setGameStatus(Loose); + + return { + isGameStarted, + isWin, + isGameOver, + setNewGame, + setInProgress, + setGameWin, + setGameLoose, + }; +}; diff --git a/src/modules/GameWithHooks/useTime.test.ts b/src/modules/GameWithHooks/useTime.test.ts new file mode 100644 index 0000000..9b42231 --- /dev/null +++ b/src/modules/GameWithHooks/useTime.test.ts @@ -0,0 +1,66 @@ +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useTime } from './useTime'; + +const moveTimersByTime = (timeMustPass: number): void => { + for (let i = 0; i < timeMustPass; i++) { + act(() => { + jest.advanceTimersByTime(1000); + }); + } +}; + +describe('useTime test cases', () => { + it('Timer works fine when game is started', () => { + jest.useFakeTimers(); + + const { result } = renderHook(() => useTime(true, false)); + + const timeMustPass = 5; + + moveTimersByTime(timeMustPass); + + expect(result.current[0]).toBe(timeMustPass); + }); + it('Timer stops when game is over', () => { + jest.useFakeTimers(); + + const { result } = renderHook(() => useTime(true, true)); + + moveTimersByTime(5); + + expect(result.current[0]).toBe(0); + }); + it('Timer full lifecycle', () => { + jest.useFakeTimers(); + + const { result, rerender } = renderHook( + ({ isGameStarted, isGameOver }) => useTime(isGameStarted, isGameOver), + { + initialProps: { isGameStarted: false, isGameOver: false }, + } + ); + + moveTimersByTime(5); + + expect(result.current[0]).toBe(0); + + rerender({ isGameStarted: true, isGameOver: false }); + + moveTimersByTime(5); + + expect(result.current[0]).toBe(5); + + rerender({ isGameStarted: true, isGameOver: true }); + + moveTimersByTime(5); + + expect(result.current[0]).toBe(5); + + act(() => { + result.current[1](); + }); + + expect(result.current[0]).toBe(0); + }); +}); diff --git a/src/modules/GameWithHooks/useTime.ts b/src/modules/GameWithHooks/useTime.ts new file mode 100644 index 0000000..a87f2b6 --- /dev/null +++ b/src/modules/GameWithHooks/useTime.ts @@ -0,0 +1,30 @@ +import { useState, useEffect } from 'react'; + +export const useTime = ( + isGameStarted: boolean, + isGameOver: boolean +): [number, () => void] => { + const [time, setTime] = useState(0); + + const onReset = () => setTime(0); + + useEffect(() => { + let interval: NodeJS.Timeout; + + if (isGameStarted) { + interval = setInterval(() => { + setTime(time + 1); + }, 1000); + + if (isGameOver) { + clearInterval(interval); + } + } + + return () => { + clearInterval(interval); + }; + }, [isGameOver, isGameStarted, time]); + + return [time, onReset]; +};