Skip to the code
cf.

Temperature converter

Build App: a converter for the tea shop's brewing guide, with two text boxes labelled Celsius and Fahrenheit. Typing a number in either box fills in the other: °F = °C × 9 / 5 + 32.

The box you type in keeps exactly what you typed, so 37.0 stays 37.0. The other box shows the converted value rounded to one decimal place, without a trailing .0: 212, 98.6, -17.8.

If the box you type in holds something that isn't a number, a message with role="alert" says Celsius must be a number. (or Fahrenheit must be a number.), and the other box is left empty instead of showing NaN. Once the text is a number again, the message goes away.

An empty box isn't a mistake: emptying one box empties the other, with no message. Both boxes start empty.

The tests only see the boxes, but aim for one piece of state for both: which box you typed in last, and its text. Work out the other box from it on every render, so the two can never disagree.

<label>Celsius <input value="100" /></label>
<label>Fahrenheit <input value="212" /></label>
 
// after typing "hot" in Celsius:
<label>Celsius <input value="hot" /></label>
<label>Fahrenheit <input value="" /></label>
<p role="alert">Celsius must be a number.</p>

What the tests check

  1. both boxes start empty, with no message
    render(<App />);
    expect(screen.getByLabelText("Celsius")).toHaveValue("");
    expect(screen.getByLabelText("Fahrenheit")).toHaveValue("");
    expect(screen.queryByRole("alert")).not.toBeInTheDocument();
    
  2. typing Celsius fills in Fahrenheit
    render(<App />);
    await user.type(screen.getByLabelText("Celsius"), "100");
    expect(screen.getByLabelText("Fahrenheit")).toHaveValue("212");
    
  3. typing Fahrenheit fills in Celsius
    render(<App />);
    await user.type(screen.getByLabelText("Fahrenheit"), "212");
    expect(screen.getByLabelText("Celsius")).toHaveValue("100");
    
  4. rounds the other box to one decimal place
    render(<App />);
    await user.type(screen.getByLabelText("Celsius"), "37");
    expect(screen.getByLabelText("Fahrenheit")).toHaveValue("98.6");
    await user.clear(screen.getByLabelText("Fahrenheit"));
    await user.type(screen.getByLabelText("Fahrenheit"), "0");
    expect(screen.getByLabelText("Celsius")).toHaveValue("-17.8");
    
  5. keeps what you typed exactly as you typed it
    render(<App />);
    await user.type(screen.getByLabelText("Celsius"), "37.0");
    expect(screen.getByLabelText("Celsius")).toHaveValue("37.0");
    expect(screen.getByLabelText("Fahrenheit")).toHaveValue("98.6");
    
  6. negative numbers work
    render(<App />);
    await user.type(screen.getByLabelText("Celsius"), "-40");
    expect(screen.getByLabelText("Fahrenheit")).toHaveValue("-40");
    expect(screen.queryByRole("alert")).not.toBeInTheDocument();
    
  7. text that isn't a number shows a message and empties the other box
    render(<App />);
    await user.type(screen.getByLabelText("Celsius"), "100");
    await user.type(screen.getByLabelText("Celsius"), "x");
    expect(screen.getByRole("alert")).toHaveTextContent("Celsius must be a number.");
    expect(screen.getByLabelText("Celsius")).toHaveValue("100x");
    expect(screen.getByLabelText("Fahrenheit")).toHaveValue("");
    
  8. the message goes away once it's a number again
    render(<App />);
    await user.type(screen.getByLabelText("Celsius"), "warm");
    expect(screen.getByRole("alert")).toBeInTheDocument();
    await user.clear(screen.getByLabelText("Celsius"));
    await user.type(screen.getByLabelText("Celsius"), "25");
    expect(screen.queryByRole("alert")).not.toBeInTheDocument();
    expect(screen.getByLabelText("Fahrenheit")).toHaveValue("77");
    
  9. the Fahrenheit box has its own message
    render(<App />);
    await user.type(screen.getByLabelText("Fahrenheit"), "hot");
    expect(screen.getByRole("alert")).toHaveTextContent("Fahrenheit must be a number.");
    expect(screen.getByLabelText("Celsius")).toHaveValue("");
    
  10. emptying a box empties the other, with no message
    render(<App />);
    await user.type(screen.getByLabelText("Fahrenheit"), "212");
    await user.clear(screen.getByLabelText("Fahrenheit"));
    expect(screen.getByLabelText("Celsius")).toHaveValue("");
    expect(screen.queryByRole("alert")).not.toBeInTheDocument();
    

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