Skip to the code
cf.

Stopwatch

Build App: a stopwatch that shows the time in seconds with one decimal place, like 0.0 s and 1.5 s. It starts stopped, at 0.0 s.

A button reads Start while it's stopped and Stop while it runs. While it runs, the time goes up by a tenth of a second every 100 ms. Stop freezes the time, and Start carries on from there.

Reset stops the stopwatch and puts it back to 0.0 s.

Leave nothing running in the background: stopping, resetting, or removing the stopwatch from the page clears its interval, and it never runs two intervals at once.

<p>1.5 s</p>
<button>Stop</button>
<button>Reset</button>

What the tests check

  1. shows 0.0 s and a Start button at first
    render(<App />);
    expect(screen.getByText("0.0 s")).toBeInTheDocument();
    expect(screen.getByRole("button", { name: "Start" })).toBeInTheDocument();
    
  2. counts up in tenths of a second while it runs
    const clock = useFakeTimers();
    render(<App />);
    await user.click(screen.getByRole("button", { name: "Start" }));
    await clock.advance(100);
    expect(screen.getByText("0.1 s")).toBeInTheDocument();
    await clock.advance(1400);
    expect(screen.getByText("1.5 s")).toBeInTheDocument();
    
  3. Start turns into Stop while it runs
    useFakeTimers();
    render(<App />);
    await user.click(screen.getByRole("button", { name: "Start" }));
    expect(screen.getByRole("button", { name: "Stop" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "Start" })).not.toBeInTheDocument();
    
  4. Stop freezes the time
    const clock = useFakeTimers();
    render(<App />);
    await user.click(screen.getByRole("button", { name: "Start" }));
    await clock.advance(1200);
    await user.click(screen.getByRole("button", { name: "Stop" }));
    await clock.advance(3000);
    expect(screen.getByText("1.2 s")).toBeInTheDocument();
    
  5. Start again carries on from where it stopped
    const clock = useFakeTimers();
    render(<App />);
    await user.click(screen.getByRole("button", { name: "Start" }));
    await clock.advance(500);
    await user.click(screen.getByRole("button", { name: "Stop" }));
    await clock.advance(1000);
    await user.click(screen.getByRole("button", { name: "Start" }));
    await clock.advance(300);
    expect(screen.getByText("0.8 s")).toBeInTheDocument();
    
  6. stopping clears the interval
    const clock = useFakeTimers();
    render(<App />);
    await user.click(screen.getByRole("button", { name: "Start" }));
    await clock.advance(200);
    expect(clock.pending()).toBeGreaterThan(0);
    await user.click(screen.getByRole("button", { name: "Stop" }));
    expect(clock.pending()).toBe(0);
    
  7. Reset goes back to 0.0 s and stops
    const clock = useFakeTimers();
    render(<App />);
    await user.click(screen.getByRole("button", { name: "Start" }));
    await clock.advance(700);
    await user.click(screen.getByRole("button", { name: "Reset" }));
    expect(screen.getByText("0.0 s")).toBeInTheDocument();
    expect(screen.getByRole("button", { name: "Start" })).toBeInTheDocument();
    await clock.advance(1000);
    expect(screen.getByText("0.0 s")).toBeInTheDocument();
    
  8. never runs two intervals at once
    const clock = useFakeTimers();
    render(<App />);
    for (let i = 0; i < 2; i++) {
      await user.click(screen.getByRole("button", { name: "Start" }));
      await user.click(screen.getByRole("button", { name: "Stop" }));
    }
    await user.click(screen.getByRole("button", { name: "Start" }));
    await clock.advance(1000);
    expect(screen.getByText("1.0 s")).toBeInTheDocument();
    
  9. removing it while it runs clears the interval
    const clock = useFakeTimers();
    const { unmount } = render(<App />);
    await user.click(screen.getByRole("button", { name: "Start" }));
    await clock.advance(300);
    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).