fix(tests): fix 2 more pre-existing vitest-browser spec failures

TranscriptionEditView: fix 4 failing tests:
- textarea → [role="textbox"] selector (editor is contenteditable, not <textarea>)
- button clicks → dispatchEvent(MouseEvent) for reliable Svelte 5 onclick with TipTap
- mentionedPersons test: init block with @mention token so deserialize() creates a
  mention node; use userEvent.type + vi.waitFor (real timers) instead of fill +
  fake timers, which prevents TipTap onUpdate from firing the debounce timer

EntityNavSection: anchor link click → add capture-phase preventDefault before
clicking to stop iframe navigation while allowing Svelte onclick handler to run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-07 12:22:06 +02:00
parent 6ab7abb9df
commit cdb54c7545
2 changed files with 39 additions and 18 deletions

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import { page, userEvent } from 'vitest/browser';
import TranscriptionEditView from './TranscriptionEditView.svelte';
import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js';
@@ -148,24 +148,28 @@ describe('TranscriptionEditView — auto-save debounce', () => {
});
it('passes the block mentionedPersons array as the 3rd save argument', async () => {
vi.useFakeTimers();
const onSaveBlock = vi.fn().mockResolvedValue(undefined);
const blockWithMention = {
...block1,
// text must contain the @displayName token so deserialize() creates a mention node;
// fill() replaces the whole content with plain text and would destroy the node
text: '@Auguste Raddatz',
mentionedPersons: [{ personId: 'p-aug', displayName: 'Auguste Raddatz' }]
};
renderView({ blocks: [blockWithMention], onSaveBlock });
const textarea = page.getByRole('textbox').first();
await textarea.fill('Hallo @Auguste Raddatz');
// type() focuses the element (cursor at position 0) then inserts without replacing the
// existing mention node. Fake timers interfere with keyboard CDP so use real timers
// + vi.waitFor to catch the 1500 ms debounce.
await userEvent.type(page.getByRole('textbox').first(), 'Hallo ');
vi.advanceTimersByTime(1500);
await vi.runAllTimersAsync();
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Hallo @Auguste Raddatz', [
{ personId: 'p-aug', displayName: 'Auguste Raddatz' }
]);
vi.useRealTimers();
await vi.waitFor(
() =>
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Hallo @Auguste Raddatz', [
{ personId: 'p-aug', displayName: 'Auguste Raddatz' }
]),
{ timeout: 3000 }
);
});
it('resets debounce timer on rapid successive changes', async () => {
@@ -238,8 +242,9 @@ describe('TranscriptionEditView — flush on blur', () => {
const textarea = page.getByRole('textbox').first();
await textarea.fill('Blur text');
// Blur before 1500ms debounce fires — locator.blur() not available, use native DOM
const el = document.querySelector('textarea') as HTMLTextAreaElement;
// Blur before 1500ms debounce fires — locator.blur() not available, use native DOM.
// PersonMentionEditor uses a contenteditable div (role=textbox), not a <textarea>.
const el = document.querySelector('[role="textbox"]') as HTMLElement;
el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
await vi.runAllTimersAsync();
@@ -335,7 +340,12 @@ describe('TranscriptionEditView — mark all reviewed', () => {
onMarkAllReviewed
});
await page.getByRole('button', { name: /Alle als fertig markieren/ }).click();
// userEvent.click() via Playwright CDP doesn't reliably trigger Svelte 5 onclick
// handlers when a TipTap editor is mounted in the same component tree.
const btn = (await page
.getByRole('button', { name: /Alle als fertig markieren/ })
.element()) as HTMLButtonElement;
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
await vi.waitFor(() => expect(onMarkAllReviewed).toHaveBeenCalledTimes(1));
});
@@ -349,9 +359,14 @@ describe('TranscriptionEditView — mark all reviewed', () => {
onMarkAllReviewed
});
const btn = page.getByRole('button', { name: /Alle als fertig markieren/ });
await btn.click();
await expect.element(btn).toBeDisabled();
// Same CDP click workaround: dispatch from browser JS to reliably fire Svelte 5 onclick
const btnEl = (await page
.getByRole('button', { name: /Alle als fertig markieren/ })
.element()) as HTMLButtonElement;
btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
await expect
.element(page.getByRole('button', { name: /Alle als fertig markieren/ }))
.toBeDisabled();
resolveMarkAll();
});
});

View File

@@ -134,7 +134,13 @@ describe('EntityNavSection — flyout variant', () => {
called = true;
}
});
document.querySelector<HTMLAnchorElement>('a[href="/admin/users"]')!.click();
const link = document.querySelector<HTMLAnchorElement>('a[href="/admin/users"]')!;
// Prevent the browser from navigating the test iframe to /admin/users (which
// would redirect to /login and kill the iframe connection). preventDefault()
// on the capture phase suppresses navigation while still letting the Svelte
// onclick handler (onFlyoutClick) run on the bubbling phase.
link.addEventListener('click', (e) => e.preventDefault(), { capture: true, once: true });
link.click();
expect(called).toBe(true);
});
});