fix(sidebar): cancel pending debounce emit on external value reset (#1244)

SidebarSearch's value-sync effect adopted external resets but left the
pending setTimeout in place. When a clear or filter-driven reset arrived
inside the 120ms window, the stale timer would fire after the adopt and
emit the previously-typed query back to the parent, silently undoing the
reset. The skip condition also leaned on lastEmittedRef, which kept a
genuine reset from winning if its value happened to equal the last emit.

Switch the skip to compare the parent value against the locally shown
value (tracked through a ref so the effect deps stay on [value]). On any
external transition the effect now clears the pending timer before
adopting, removing the race entirely. lastEmittedRef is dead under this
model and is removed.

Adds a fake-timer test that types mid-window, rerenders with a different
value before the debounce fires, advances past the original deadline, and
asserts the parent never receives the stale emit.
This commit is contained in:
Anso
2026-05-28 15:27:26 -04:00
committed by GitHub
parent 755c3c82cb
commit 0a8e6a79ae
2 changed files with 55 additions and 9 deletions
@@ -88,4 +88,35 @@ describe('SidebarSearch', () => {
expect(input.value).toBe('');
});
it('cancels the pending debounce emit when the parent resets the value mid-window', () => {
const onValueChange = vi.fn();
// Start at a non-empty initial so the later reset to '' is a real prop
// transition; rerendering with the same string would no-op in React.
const { getByPlaceholderText, rerender } = renderInsideCommand({ value: 'initial', onValueChange });
const input = getByPlaceholderText('Search stacks...') as HTMLInputElement;
expect(input.value).toBe('initial');
act(() => {
fireEvent.input(input, { target: { value: 'web' } });
});
act(() => {
vi.advanceTimersByTime(50);
});
expect(onValueChange).not.toHaveBeenCalled();
rerender(
<Command shouldFilter={false}>
<SidebarSearch value="" onValueChange={onValueChange} />
</Command>,
);
act(() => {
vi.advanceTimersByTime(200);
});
// The stale timer must not fire and re-emit 'web', undoing the reset.
expect(onValueChange).not.toHaveBeenCalled();
expect(input.value).toBe('');
});
});