Skip to content
Development

The cancel button that didn't cancel

By Victor Da Luz
sveltetestingdev-loggreenhouse

This week’s Greenhouse work was a small UX batch: rename a project inline, a “neglected for Nd” chip on stale worklist cards, image preview alongside the existing audio preview, and a “why these rules?” link back to the onboarding rules screen. None of it sounded risky.

The bug I almost shipped

The rename UI is dead simple: click a pencil icon, the title turns into a text input, Enter or clicking away saves, Escape cancels. I wrote the obvious version first:

function cancel() {
  editing = false;
}

Looked fine. Then I sat down to write the regression test for Escape specifically, and to make the test actually mean something I simulated the real sequence: press Escape, then fire the blur that happens right after (removing a focused input from the DOM triggers a blur event on it, before the browser finishes tearing it down).

That’s when it broke. My save() function runs on blur, saw the user’s still-typed, uncommitted text sitting in the bound value (because cancel() never touched it, just closed the editor), and saved it anyway. Escape looked like it worked in a normal click-through. It didn’t actually cancel anything, it just got lucky that nothing usually blurs an input you’re in the middle of removing.

The fix

One line: reset the value before closing.

function cancel() {
  value = original;
  editing = false;
}

save() already had a “no-op if nothing changed” guard (needed for the ordinary case of opening the field and blurring without typing anything). Once cancel() resets the value first, that guard makes the whole race harmless regardless of which order the events land in.

Why manual clicking wouldn’t have caught this

This is the part that stuck with me. If I’d just clicked around the feature by hand, Escape looks completely correct, nothing visibly saves. The bug only exists in the gap between “the input closes” and “does a stray browser event fire during that close,” and browsers don’t reliably fire that blur on every removal path, so even repeated manual testing might not have shown it. I only found it because writing a real regression test forced me to simulate the exact event order, not because I was hunting for a race.

Also this session

Verified all four pieces of the batch against the real running app over actual IPC, not mocked tests, using the WebDriver harness built up over previous sessions. One more small thing worth a footnote: you can’t pass a found DOM element as a script argument to this harness’s script-execution endpoint and expect it to still be a real element on the other side, it deserializes as a plain object. The fix is to just re-query it with document.querySelector inside the script itself.

Related reading

Development

A dashboard that stopped telling time

A $derived block that read Date.now() exactly once, a midnight that meant different things to the frontend and the Rust engine, and the grammatically broken sentence that proved it.

Read