Skip to the code
cf.

One-time code input

HardForms

Build App({ onComplete }): the boxes for the six-digit code a tea shop texts you when you sign in. It's a <fieldset> with the legend Verification code, holding six text boxes labelled Digit 1 to Digit 6, each holding at most one digit.

Typing a digit fills the box you're in, replacing any digit already there, and moves focus to the next box. On the last box, focus stays put. Anything that isn't a digit is ignored: the box doesn't change and focus doesn't move.

Backspace in a box with a digit clears that digit, and focus stays. Backspace in an empty box moves focus to the box before it and leaves that box's digit alone, so a second Backspace clears it. In the first box, an empty Backspace does nothing.

Pasting into a box fills it and the boxes after it with the digits from the pasted text, one digit per box. Anything that isn't a digit is skipped, and digits that don't fit are dropped. Focus moves to the box after the last one filled, or to the last box. Pasting 123456 into the first box fills them all.

Whenever a change leaves all six boxes filled, call onComplete(code) once, with the six digits as a string like "123456". Changing a digit in a full code calls it again with the new code. Never call it while any box is empty. The starter gives onComplete a default so the Preview works; the tests pass their own.

<App onComplete={(code) => verify(code)} />
 
<fieldset>
  <legend>Verification code</legend>
  <input aria-label="Digit 1" inputmode="numeric" />
  <!-- Digit 2 to Digit 6 -->
</fieldset>

What the tests check

  1. shows six boxes, Digit 1 to Digit 6, in a Verification code group
    render(<App onComplete={spy()} />);
    const group = screen.getByRole("group", { name: "Verification code" });
    const boxes = within(group).getAllByRole("textbox");
    expect(boxes).toHaveLength(6);
    for (let n = 1; n <= 6; n++) expect(within(group).getByLabelText("Digit " + n)).toHaveValue("");
    
  2. typing a digit fills the box and moves focus to the next one
    render(<App onComplete={spy()} />);
    await user.click(screen.getByLabelText("Digit 1"));
    await user.keyboard("4");
    expect(screen.getByLabelText("Digit 1")).toHaveValue("4");
    expect(screen.getByLabelText("Digit 2")).toHaveFocus();
    await user.keyboard("2");
    expect(screen.getByLabelText("Digit 2")).toHaveValue("2");
    expect(screen.getByLabelText("Digit 3")).toHaveFocus();
    
  3. ignores anything that isn't a digit
    render(<App onComplete={spy()} />);
    await user.click(screen.getByLabelText("Digit 1"));
    await user.keyboard("a- ");
    expect(screen.getByLabelText("Digit 1")).toHaveValue("");
    expect(screen.getByLabelText("Digit 1")).toHaveFocus();
    await user.keyboard("7");
    expect(screen.getByLabelText("Digit 1")).toHaveValue("7");
    expect(screen.getByLabelText("Digit 2")).toHaveFocus();
    
  4. typing six digits calls onComplete once with the code, and focus stays on the last box
    const onComplete = spy();
    render(<App onComplete={onComplete} />);
    await user.click(screen.getByLabelText("Digit 1"));
    await user.keyboard("12345");
    expect(onComplete).not.toHaveBeenCalled();
    await user.keyboard("6");
    expect(onComplete).toHaveBeenCalledTimes(1);
    expect(onComplete).toHaveBeenCalledWith("123456");
    expect(screen.getByLabelText("Digit 6")).toHaveFocus();
    
  5. Backspace in a filled box clears it and keeps focus there
    render(<App onComplete={spy()} />);
    await user.click(screen.getByLabelText("Digit 1"));
    await user.keyboard("123");
    await user.click(screen.getByLabelText("Digit 2"));
    await user.keyboard("{Backspace}");
    expect(screen.getByLabelText("Digit 2")).toHaveValue("");
    expect(screen.getByLabelText("Digit 2")).toHaveFocus();
    expect(screen.getByLabelText("Digit 1")).toHaveValue("1");
    expect(screen.getByLabelText("Digit 3")).toHaveValue("3");
    
  6. Backspace in an empty box moves back without clearing, and a second one clears
    render(<App onComplete={spy()} />);
    await user.click(screen.getByLabelText("Digit 1"));
    await user.keyboard("123");
    expect(screen.getByLabelText("Digit 4")).toHaveFocus();
    await user.keyboard("{Backspace}");
    expect(screen.getByLabelText("Digit 3")).toHaveFocus();
    expect(screen.getByLabelText("Digit 3")).toHaveValue("3");
    await user.keyboard("{Backspace}");
    expect(screen.getByLabelText("Digit 3")).toHaveValue("");
    expect(screen.getByLabelText("Digit 3")).toHaveFocus();
    
  7. typing over a filled box replaces its digit and calls onComplete with the new code
    const onComplete = spy();
    render(<App onComplete={onComplete} />);
    await user.click(screen.getByLabelText("Digit 1"));
    await user.keyboard("123456");
    await user.click(screen.getByLabelText("Digit 2"));
    await user.keyboard("9");
    expect(screen.getByLabelText("Digit 2")).toHaveValue("9");
    expect(screen.getByLabelText("Digit 3")).toHaveFocus();
    expect(onComplete).toHaveBeenCalledTimes(2);
    expect(onComplete).toHaveBeenLastCalledWith("193456");
    
  8. pasting six digits into the first box fills every box and calls onComplete
    const onComplete = spy();
    render(<App onComplete={onComplete} />);
    await user.click(screen.getByLabelText("Digit 1"));
    await user.paste("804216");
    ["8", "0", "4", "2", "1", "6"].forEach((digit, i) => expect(screen.getByLabelText("Digit " + (i + 1))).toHaveValue(digit));
    expect(onComplete).toHaveBeenCalledTimes(1);
    expect(onComplete).toHaveBeenCalledWith("804216");
    expect(screen.getByLabelText("Digit 6")).toHaveFocus();
    
  9. pasting fills from the box you paste into and skips anything that isn't a digit
    const onComplete = spy();
    render(<App onComplete={onComplete} />);
    await user.click(screen.getByLabelText("Digit 2"));
    await user.paste("4-5 6");
    expect(screen.getByLabelText("Digit 1")).toHaveValue("");
    expect(screen.getByLabelText("Digit 2")).toHaveValue("4");
    expect(screen.getByLabelText("Digit 3")).toHaveValue("5");
    expect(screen.getByLabelText("Digit 4")).toHaveValue("6");
    expect(screen.getByLabelText("Digit 5")).toHaveValue("");
    expect(screen.getByLabelText("Digit 5")).toHaveFocus();
    expect(onComplete).not.toHaveBeenCalled();
    
  10. pasted digits that don't fit are dropped
    render(<App onComplete={spy()} />);
    await user.click(screen.getByLabelText("Digit 4"));
    await user.paste("98765");
    expect(screen.getByLabelText("Digit 4")).toHaveValue("9");
    expect(screen.getByLabelText("Digit 5")).toHaveValue("8");
    expect(screen.getByLabelText("Digit 6")).toHaveValue("7");
    expect(screen.getByLabelText("Digit 6")).toHaveFocus();
    

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