feat(transcription): decouple @mention display text from person search (#380) #629
@@ -268,6 +268,13 @@ onMount(() => {
|
||||
return exports?.onKeyDown(event) ?? false;
|
||||
},
|
||||
onExit() {
|
||||
// Cancel any pending debounce so a closed dropdown's trailing
|
||||
// runSearch cannot fire against the *next* dropdown's state.
|
||||
// The hoisted `cancelPendingSearch` would be overwritten by
|
||||
// the next render()'s onStart before the trailing call fires,
|
||||
// so we cancel locally via the closure-scoped debouncedSearch.
|
||||
// Felix #1 on PR #629.
|
||||
debouncedSearch.cancel();
|
||||
if (mountedDropdown) {
|
||||
unmount(mountedDropdown);
|
||||
mountedDropdown = null;
|
||||
|
||||
@@ -309,6 +309,46 @@ describe('PersonMentionEditor — stale-response race', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── onExit cancels pending debounce (Felix #1 on PR #629) ───────────────────
|
||||
|
||||
describe('PersonMentionEditor — onExit cancels pending debounce', () => {
|
||||
it('cancels the pending debounced fetch when Escape closes the dropdown before the debounce fires', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) });
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
renderHost();
|
||||
|
||||
// Open the dropdown by typing @ + a query in the editor.
|
||||
await userEvent.type(page.getByRole('textbox'), '@A');
|
||||
await vi.waitFor(async () => {
|
||||
await expect.element(page.getByRole('searchbox')).toBeVisible();
|
||||
});
|
||||
|
||||
// Wait for any in-flight fetch from opening the dropdown to settle.
|
||||
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));
|
||||
|
||||
const newFetches = fetchMock.mock.calls.slice(fetchesBeforeEscape);
|
||||
const walterFetches = newFetches.filter(
|
||||
([url]) => typeof url === 'string' && url.includes('q=Walter')
|
||||
);
|
||||
expect(walterFetches.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── AC-1: search input prefilled with text typed after @ ───────────────────
|
||||
|
||||
describe('PersonMentionEditor — AC-1: search input prefill', () => {
|
||||
|
||||
Reference in New Issue
Block a user