test(debounce): fix flaky onExit-cancels-debounce test
All checks were successful
CI / fail2ban Regex (push) Successful in 42s
CI / Unit & Component Tests (pull_request) Successful in 4m5s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m35s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
CI / Unit & Component Tests (push) Successful in 3m46s
CI / OCR Service Tests (push) Successful in 22s
CI / Backend Unit Tests (push) Successful in 3m27s
CI / Semgrep Security Scan (push) Successful in 25s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
nightly / deploy-staging (push) Successful in 4m0s
All checks were successful
CI / fail2ban Regex (push) Successful in 42s
CI / Unit & Component Tests (pull_request) Successful in 4m5s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m35s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
CI / Unit & Component Tests (push) Successful in 3m46s
CI / OCR Service Tests (push) Successful in 22s
CI / Backend Unit Tests (push) Successful in 3m27s
CI / Semgrep Security Scan (push) Successful in 25s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
nightly / deploy-staging (push) Successful in 4m0s
The test raced a real 150 ms setTimeout: fill('Walter') started the
debounce, then focus + keyboard(Escape) had to complete before 150 ms
elapsed. Under CI load the Playwright CDP round-trips exceeded 150 ms,
letting the debounce fire first.
Fix: install vi.useFakeTimers() after the stable-state setup (so
vi.waitFor()'s real-timer polling still works), freeze the Walter
debounce, let Escape trigger onExit/cancel, then advance fake time
with vi.advanceTimersByTimeAsync() — no real-wall-clock race.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #661.
This commit is contained in:
@@ -409,19 +409,24 @@ describe('PersonMentionEditor — onExit cancels pending debounce', () => {
|
||||
await new Promise((r) => setTimeout(r, SEARCH_DEBOUNCE_MS + POST_DEBOUNCE_SLACK_MS));
|
||||
const fetchesBeforeEscape = fetchMock.mock.calls.length;
|
||||
|
||||
// Trigger a new debounced search (queues runSearch after 150 ms), then
|
||||
// immediately Escape *while focus is back in the editor* so Tiptap's
|
||||
// suggestion-plugin Escape handler fires onExit before the debounce.
|
||||
// Without onExit cancelling the pending debounce, runSearch executes
|
||||
// against the now-unmounted dropdown's state.
|
||||
await page.getByRole('searchbox').fill('Walter');
|
||||
// Focus the editor so the Escape lands on Tiptap's suggestion handler.
|
||||
(page.getByRole('textbox').element() as HTMLElement).focus();
|
||||
await userEvent.keyboard('{Escape}');
|
||||
|
||||
// Wait past the debounce window. If onExit did not cancel the pending
|
||||
// debounce, a fetch with q=Walter would still fire here.
|
||||
await new Promise((r) => setTimeout(r, SEARCH_DEBOUNCE_MS + POST_DEBOUNCE_SLACK_MS));
|
||||
// Freeze setTimeout so the 150 ms debounce cannot fire before Escape
|
||||
// triggers onExit. We install fake timers only now — after the setup
|
||||
// above — so that vi.waitFor()'s real-timer polling still worked.
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
// fill() dispatches the input event synchronously via CDP; by the
|
||||
// time the await resolves, onSearch('Walter') has run and the fake
|
||||
// debounce timer is set.
|
||||
await page.getByRole('searchbox').fill('Walter');
|
||||
// Focus the editor so the Escape lands on Tiptap's suggestion handler.
|
||||
(page.getByRole('textbox').element() as HTMLElement).focus();
|
||||
await userEvent.keyboard('{Escape}');
|
||||
// onExit has now called debouncedSearch.cancel(). Advance past the
|
||||
// debounce window — the cancelled timer must not fire.
|
||||
await vi.advanceTimersByTimeAsync(SEARCH_DEBOUNCE_MS + POST_DEBOUNCE_SLACK_MS);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
const newFetches = fetchMock.mock.calls.slice(fetchesBeforeEscape);
|
||||
const walterFetches = newFetches.filter(
|
||||
|
||||
Reference in New Issue
Block a user