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
The bug my unit tests could never have found
A kanban board rendered a prop that went stale the moment other data changed, and every mocked test passed because a mock can't express staleness.
A two-second accessibility gate that runs on every push
axe-core over the existing component-test harness, scoped to actual WCAG rules - and an honest accounting of what a jsdom gate structurally cannot catch.
Adding Spanish and Portuguese to a desktop app, and getting caught on my own assumption
A reload-based locale switch proven against the real compiled app, a settings file that turned out to matter, and a Spanish word from the wrong hemisphere.
You might also find useful
Proton VPN
Commercial VPN with NetShield filtering and a kill switch.
As a Proton Partner, I earn from qualifying purchases of Proton's privacy and security services (Pass, Mail, VPN, Drive).
Learn moreAdGuard for iOS
System-wide ad and tracker blocking on iOS, no separate DNS server required.
As an AdGuard affiliate, I earn from qualifying purchases.
Learn moreAiralo eSIM
Local data eSIM for travel - no physical SIM swap needed.
This is my Airalo referral link. You get a discount on your first eSIM, and I earn Airalo credit toward mine.
Learn more