diff --git a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts index 3b58a62f..a0486e6d 100644 --- a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts +++ b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts @@ -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(