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>
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();
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);
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"]);
const clock = useFakeTimers();
render(<App passage="sip slowly and enjoy" />);
await clock.advance(5000);
expect(screen.getByText("Time: 0 s")).toBeInTheDocument();
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();
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();
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();
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();
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);
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 Typing speed test. 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.