From e8ba8405604f5d26ce62177c8cd91843612b85ac Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 29 Apr 2026 01:16:55 +0200 Subject: [PATCH] test(person-mention): drive editor specs via fake timers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tester #5506 §1: 14 tests × 250ms real-timer waits = 3.5s wall-clock, also racing the 200ms internal debounce by only 50ms — a flake on a busy CI runner. Switch to vi.useFakeTimers + advanceTimersByTimeAsync; test execution now 236ms (was 3.08s), determinism guaranteed because the debounce runs against the fake clock. Co-Authored-By: Claude Sonnet 4.6 --- .../PersonMentionEditor.svelte.spec.ts | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/frontend/src/lib/components/PersonMentionEditor.svelte.spec.ts b/frontend/src/lib/components/PersonMentionEditor.svelte.spec.ts index 25362537..a0c77395 100644 --- a/frontend/src/lib/components/PersonMentionEditor.svelte.spec.ts +++ b/frontend/src/lib/components/PersonMentionEditor.svelte.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, afterEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import PersonMentionEditorHost from './PersonMentionEditor.test-host.svelte'; @@ -7,8 +7,19 @@ import type { components } from '$lib/generated/api'; type Person = components['schemas']['Person']; type PersonMention = components['schemas']['PersonMention']; -const waitForDebounce = () => new Promise((r) => setTimeout(r, 250)); -const tick = () => new Promise((r) => setTimeout(r, 0)); +// Editor's internal search debounce is 200ms — drive it via fake timers +// so tests are deterministic and fast (Tester #5506 §1). +const DEBOUNCE_MS = 200; + +async function flushDebounce() { + await vi.advanceTimersByTimeAsync(DEBOUNCE_MS); + // Let the awaited fetch resolve and the resulting state assignments flush. + await vi.runAllTimersAsync(); +} + +async function tick() { + await vi.advanceTimersByTimeAsync(0); +} const AUGUSTE: Person = { id: 'p-aug', @@ -73,9 +84,14 @@ function renderHost(initial: { value?: string; mentionedPersons?: PersonMention[ }; } +beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }); +}); + afterEach(() => { cleanup(); vi.unstubAllGlobals(); + vi.useRealTimers(); }); // ─── Rendering ──────────────────────────────────────────────────────────────── @@ -115,7 +131,7 @@ describe('PersonMentionEditor — typeahead', () => { ta.selectionEnd = 4; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); await expect.element(page.getByText('Auguste Raddatz')).toBeInTheDocument(); }); @@ -131,7 +147,7 @@ describe('PersonMentionEditor — typeahead', () => { ta.selectionEnd = 4; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/persons?q=Aug')); }); @@ -146,7 +162,7 @@ describe('PersonMentionEditor — typeahead', () => { ta.selectionStart = 4; ta.selectionEnd = 4; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); await expect.element(page.getByText('* 1882 – † 1944')).toBeInTheDocument(); }); @@ -161,7 +177,7 @@ describe('PersonMentionEditor — typeahead', () => { ta.selectionStart = 4; ta.selectionEnd = 4; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); await expect.element(page.getByText('Keine Personen gefunden')).toBeInTheDocument(); }); @@ -176,7 +192,7 @@ describe('PersonMentionEditor — typeahead', () => { ta.selectionStart = 9; ta.selectionEnd = 9; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); await expect.element(page.getByText('Auguste Raddatz')).toBeInTheDocument(); }); @@ -195,7 +211,7 @@ describe('PersonMentionEditor — selecting a person', () => { ta.selectionStart = 4; ta.selectionEnd = 4; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); clickOption('p-aug'); await tick(); @@ -213,7 +229,7 @@ describe('PersonMentionEditor — selecting a person', () => { ta.selectionStart = 4; ta.selectionEnd = 4; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); clickOption('p-aug'); await tick(); @@ -236,7 +252,7 @@ describe('PersonMentionEditor — selecting a person', () => { ta.selectionStart = ta.value.length; ta.selectionEnd = ta.value.length; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); clickOption('p-aug'); await tick(); @@ -258,7 +274,7 @@ describe('PersonMentionEditor — keyboard navigation (B11b)', () => { ta.selectionStart = 2; ta.selectionEnd = 2; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); const optAuguste = document.querySelector( '[role="option"][data-test-person-id="p-aug"]' @@ -293,7 +309,7 @@ describe('PersonMentionEditor — keyboard navigation (B11b)', () => { ta.selectionStart = 2; ta.selectionEnd = 2; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); ta.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); await tick(); @@ -315,7 +331,7 @@ describe('PersonMentionEditor — keyboard navigation (B11b)', () => { ta.selectionStart = 4; ta.selectionEnd = 4; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); ta.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); await tick(); @@ -339,7 +355,7 @@ describe('PersonMentionEditor — touch target', () => { ta.selectionStart = 4; ta.selectionEnd = 4; ta.dispatchEvent(new Event('input', { bubbles: true })); - await waitForDebounce(); + await flushDebounce(); const option = document.querySelector('[role="option"]') as HTMLElement; expect(option).not.toBeNull();