Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<Game> tests, 100% coverage #27

Merged
merged 4 commits into from
Sep 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,13 @@ module.exports = {
],
moduleNameMapper: {
'\\.css$': '<rootDir>/__mocks__/styleMock.js'
},
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100
}
}
}
63 changes: 41 additions & 22 deletions src/components/Game/Game.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ class Game extends React.Component {
this.handleRight = this.handleRight.bind(this)
this.handleUp = this.handleUp.bind(this)
this.handleDown = this.handleDown.bind(this)
this.handleCtrlZ = this.handleCtrlZ.bind(this)
this.handleCtrlY = this.handleCtrlY.bind(this)
this.handleUndo = this.handleUndo.bind(this)
this.handleRedo = this.handleRedo.bind(this)
this.onKeyDown = this.onKeyDown.bind(this)
}

Expand Down Expand Up @@ -189,32 +189,35 @@ class Game extends React.Component {
replayTimeout
} = this.props

const {
let {
replayTimeoutId
} = this.state

// there may be a replay in progress, this
// must be killed
if (replayTimeoutId) {
clearTimeout(replayTimeoutId)
replayTimeoutId = undefined
}

// user inputs replay string
const string = window.prompt()

const replay = hatetrisReplayCodec.decode(string)
// TODO: what if the replay is bad?

const wellStateId = 0
const nextReplayTimeoutId = wellStateId in replay ? setTimeout(this.inputReplayStep, replayTimeout)
replayTimeoutId = wellStateId in replay ? setTimeout(this.inputReplayStep, replayTimeout)
: undefined
const mode = wellStateId in replay ? 'REPLAYING' : 'PLAYING'

// GO
this.setState({
mode: 'REPLAYING',
mode: mode,
wellStateId: wellStateId,
wellStates: [this.firstWellState],
replay: replay,
replayTimeoutId: nextReplayTimeoutId
replayTimeoutId: replayTimeoutId
})
}

Expand All @@ -232,7 +235,7 @@ class Game extends React.Component {
let nextReplayTimeoutId

if (mode === 'REPLAYING') {
this.handleCtrlY()
this.handleRedo()

if (wellStateId + 1 in replay) {
nextReplayTimeoutId = setTimeout(this.inputReplayStep, replayTimeout)
Expand Down Expand Up @@ -291,9 +294,7 @@ class Game extends React.Component {
// so there is only one line which we need to check.
const gameIsOver = nextWellState.well[bar - 1] !== 0

const nextMode = gameIsOver ? 'GAME_OVER'
: (mode === 'REPLAYING' && !(nextWellStateId in replay)) ? 'PLAYING'
: mode
const nextMode = gameIsOver ? 'GAME_OVER' : mode

// no live piece? make a new one
// suited to the new world, of course
Expand Down Expand Up @@ -348,7 +349,7 @@ class Game extends React.Component {
}
}

handleCtrlZ () {
handleUndo () {
const {
replayTimeoutId,
wellStateId,
Expand Down Expand Up @@ -376,7 +377,7 @@ class Game extends React.Component {
}
}

handleCtrlY () {
handleRedo () {
const {
mode,
replay,
Expand All @@ -397,16 +398,26 @@ class Game extends React.Component {
onKeyDown (event) {
if (event.keyCode === 37) {
this.handleLeft()
} else if (event.keyCode === 39) {
}

if (event.keyCode === 39) {
this.handleRight()
} else if (event.keyCode === 40) {
}

if (event.keyCode === 40) {
this.handleDown()
} else if (event.keyCode === 38) {
}

if (event.keyCode === 38) {
this.handleUp()
} else if (event.keyCode === 90 && event.ctrlKey === true) {
this.handleCtrlZ()
} else if (event.keyCode === 89 && event.ctrlKey === true) {
this.handleCtrlY()
}

if (event.keyCode === 90 && event.ctrlKey === true) {
this.handleUndo()
}

if (event.keyCode === 89 && event.ctrlKey === true) {
this.handleRedo()
}
}

Expand Down Expand Up @@ -443,8 +454,8 @@ class Game extends React.Component {
onClickR={this.handleRight}
onClickU={this.handleUp}
onClickD={this.handleDown}
onClickZ={this.handleCtrlZ}
onClickY={this.handleCtrlY}
onClickZ={this.handleUndo}
onClickY={this.handleRedo}
/>
</div>
<div className='game__right'>
Expand All @@ -461,7 +472,7 @@ class Game extends React.Component {
</p>

<p className='game__paragraph'>
<button type='button' onClick={this.handleClickReplay}>
<button type='button' className='game__replay-button' onClick={this.handleClickReplay}>
show a replay
</button>
</p>
Expand Down Expand Up @@ -502,6 +513,14 @@ class Game extends React.Component {
}

componentWillUnmount () {
const {
replayTimeoutId
} = this.state

if (replayTimeoutId) {
clearTimeout(replayTimeoutId)
}

document.removeEventListener('keydown', this.onKeyDown)
}
}
Expand Down
144 changes: 139 additions & 5 deletions src/components/Game/Game.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,36 @@ describe('<Game>', () => {
expect(() => getGame({ wellWidth: 3 })).toThrowError()
})

it('ignores all keystrokes before the game has begun', () => {
const game = getGame()
expect(game.state()).toEqual({
mode: 'GAME_OVER',
wellStateId: -1,
wellStates: [],
replay: [],
replayTimeoutId: undefined
})

jest.spyOn(console, 'warn').mockImplementation(() => {})
game.instance().onKeyDown({ keyCode: 37 }) // left
game.instance().onKeyDown({ keyCode: 39 }) // right
game.instance().onKeyDown({ keyCode: 40 }) // down
game.instance().onKeyDown({ keyCode: 38 }) // up
game.instance().onKeyDown({ keyCode: 90, ctrlKey: true }) // Ctrl+Z
game.instance().onKeyDown({ keyCode: 89, ctrlKey: true }) // Ctrl+Y
expect(console.warn).toHaveBeenCalledTimes(6)
expect(game.state()).toEqual({
mode: 'GAME_OVER',
wellStateId: -1,
wellStates: [],
replay: [],
replayTimeoutId: undefined
})

console.warn.mockRestore()
game.unmount()
})

it('lets you play a few moves', () => {
const game = getGame()
expect(game.state()).toEqual({
Expand Down Expand Up @@ -208,6 +238,109 @@ describe('<Game>', () => {
replay: ['L', 'R', 'D', 'U'],
replayTimeoutId: undefined
})

// Warn on attempted redo at end of history
jest.spyOn(console, 'warn').mockImplementation(() => {})
game.instance().onKeyDown({ keyCode: 89, ctrlKey: true }) // Ctrl+Y
expect(console.warn).toHaveBeenCalledTimes(1)
console.warn.mockRestore()

game.unmount()
})

it('just lets you play if you enter an empty replay', () => {
const game = getGame()

jest.spyOn(window, 'prompt').mockReturnValueOnce('')
game.find('.game__replay-button').props().onClick()
window.prompt.mockRestore()

expect(game.state()).toEqual(expect.objectContaining({
mode: 'PLAYING',
replay: [],
replayTimeoutId: undefined,
wellStates: [
expect.anything()
],
wellStateId: 0
}))

game.unmount()
})

describe('when a replay is in progress', () => {
let game

beforeEach(() => {
game = getGame()

jest.spyOn(window, 'prompt').mockReturnValueOnce('AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA A2')
game.find('.game__replay-button').props().onClick()
window.prompt.mockRestore()

// Play a little of the replay
jest.runOnlyPendingTimers()
jest.runOnlyPendingTimers()
jest.runOnlyPendingTimers()

expect(game.state()).toEqual(expect.objectContaining({
mode: 'REPLAYING',
wellStates: [
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything()
],
wellStateId: 3,
replayTimeoutId: expect.any(Number)
}))
})

afterEach(() => {
game.unmount()
})

it('lets you start a new game', () => {
game.find('.game__start-button').props().onClick()
expect(game.state()).toEqual(expect.objectContaining({
mode: 'PLAYING',
wellStates: [
expect.anything()
],
wellStateId: 0,
replayTimeoutId: undefined // trashed
}))
})

it('lets you start a new replay', () => {
jest.spyOn(window, 'prompt').mockReturnValueOnce('AAAA 1234 BCDE 2345 CDEF 3456')
game.find('.game__replay-button').props().onClick()
window.prompt.mockRestore()

expect(game.state()).toEqual(expect.objectContaining({
mode: 'REPLAYING',
wellStates: [
expect.anything()
],
wellStateId: 0,
replayTimeoutId: expect.any(Number)
}))
})

it('lets you undo and stops replaying if you do so', () => {
game.instance().onKeyDown({ keyCode: 90, ctrlKey: true }) // Ctrl+Z
expect(game.state()).toEqual(expect.objectContaining({
mode: 'PLAYING', // no longer replaying
wellStates: [
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything() // well state ID 3 still exists
],
wellStateId: 2, // down from 3
replayTimeoutId: undefined
}))
})
})

describe('check known replays', () => {
Expand Down Expand Up @@ -281,18 +414,19 @@ describe('<Game>', () => {
describe(run.name, () => {
Object.entries(run.replays).forEach(([encoding, string]) => {
it(encoding, () => {
jest.spyOn(window, 'prompt').mockReturnValueOnce(string)
const game = getGame()

jest.spyOn(window, 'prompt').mockReturnValueOnce(string)
game.instance().handleClickReplay()
window.prompt.mockRestore()

while (game.state().replayTimeoutId !== undefined) {
jest.runAllTimers()
}
jest.runAllTimers()

const state = game.state()
expect(state.mode).toBe('GAME_OVER')
expect(state.wellStates[state.wellStateId].score).toBe(run.expectedScore)
window.prompt.mockRestore()

game.unmount()
})
})
})
Expand Down