test(person-mention): drive editor specs via fake timers

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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-29 01:16:55 +02:00
parent 09f71a2dce
commit e8ba840560

View File

@@ -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();