The cancel button that didn't cancel
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
Six small UI items, and the two near-misses hiding inside them
A CSS block that grep said was dead but a test depended on, and a single line of localStorage that broke thirty-nine unrelated tests because of a Node upgrade.
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.
The bug my unit tests could never have found
A kanban board rendered a prop that went stale the moment new data existed anywhere else - and every mocked test passed, because a mock can't express staleness.