From 8adeb3021fc28f46b8f33c7f37a27803df4b4aca Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 23 Aug 2024 16:30:29 -0600 Subject: [PATCH] finish all tests --- .../board-game.test.ts | 93 +++++++++++++++ .../02.problem.local-storage/README.mdx | 5 + .../local-storage.test.ts | 69 ++++++++++++ .../03.solution.history/local-storage.test.ts | 106 ++++++++++++++++++ 4 files changed, 273 insertions(+) create mode 100644 exercises/06.tic-tac-toe/01.solution.set-state-callback/board-game.test.ts create mode 100644 exercises/06.tic-tac-toe/02.solution.local-storage/local-storage.test.ts create mode 100644 exercises/06.tic-tac-toe/03.solution.history/local-storage.test.ts diff --git a/exercises/06.tic-tac-toe/01.solution.set-state-callback/board-game.test.ts b/exercises/06.tic-tac-toe/01.solution.set-state-callback/board-game.test.ts new file mode 100644 index 000000000..c17668be5 --- /dev/null +++ b/exercises/06.tic-tac-toe/01.solution.set-state-callback/board-game.test.ts @@ -0,0 +1,93 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent, waitFor } = dtl + +await import('./index.tsx') + +function getSquares() { + return waitFor(() => { + const squares = document.querySelectorAll('button.square') + expect(squares).toHaveLength(9) + return squares + }) +} + +await testStep('Initial board state', getSquares) + +const statusElement = await testStep('Find status element', async () => { + const status = await screen.findByText(/Next player: X/) + expect(status).toBeInTheDocument() + return status +}) + +await testStep('Play a game', async () => { + const squares = await getSquares() + + // X plays + fireEvent.click(squares[0]) + await waitFor(() => { + expect(squares[0]).toHaveTextContent('X') + }) + expect(statusElement).toHaveTextContent('Next player: O') + + // O plays + fireEvent.click(squares[4]) + await waitFor(() => { + expect(squares[4]).toHaveTextContent('O') + }) + expect(statusElement).toHaveTextContent('Next player: X') + + // X plays + fireEvent.click(squares[1]) + // O plays + fireEvent.click(squares[5]) + // X plays and wins + fireEvent.click(squares[2]) + + await waitFor(() => { + expect(statusElement).toHaveTextContent('Winner: X') + }) +}) + +await testStep('Restart game', async () => { + const restartButton = await screen.findByRole('button', { name: /restart/i }) + fireEvent.click(restartButton) + + await waitFor(async () => { + const squares = await getSquares() + expect(squares).toHaveLength(9) + expect(statusElement).toHaveTextContent('Next player: X') + }) +}) + +await testStep('Cannot play on occupied square', async () => { + const squares = await getSquares() + + fireEvent.click(squares[0]) + await waitFor(() => { + expect(squares[0]).toHaveTextContent('X') + }) + + fireEvent.click(squares[0]) + await waitFor(() => { + expect(squares[0]).toHaveTextContent('X') + expect(statusElement).toHaveTextContent('Next player: O') + }) +}) + +await testStep('Game ends in a draw', async () => { + const restartButton = await screen.findByRole('button', { name: /restart/i }) + fireEvent.click(restartButton) + await new Promise(resolve => setTimeout(resolve, 10)) + + const squares = await getSquares() + const moves = [0, 1, 2, 4, 3, 5, 7, 6, 8] + + for (const move of moves) { + fireEvent.click(squares[move]) + await new Promise(resolve => setTimeout(resolve, 10)) + } + + await waitFor(() => { + expect(statusElement).toHaveTextContent(`Cat's game`) + }) +}) diff --git a/exercises/06.tic-tac-toe/02.problem.local-storage/README.mdx b/exercises/06.tic-tac-toe/02.problem.local-storage/README.mdx index e67ae9630..6eb2c0640 100644 --- a/exercises/06.tic-tac-toe/02.problem.local-storage/README.mdx +++ b/exercises/06.tic-tac-toe/02.problem.local-storage/README.mdx @@ -15,3 +15,8 @@ For keeping the squares up-to-date in `localStorage`, you'll want to use 📜 If you need to learn a bit about the `localStorage` API, you can check out the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). + + + 🚨 Note this exercise depends on `localStorage` and so the tests could + interfer with your work by changing the `localStorage` you're working with. + diff --git a/exercises/06.tic-tac-toe/02.solution.local-storage/local-storage.test.ts b/exercises/06.tic-tac-toe/02.solution.local-storage/local-storage.test.ts new file mode 100644 index 000000000..f0cd09eff --- /dev/null +++ b/exercises/06.tic-tac-toe/02.solution.local-storage/local-storage.test.ts @@ -0,0 +1,69 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent, waitFor } = dtl + +const localStorageKey = 'squares' +const initialState = ['X', null, 'O', null, 'X', null, null, null, null] +window.localStorage.setItem(localStorageKey, JSON.stringify(initialState)) + +// Dynamically import the game component +await import('./index.tsx') + +await testStep('Game initializes from localStorage', async () => { + await waitFor(() => { + const squares = document.querySelectorAll('button.square') + expect(squares[0]).toHaveTextContent('X') + expect(squares[2]).toHaveTextContent('O') + expect(squares[4]).toHaveTextContent('X') + }) +}) + +await testStep('Game updates localStorage after a move', async () => { + // Make a move + const squares = document.querySelectorAll('button.square') + fireEvent.click(squares[1]) + + // Verify localStorage is updated + await waitFor(() => { + const storedState = JSON.parse( + window.localStorage.getItem(localStorageKey) || '[]', + ) + expect(storedState).toEqual([ + 'X', + 'O', + 'O', + null, + 'X', + null, + null, + null, + null, + ]) + }) +}) + +await testStep('Restart button clears localStorage', async () => { + const restartButton = await screen.findByRole('button', { name: /restart/i }) + fireEvent.click(restartButton) + + // Check if localStorage is cleared + await waitFor(() => { + const storedState = JSON.parse( + window.localStorage.getItem(localStorageKey) || '[]', + ) + expect(storedState).toEqual([ + null, + null, + null, + null, + null, + null, + null, + null, + null, + ]) + }) + + // Check if the board is reset + const squares = document.querySelectorAll('button.square') + expect(squares).toHaveLength(9) +}) diff --git a/exercises/06.tic-tac-toe/03.solution.history/local-storage.test.ts b/exercises/06.tic-tac-toe/03.solution.history/local-storage.test.ts new file mode 100644 index 000000000..71a4918be --- /dev/null +++ b/exercises/06.tic-tac-toe/03.solution.history/local-storage.test.ts @@ -0,0 +1,106 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent, waitFor } = dtl + +const localStorageKey = 'tic-tac-toe' +const initialState = { + history: [['X', null, 'O', null, 'X', null, null, null, null]], + currentStep: 0, +} +window.localStorage.setItem(localStorageKey, JSON.stringify(initialState)) + +// Dynamically import the game component +await import('./index.tsx') + +function getSquares() { + return waitFor(() => { + const squares = document.querySelectorAll('button.square') + expect(squares).toHaveLength(9) + return squares + }) +} + +await testStep('Game initializes from localStorage', async () => { + await waitFor(async () => { + const squares = await getSquares() + expect(squares[0]).toHaveTextContent('X') + expect(squares[2]).toHaveTextContent('O') + expect(squares[4]).toHaveTextContent('X') + }) +}) + +await testStep('Game updates localStorage after a move', async () => { + // Make a move + const squares = await getSquares() + fireEvent.click(squares[1]) + + // Verify localStorage is updated + await waitFor(() => { + const storedState = JSON.parse( + window.localStorage.getItem(localStorageKey) || '{}', + ) + expect(storedState.history).toHaveLength(2) + expect(storedState.currentStep).toBe(1) + expect(storedState.history[1]).toEqual([ + 'X', + 'O', + 'O', + null, + 'X', + null, + null, + null, + null, + ]) + }) +}) + +await testStep('Adding another move', async () => { + const squares = await getSquares() + fireEvent.click(squares[5]) + await new Promise(resolve => setTimeout(resolve, 100)) +}) + +await testStep('Game history allows going back to previous moves', async () => { + // Go back to the first move + const moveButtons = screen.getAllByRole('button', { name: /Go to move/i }) + fireEvent.click(moveButtons[0]) + + // Verify the board state + await waitFor(async () => { + const squares = await getSquares() + expect(squares[0]).toHaveTextContent('X') + expect(squares[1]).toHaveTextContent('O') + expect(squares[2]).toHaveTextContent('O') + expect(squares[4]).toHaveTextContent('X') + }) + + // Verify localStorage is updated + const storedState = JSON.parse( + window.localStorage.getItem(localStorageKey) || '{}', + ) + expect(storedState.currentStep).toBe(1) +}) + +await testStep('Restart button clears game history', async () => { + const restartButton = await screen.findByRole('button', { name: /restart/i }) + fireEvent.click(restartButton) + + // Check if localStorage is reset + await waitFor(() => { + const storedState = JSON.parse( + window.localStorage.getItem(localStorageKey) || '{}', + ) + expect(storedState).toEqual({ + history: [Array(9).fill(null)], + currentStep: 0, + }) + }) + + // Check if the board is reset + const squares = await getSquares() + squares.forEach(square => expect(square).toHaveTextContent('')) + + // Check if move history is cleared + const moveButtons = screen.queryAllByRole('button', { name: /Go to/i }) + expect(moveButtons).toHaveLength(1) // Only "Go to game start" should remain +})