The tea shop keeps a tablet on the window table so regulars can play while their tea steeps. Build TicTacToe, the default export: tic-tac-toe for two players taking turns on one screen. It takes no props.
The board is 9 buttons in 3 rows. Give each one the accessible name Row 1, column 1 through Row 3, column 3 with aria-label, and show X, O, or nothing as its text. X always goes first. A square that's taken is disabled.
Above the board, a <p aria-live="polite"> shows the game's status: Next player: X (or O) while the game is on, Winner: X (or O) when someone gets three in a row, a column, or either diagonal, and Draw when the board is full and nobody won. A win on the ninth square is a win, not a draw.
Once someone wins, the game is over: every square is disabled.
A Restart button clears the board and gives the first move back to X.
Below the board, list the history: a Go to game start button, then one button per move made so far, Go to move 1, Go to move 2, and so on. Clicking one shows the board as it was after that move, with the right player to go next. Playing a move from there throws away the moves that came after it, so the history now ends at the new move.
<p aria-live="polite">Next player: O</p>
<button aria-label="Row 1, column 1">X</button>
<button aria-label="Row 1, column 2"></button>
...
<button>Restart</button>
<ol>
<li><button>Go to game start</button></li>
<li><button>Go to move 1</button></li>
</ol>
render(<App />);
for (let row = 1; row <= 3; row++) {
for (let column = 1; column <= 3; column++) {
const square = screen.getByRole("button", { name: "Row " + row + ", column " + column });
expect(square.textContent).toBe("");
expect(square).toBeEnabled();
}
}
expect(screen.getByText("Next player: X")).toBeInTheDocument();
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
render(<App />);
await user.click(square(1, 1));
expect(square(1, 1)).toHaveTextContent("X");
expect(screen.getByText("Next player: O")).toBeInTheDocument();
await user.click(square(2, 2));
expect(square(2, 2)).toHaveTextContent("O");
expect(screen.getByText("Next player: X")).toBeInTheDocument();
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
render(<App />);
await user.click(square(1, 1));
expect(square(1, 1)).toBeDisabled();
await user.click(square(1, 1));
expect(square(1, 1)).toHaveTextContent("X");
expect(screen.getByText("Next player: O")).toBeInTheDocument();
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
const play = async (moves) => {
for (const [row, column] of moves) await user.click(square(row, column));
};
const first = render(<App />);
await play([[1, 1], [2, 1], [1, 2], [2, 2], [1, 3]]);
expect(screen.getByText("Winner: X")).toBeInTheDocument();
first.unmount();
render(<App />);
await play([[1, 1], [1, 2], [2, 1], [2, 2], [3, 3], [3, 2]]);
expect(screen.getByText("Winner: O")).toBeInTheDocument();
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
const play = async (moves) => {
for (const [row, column] of moves) await user.click(square(row, column));
};
const first = render(<App />);
await play([[1, 1], [1, 2], [2, 2], [1, 3], [3, 3]]);
expect(screen.getByText("Winner: X")).toBeInTheDocument();
first.unmount();
render(<App />);
await play([[1, 1], [1, 3], [1, 2], [2, 2], [3, 3], [3, 1]]);
expect(screen.getByText("Winner: O")).toBeInTheDocument();
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
render(<App />);
for (const [row, column] of [[1, 1], [2, 1], [1, 2], [2, 2], [1, 3]]) await user.click(square(row, column));
for (const [row, column] of [[2, 3], [3, 1], [3, 2], [3, 3]]) expect(square(row, column)).toBeDisabled();
await user.click(square(3, 3));
expect(square(3, 3).textContent).toBe("");
expect(screen.getByText("Winner: X")).toBeInTheDocument();
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
render(<App />);
// X O X
// X O O
// O X X
for (const [row, column] of [[1, 1], [1, 2], [1, 3], [2, 2], [2, 1], [2, 3], [3, 2], [3, 1], [3, 3]]) {
await user.click(square(row, column));
}
expect(screen.getByText("Draw")).toBeInTheDocument();
expect(screen.queryByText(/Winner/)).not.toBeInTheDocument();
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
render(<App />);
// X O X
// X O O
// X X O (X fills the first column with the last move)
for (const [row, column] of [[1, 1], [1, 2], [1, 3], [2, 2], [2, 1], [2, 3], [3, 2], [3, 3], [3, 1]]) {
await user.click(square(row, column));
}
expect(screen.getByText("Winner: X")).toBeInTheDocument();
expect(screen.queryByText("Draw")).not.toBeInTheDocument();
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
render(<App />);
await user.click(square(1, 1));
await user.click(square(2, 2));
await user.click(square(3, 3));
await user.click(screen.getByRole("button", { name: "Restart" }));
expect(square(1, 1).textContent).toBe("");
expect(square(2, 2).textContent).toBe("");
expect(square(1, 1)).toBeEnabled();
expect(screen.getByText("Next player: X")).toBeInTheDocument();
await user.click(square(2, 2));
expect(square(2, 2)).toHaveTextContent("X");
const square = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
render(<App />);
await user.click(square(1, 1));
await user.click(square(2, 2));
await user.click(square(3, 3));
expect(screen.getByRole("button", { name: "Go to game start" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Go to move 1" }));
expect(square(1, 1)).toHaveTextContent("X");
expect(square(2, 2).textContent).toBe("");
expect(square(3, 3).textContent).toBe("");
expect(screen.getByText("Next player: O")).toBeInTheDocument();
await user.click(square(1, 3));
expect(square(1, 3)).toHaveTextContent("O");
expect(screen.getByRole("button", { name: "Go to move 2" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Go to move 3" })).not.toBeInTheDocument();
expect(screen.getByText("Next player: X")).toBeInTheDocument();
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Tic-tac-toe. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.