test: increase coverage

This commit is contained in:
Marcel
2026-04-06 11:20:57 +02:00
parent f359c19e4c
commit e89d8a4ca9
7 changed files with 1306 additions and 0 deletions

View File

@@ -159,3 +159,59 @@ describe('TranscriptionBlock — reorder controls', () => {
expect(onMoveDown).toHaveBeenCalled();
});
});
// ─── Delete confirmation ──────────────────────────────────────────────────────
describe('TranscriptionBlock — delete confirmation', () => {
it('does not call onDeleteClick when user cancels confirm dialog', async () => {
const onDeleteClick = vi.fn();
vi.spyOn(window, 'confirm').mockReturnValue(false);
renderBlock({ onDeleteClick });
const deleteBtn = page.getByRole('button', { name: 'Löschen' });
await deleteBtn.click();
expect(onDeleteClick).not.toHaveBeenCalled();
vi.restoreAllMocks();
});
it('calls onDeleteClick when user confirms deletion', async () => {
const onDeleteClick = vi.fn();
vi.spyOn(window, 'confirm').mockReturnValue(true);
renderBlock({ onDeleteClick });
const deleteBtn = page.getByRole('button', { name: 'Löschen' });
await deleteBtn.click();
expect(onDeleteClick).toHaveBeenCalledOnce();
vi.restoreAllMocks();
});
});
// ─── Quote selection ─────────────────────────────────────────────────────────
describe('TranscriptionBlock — quote selection', () => {
it('shows quote hint after text is selected in textarea', async () => {
renderBlock({ text: 'Breslau, den 12. August' });
const textarea = page.getByRole('textbox');
// Select all text via keyboard shortcut to trigger mouseup with selection
await textarea.click();
await textarea.selectText();
// Fire mouseup to trigger the selection handler
await textarea.dispatchEvent('mouseup');
await expect.element(page.getByText(/Zitat/)).toBeInTheDocument();
});
});
// ─── Fading state ────────────────────────────────────────────────────────────
describe('TranscriptionBlock — fading save state', () => {
it('shows Gespeichert text in fading state (opacity-0 fade-out)', async () => {
renderBlock({ saveState: 'fading' });
const indicator = page.getByText(/Gespeichert/);
await expect.element(indicator).toBeInTheDocument();
// The fading class sets opacity-0
const el = document.querySelector('.opacity-0');
expect(el).not.toBeNull();
});
});

View File

@@ -93,3 +93,132 @@ describe('TranscriptionEditView — reorder', () => {
expect(handles.length).toBe(2);
});
});
// ─── Auto-save debounce ───────────────────────────────────────────────────────
describe('TranscriptionEditView — auto-save debounce', () => {
it('calls onSaveBlock after 1500ms debounce when text changes', async () => {
vi.useFakeTimers();
const onSaveBlock = vi.fn().mockResolvedValue(undefined);
renderView({ onSaveBlock });
const textarea = page.getByRole('textbox').first();
await textarea.fill('Neue Zeile');
// Not called immediately
expect(onSaveBlock).not.toHaveBeenCalled();
// Advance past debounce
vi.advanceTimersByTime(1500);
await vi.runAllTimersAsync();
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Neue Zeile');
vi.useRealTimers();
});
it('resets debounce timer on rapid successive changes', async () => {
vi.useFakeTimers();
const onSaveBlock = vi.fn().mockResolvedValue(undefined);
renderView({ onSaveBlock });
const textarea = page.getByRole('textbox').first();
await textarea.fill('First');
vi.advanceTimersByTime(500);
await textarea.fill('Second');
vi.advanceTimersByTime(500);
// 1000ms elapsed since first change — should not have saved yet
expect(onSaveBlock).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
await vi.runAllTimersAsync();
// Only one save with the final value
expect(onSaveBlock).toHaveBeenCalledTimes(1);
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Second');
vi.useRealTimers();
});
});
// ─── Save state transitions ───────────────────────────────────────────────────
describe('TranscriptionEditView — save state indicators', () => {
it('shows saving indicator while onSaveBlock is in-flight', async () => {
vi.useFakeTimers();
let resolveSave!: () => void;
const onSaveBlock = vi.fn().mockReturnValue(new Promise<void>((r) => (resolveSave = r)));
renderView({ onSaveBlock });
await page.getByRole('textbox').first().fill('Hello');
vi.advanceTimersByTime(1500);
await vi.runAllTimersAsync();
await expect.element(page.getByText('Speichere...')).toBeInTheDocument();
resolveSave();
vi.useRealTimers();
});
it('shows error state when onSaveBlock rejects', async () => {
vi.useFakeTimers();
const onSaveBlock = vi.fn().mockRejectedValue(new Error('network'));
renderView({ onSaveBlock });
await page.getByRole('textbox').first().fill('Fails');
vi.advanceTimersByTime(1500);
await vi.runAllTimersAsync();
await expect.element(page.getByText('Nicht gespeichert')).toBeInTheDocument();
await expect.element(page.getByText('Erneut versuchen')).toBeInTheDocument();
vi.useRealTimers();
});
});
// ─── Flush on blur ────────────────────────────────────────────────────────────
describe('TranscriptionEditView — flush on blur', () => {
it('flushes pending save immediately on textarea blur before debounce expires', async () => {
vi.useFakeTimers();
const onSaveBlock = vi.fn().mockResolvedValue(undefined);
renderView({ onSaveBlock });
const textarea = page.getByRole('textbox').first();
await textarea.fill('Blur text');
// Blur before 1500ms debounce fires
await textarea.blur();
await vi.runAllTimersAsync();
expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Blur text');
vi.useRealTimers();
});
});
// ─── onDeleteBlock callback ───────────────────────────────────────────────────
describe('TranscriptionEditView — delete block', () => {
it('calls onDeleteBlock with correct blockId when delete is confirmed', async () => {
const onDeleteBlock = vi.fn().mockResolvedValue(undefined);
vi.spyOn(window, 'confirm').mockReturnValue(true);
renderView({ onDeleteBlock });
const deleteBtn = page.getByRole('button', { name: 'Löschen' }).first();
await deleteBtn.click();
expect(onDeleteBlock).toHaveBeenCalledWith('b1');
vi.restoreAllMocks();
});
it('does not call onDeleteBlock when deletion is cancelled', async () => {
const onDeleteBlock = vi.fn();
vi.spyOn(window, 'confirm').mockReturnValue(false);
renderView({ onDeleteBlock });
const deleteBtn = page.getByRole('button', { name: 'Löschen' }).first();
await deleteBtn.click();
expect(onDeleteBlock).not.toHaveBeenCalled();
vi.restoreAllMocks();
});
});