Skip to the code
cf.

Grid lights

The tea shop's counter has a small light panel for children to play with while their parents order. Build GridLights, the default export, with no props: a 3 by 3 grid of light cells with an empty space in the middle, so 8 cells in all.

Each cell is a button with the accessible name Row 1, column 1 through Row 3, column 3 (use aria-label), skipping Row 2, column 2, which is just an empty space. A lit cell has aria-pressed="true", an unlit one aria-pressed="false". Show lit cells in a different colour too (the tests check aria-pressed, not the colour).

Clicking an unlit cell lights it. Clicking a lit cell does nothing: it stays lit. The lights stay on until all 8 are lit.

Then they turn off one at a time, in the reverse of the order they were lit: the last one lit goes off 300 ms after it was clicked, the one lit before it 300 ms after that, and so on, until all are off. While they're turning off, every cell is disabled. Once the last one is off, the cells work again and you can play another round.

If the grid leaves the page while the lights are turning off, stop: leave no timer running.

<button aria-label="Row 1, column 1" aria-pressed="true"></button>
<button aria-label="Row 1, column 2" aria-pressed="false"></button>
<button aria-label="Row 1, column 3" aria-pressed="false"></button>
<button aria-label="Row 2, column 1" aria-pressed="false"></button>
<span></span>
<button aria-label="Row 2, column 3" aria-pressed="false"></button>
...

What the tests check

  1. shows eight unlit cells around an empty middle
    render(<App />);
    const buttons = screen.getAllByRole("button");
    expect(buttons.length).toBe(8);
    for (const button of buttons) {
      expect(button).toHaveAttribute("aria-pressed", "false");
      expect(button).toBeEnabled();
    }
    expect(screen.queryByRole("button", { name: "Row 2, column 2" })).not.toBeInTheDocument();
    expect(screen.getByRole("button", { name: "Row 2, column 3" })).toBeInTheDocument();
    
  2. clicking a cell lights it
    const cell = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
    render(<App />);
    await user.click(cell(1, 1));
    expect(cell(1, 1)).toHaveAttribute("aria-pressed", "true");
    expect(cell(1, 2)).toHaveAttribute("aria-pressed", "false");
    await user.click(cell(3, 2));
    expect(cell(3, 2)).toHaveAttribute("aria-pressed", "true");
    
  3. clicking a lit cell keeps it lit
    const cell = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
    render(<App />);
    await user.click(cell(1, 1));
    await user.click(cell(1, 1));
    expect(cell(1, 1)).toHaveAttribute("aria-pressed", "true");
    
  4. the lights stay on until all eight are lit
    const cell = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
    const clock = useFakeTimers();
    render(<App />);
    const seven = [[1, 1], [1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]];
    for (const [row, column] of seven) await user.click(cell(row, column));
    await clock.advance(5000);
    for (const [row, column] of seven) expect(cell(row, column)).toHaveAttribute("aria-pressed", "true");
    expect(cell(3, 3)).toHaveAttribute("aria-pressed", "false");
    
  5. once all are lit, they turn off in reverse order, one every 300 ms
    const cell = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
    const clock = useFakeTimers();
    render(<App />);
    const order = [[3, 3], [1, 1], [2, 1], [1, 3], [3, 2], [1, 2], [2, 3], [3, 1]];
    for (const [row, column] of order) await user.click(cell(row, column));
    await clock.advance(299);
    for (const [row, column] of order) expect(cell(row, column)).toHaveAttribute("aria-pressed", "true");
    await clock.advance(1);
    for (let off = 1; off <= 8; off++) {
      order.forEach(([row, column], index) => {
        expect(cell(row, column)).toHaveAttribute("aria-pressed", index < 8 - off ? "true" : "false");
      });
      if (off < 8) await clock.advance(300);
    }
    
  6. cells are disabled while the lights turn off
    const cell = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
    const clock = useFakeTimers();
    render(<App />);
    const order = [[1, 1], [1, 2], [1, 3], [2, 3], [3, 3], [3, 2], [3, 1], [2, 1]];
    for (const [row, column] of order) await user.click(cell(row, column));
    for (const button of screen.getAllByRole("button")) expect(button).toBeDisabled();
    await clock.advance(300);
    expect(cell(2, 1)).toHaveAttribute("aria-pressed", "false");
    expect(cell(2, 1)).toBeDisabled();
    await user.click(cell(2, 1));
    expect(cell(2, 1)).toHaveAttribute("aria-pressed", "false");
    await clock.advance(300);
    expect(cell(3, 1)).toHaveAttribute("aria-pressed", "false");
    expect(cell(2, 1)).toHaveAttribute("aria-pressed", "false");
    
  7. works again for another round once all are off
    const cell = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
    const clock = useFakeTimers();
    render(<App />);
    const order = [[1, 1], [1, 2], [1, 3], [2, 3], [3, 3], [3, 2], [3, 1], [2, 1]];
    for (const [row, column] of order) await user.click(cell(row, column));
    await clock.advance(2400);
    for (const button of screen.getAllByRole("button")) {
      expect(button).toHaveAttribute("aria-pressed", "false");
      expect(button).toBeEnabled();
    }
    await user.click(cell(2, 3));
    await clock.advance(1000);
    expect(cell(2, 3)).toHaveAttribute("aria-pressed", "true");
    for (const [row, column] of order) await user.click(cell(row, column));
    await clock.advance(300);
    expect(cell(2, 1)).toHaveAttribute("aria-pressed", "false");
    expect(cell(2, 3)).toHaveAttribute("aria-pressed", "true");
    
  8. leaves no timer running when it leaves the page
    const cell = (row, column) => screen.getByRole("button", { name: "Row " + row + ", column " + column });
    const clock = useFakeTimers();
    const { unmount } = render(<App />);
    for (const [row, column] of [[1, 1], [1, 2], [1, 3], [2, 3], [3, 3], [3, 2], [3, 1], [2, 1]]) {
      await user.click(cell(row, column));
    }
    await clock.advance(600);
    unmount();
    expect(clock.pending()).toBe(0);
    

Learn more

App.jsxSaved in this browser

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).