test(person-mention): replace setTimeout waits with vi.waitFor

Sara #1 + Felix #4: setTimeout(r, 50) and setTimeout(r, 5) were racing the
microtask queue — passes on a fast laptop, will fail on a loaded CI runner.
Replace all six occurrences with vi.waitFor(() => expect(...)) which polls
until the assertion passes (default 1s timeout, 10ms interval).

Tests are now deterministic — they pass the moment the condition is true,
fail the moment the timeout elapses, and never spuriously time out on slow
CI hardware.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-29 09:04:02 +02:00
parent 060a1149e0
commit 515fa03088

View File

@@ -280,13 +280,14 @@ describe('TranscriptionReadView — person-mention rendering', () => {
const link = document.querySelector('a.person-mention')!; const link = document.querySelector('a.person-mention')!;
link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
await new Promise((r) => setTimeout(r, 10));
await vi.waitFor(() => {
const personFetches = fetchMock.mock.calls.filter((c) => const personFetches = fetchMock.mock.calls.filter((c) =>
String(c[0]).includes(`/api/persons/${PERSON_ID}`) String(c[0]).includes(`/api/persons/${PERSON_ID}`)
); );
expect(personFetches.length).toBeGreaterThanOrEqual(1); expect(personFetches.length).toBeGreaterThanOrEqual(1);
}); });
});
it('deduplicates fetches for the same personId across multiple mouseenter events (B15.5)', async () => { it('deduplicates fetches for the same personId across multiple mouseenter events (B15.5)', async () => {
const fetchMock = vi.fn().mockResolvedValue({ const fetchMock = vi.fn().mockResolvedValue({
@@ -308,13 +309,13 @@ describe('TranscriptionReadView — person-mention rendering', () => {
// Plus a re-hover on the first // Plus a re-hover on the first
links[0].dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); links[0].dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
await new Promise((r) => setTimeout(r, 10)); await vi.waitFor(() => {
const personFetches = fetchMock.mock.calls.filter( const personFetches = fetchMock.mock.calls.filter(
(c) => String(c[0]) === `/api/persons/${PERSON_ID}` (c) => String(c[0]) === `/api/persons/${PERSON_ID}`
); );
expect(personFetches.length).toBe(1); expect(personFetches.length).toBe(1);
}); });
});
it('mounts the hover card on mouseenter when the fetch loads', async () => { it('mounts the hover card on mouseenter when the fetch loads', async () => {
vi.stubGlobal( vi.stubGlobal(
@@ -349,10 +350,11 @@ describe('TranscriptionReadView — person-mention rendering', () => {
const link = document.querySelector('a.person-mention')!; const link = document.querySelector('a.person-mention')!;
link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
await new Promise((r) => setTimeout(r, 50)); await vi.waitFor(() => {
const card = document.querySelector('[data-testid="person-hover-card"]'); const card = document.querySelector('[data-testid="person-hover-card"]');
expect(card).not.toBeNull(); expect(card).not.toBeNull();
}); });
});
it('unmounts the hover card on mouseleave', async () => { it('unmounts the hover card on mouseleave', async () => {
render(TranscriptionReadView, { render(TranscriptionReadView, {
@@ -362,13 +364,13 @@ describe('TranscriptionReadView — person-mention rendering', () => {
const link = document.querySelector('a.person-mention')!; const link = document.querySelector('a.person-mention')!;
link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
await new Promise((r) => setTimeout(r, 5));
link.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true })); link.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
await new Promise((r) => setTimeout(r, 5));
await vi.waitFor(() => {
const card = document.querySelector('[data-testid="person-hover-card"]'); const card = document.querySelector('[data-testid="person-hover-card"]');
expect(card).toBeNull(); expect(card).toBeNull();
}); });
});
it('mounts the hover card on focusin so keyboard users see the preview (WCAG 2.1.1)', async () => { it('mounts the hover card on focusin so keyboard users see the preview (WCAG 2.1.1)', async () => {
vi.stubGlobal( vi.stubGlobal(
@@ -468,13 +470,15 @@ describe('TranscriptionReadView — person-mention rendering', () => {
const link = document.querySelector('a.person-mention')!; const link = document.querySelector('a.person-mention')!;
link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
await new Promise((r) => setTimeout(r, 50));
// 404 → no card mounted await vi.waitFor(() => {
const card = document.querySelector('[data-testid="person-hover-card"]');
expect(card).toBeNull();
// Anchor is marked as deleted so subsequent hovers/clicks treat it as plain text // Anchor is marked as deleted so subsequent hovers/clicks treat it as plain text
const stillLink = document.querySelector('a.person-mention')!; const stillLink = document.querySelector('a.person-mention')!;
expect(stillLink.getAttribute('data-person-deleted')).toBe('true'); expect(stillLink.getAttribute('data-person-deleted')).toBe('true');
}); });
// 404 → no card mounted
const card = document.querySelector('[data-testid="person-hover-card"]');
expect(card).toBeNull();
});
}); });