Skip to the code
cf.

Typing speed test

The tea shop is hiring, and the till needs fast, accurate typing. Build TypingTest, the default export: it shows a passage, you type it, and it tells you how fast and how accurately you did.

It takes passage, the text to type. It's optional, with a sentence of your choice as the default. You type into a <textarea> labelled Type the passage.

Show the passage with each of its characters in its own <span> with a data-status: correct when you've typed that character right, incorrect when you've typed something else in its place, and pending when you haven't got to it yet. It follows what's in the textarea now, so fixing a mistake with Backspace turns that character back to pending. Give the statuses different colours too (the tests check data-status, not the colour). Nothing else on the page has a data-status.

The clock starts at your first keystroke, not before. Show the whole seconds since then as Time: 0 s, Time: 1 s, and so on, updated every second.

When you've typed as many characters as the passage has, the test is over. The clock stops, the textarea is disabled (give it a maxLength of the passage's length too), and two results appear: Speed: 8 WPM and Accuracy: 90%.

Speed counts correct characters only. A word is 5 characters, so WPM is the number of correct characters, divided by 5, divided by the minutes from your first keystroke to your last, rounded to a whole number. Accuracy is the correct characters as a share of the passage's length, rounded to a whole percent.

Leave nothing running: stop the clock's timer when the test is over and when the component leaves the page.

<TypingTest passage="sip slowly and enjoy" />
 
// partway through, after typing "sop":
<p>
  <span data-status="correct">s</span><span data-status="incorrect">i</span><span data-status="correct">p</span><span data-status="pending"> </span>...
</p>
<label>Type the passage <textarea maxLength={20} /></label>
<p>Time: 4 s</p>
 
// at the end:
<p>Speed: 8 WPM</p>
<p>Accuracy: 90%</p>

What the tests check

  1. shows the passage with every character pending
    render(<App passage="sip slowly and enjoy" />);
    const chars = [...document.querySelectorAll("[data-status]")];
    expect(chars.map((char) => char.textContent).join("")).toBe("sip slowly and enjoy");
    expect(chars.every((char) => char.getAttribute("data-status") === "pending")).toBe(true);
    expect(screen.getByLabelText("Type the passage")).toHaveValue("");
    expect(screen.getByText("Time: 0 s")).toBeInTheDocument();
    expect(screen.queryByText(/WPM/)).not.toBeInTheDocument();
    
  2. marks typed characters correct or incorrect
    const statuses = () => [...document.querySelectorAll("[data-status]")].map((char) => char.getAttribute("data-status"));
    useFakeTimers();
    render(<App passage="sip slowly and enjoy" />);
    await user.type(screen.getByLabelText("Type the passage"), "sop");
    expect(statuses().slice(0, 5)).toEqual(["correct", "incorrect", "correct", "pending", "pending"]);
    expect(statuses().filter((status) => status === "pending").length).toBe(17);
    
  3. Backspace turns a character back to pending
    const statuses = () => [...document.querySelectorAll("[data-status]")].map((char) => char.getAttribute("data-status"));
    useFakeTimers();
    render(<App passage="sip slowly and enjoy" />);
    const input = screen.getByLabelText("Type the passage");
    await user.type(input, "so");
    expect(statuses().slice(0, 3)).toEqual(["correct", "incorrect", "pending"]);
    await user.type(input, "{Backspace}");
    expect(statuses().slice(0, 3)).toEqual(["correct", "pending", "pending"]);
    await user.type(input, "i");
    expect(statuses().slice(0, 3)).toEqual(["correct", "correct", "pending"]);
    
  4. doesn't start the clock before the first keystroke
    const clock = useFakeTimers();
    render(<App passage="sip slowly and enjoy" />);
    await clock.advance(5000);
    expect(screen.getByText("Time: 0 s")).toBeInTheDocument();
    
  5. starts the clock at the first keystroke and counts whole seconds
    const clock = useFakeTimers();
    render(<App passage="sip slowly and enjoy" />);
    await clock.advance(2500);
    await user.type(screen.getByLabelText("Type the passage"), "s");
    await clock.advance(999);
    expect(screen.getByText("Time: 0 s")).toBeInTheDocument();
    await clock.advance(1);
    expect(screen.getByText("Time: 1 s")).toBeInTheDocument();
    await clock.advance(2000);
    expect(screen.getByText("Time: 3 s")).toBeInTheDocument();
    
  6. stops the clock and the typing at the end of the passage
    const clock = useFakeTimers();
    render(<App passage="sip slowly and enjoy" />);
    const input = screen.getByLabelText("Type the passage");
    await user.type(input, "s");
    await clock.advance(4000);
    await user.type(input, "ip slowly and enjoy");
    expect(input).toBeDisabled();
    expect(input).toHaveAttribute("maxLength", "20");
    expect(screen.getByText("Time: 4 s")).toBeInTheDocument();
    expect(clock.pending()).toBe(0);
    await clock.advance(5000);
    expect(screen.getByText("Time: 4 s")).toBeInTheDocument();
    
  7. shows speed and accuracy for a perfect run
    const clock = useFakeTimers();
    render(<App passage="sip slowly and enjoy" />);
    const input = screen.getByLabelText("Type the passage");
    await user.type(input, "s");
    expect(screen.queryByText(/WPM/)).not.toBeInTheDocument();
    await clock.advance(30000);
    await user.type(input, "ip slowly and enjoy");
    // 20 correct characters are 4 words, in half a minute.
    expect(screen.getByText("Speed: 8 WPM")).toBeInTheDocument();
    expect(screen.getByText("Accuracy: 100%")).toBeInTheDocument();
    
  8. counts mistakes against speed and accuracy
    const clock = useFakeTimers();
    render(<App passage="sip slowly and enjoy" />);
    const input = screen.getByLabelText("Type the passage");
    await user.type(input, "s");
    await clock.advance(30000);
    await user.type(input, "op slowly and enjoq");
    // 18 of 20 characters are right: 3.6 words in half a minute.
    expect(screen.getByText("Speed: 7 WPM")).toBeInTheDocument();
    expect(screen.getByText("Accuracy: 90%")).toBeInTheDocument();
    
  9. stops the clock when it leaves the page
    const clock = useFakeTimers();
    const { unmount } = render(<App passage="sip slowly and enjoy" />);
    await user.type(screen.getByLabelText("Type the passage"), "sip");
    await clock.advance(1000);
    expect(clock.pending()).toBeGreaterThan(0);
    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).