Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
TranscriptionBlock: - Desktop: grip handle (⠿) on left side, serves as drag handle - Mobile (<768px): ▲/▼ arrow buttons (44px tap targets) replace grip - isFirst/isLast disable boundary arrows - onMoveUp/onMoveDown callbacks for arrow button clicks TranscriptionEditView: - HTML5 drag-and-drop on block wrappers (only initiates from grip handle) - Dragged block shows 40% opacity - On drop: reorder array and call PUT /reorder endpoint - Arrow handlers: swap adjacent blocks and call reorder endpoint 5 new tests: - drag handle element present - move-up disabled when isFirst - move-down disabled when isLast - onMoveUp fires on click - onMoveDown fires on click Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page } from 'vitest/browser';
|
|
import TranscriptionBlock from './TranscriptionBlock.svelte';
|
|
|
|
afterEach(cleanup);
|
|
|
|
function renderBlock(overrides: Record<string, unknown> = {}) {
|
|
return render(TranscriptionBlock, {
|
|
blockId: 'block-1',
|
|
documentId: 'doc-1',
|
|
blockNumber: 3,
|
|
text: 'Liebe Mutter,',
|
|
label: null,
|
|
active: false,
|
|
saveState: 'idle' as const,
|
|
canComment: true,
|
|
currentUserId: 'user-1',
|
|
onTextChange: vi.fn(),
|
|
onFocus: vi.fn(),
|
|
onDeleteClick: vi.fn(),
|
|
onRetry: vi.fn(),
|
|
...overrides
|
|
});
|
|
}
|
|
|
|
// ─── Rendering ───────────────────────────────────────────────────────────────
|
|
|
|
describe('TranscriptionBlock — rendering', () => {
|
|
it('renders block number in turquoise badge', async () => {
|
|
renderBlock();
|
|
await expect.element(page.getByText('3')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders text in textarea', async () => {
|
|
renderBlock();
|
|
const textarea = page.getByRole('textbox');
|
|
await expect.element(textarea).toHaveValue('Liebe Mutter,');
|
|
});
|
|
|
|
it('renders optional label when provided', async () => {
|
|
renderBlock({ label: 'Anrede' });
|
|
await expect.element(page.getByText('Anrede')).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not render label when null', async () => {
|
|
renderBlock({ label: null });
|
|
const label = page.getByText('Anrede');
|
|
await expect.element(label).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// ─── Save states ─────────────────────────────────────────────────────────────
|
|
|
|
describe('TranscriptionBlock — save states', () => {
|
|
it('shows nothing in idle state', async () => {
|
|
renderBlock({ saveState: 'idle' });
|
|
const saving = page.getByText('Speichere...');
|
|
await expect.element(saving).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows "Speichere..." in saving state', async () => {
|
|
renderBlock({ saveState: 'saving' });
|
|
await expect.element(page.getByText('Speichere...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows "Gespeichert" in saved state', async () => {
|
|
renderBlock({ saveState: 'saved' });
|
|
await expect.element(page.getByText(/Gespeichert/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows error with retry button in error state', async () => {
|
|
const onRetry = vi.fn();
|
|
renderBlock({ saveState: 'error', onRetry });
|
|
await expect.element(page.getByText('Nicht gespeichert')).toBeInTheDocument();
|
|
const retryBtn = page.getByText('Erneut versuchen');
|
|
await expect.element(retryBtn).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// ─── Active state ────────────────────────────────────────────────────────────
|
|
|
|
describe('TranscriptionBlock — active border', () => {
|
|
it('has turquoise left border when active', async () => {
|
|
renderBlock({ active: true });
|
|
await expect.element(page.getByRole('textbox')).toBeInTheDocument();
|
|
const block = document.querySelector('[data-block-id="block-1"]')!;
|
|
expect(block.className).toContain('border-turquoise');
|
|
});
|
|
|
|
it('has error left border when save failed', async () => {
|
|
renderBlock({ saveState: 'error' });
|
|
await expect.element(page.getByRole('textbox')).toBeInTheDocument();
|
|
const block = document.querySelector('[data-block-id="block-1"]')!;
|
|
expect(block.className).toContain('border-error');
|
|
});
|
|
});
|
|
|
|
// ─── Interactions ────────────────────────────────────────────────────────────
|
|
|
|
describe('TranscriptionBlock — interactions', () => {
|
|
it('calls onTextChange when typing in textarea', async () => {
|
|
const onTextChange = vi.fn();
|
|
renderBlock({ onTextChange });
|
|
const textarea = page.getByRole('textbox');
|
|
await textarea.fill('Neue Zeile');
|
|
expect(onTextChange).toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls onFocus when textarea is focused', async () => {
|
|
const onFocus = vi.fn();
|
|
renderBlock({ onFocus });
|
|
const textarea = page.getByRole('textbox');
|
|
await textarea.click();
|
|
expect(onFocus).toHaveBeenCalled();
|
|
});
|
|
|
|
it('shows Kommentieren button when no comments exist', async () => {
|
|
renderBlock();
|
|
const btn = page.getByText('Kommentieren');
|
|
await expect.element(btn).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// ─── Reorder controls ────────────────────────────────────────────────────────
|
|
|
|
describe('TranscriptionBlock — reorder controls', () => {
|
|
it('shows a drag handle element', async () => {
|
|
renderBlock();
|
|
const handle = document.querySelector('[data-drag-handle]');
|
|
expect(handle).not.toBeNull();
|
|
});
|
|
|
|
it('disables move-up button when isFirst', async () => {
|
|
renderBlock({ isFirst: true });
|
|
const btn = page.getByRole('button', { name: 'Nach oben' });
|
|
await expect.element(btn).toBeDisabled();
|
|
});
|
|
|
|
it('disables move-down button when isLast', async () => {
|
|
renderBlock({ isLast: true });
|
|
const btn = page.getByRole('button', { name: 'Nach unten' });
|
|
await expect.element(btn).toBeDisabled();
|
|
});
|
|
|
|
it('calls onMoveUp when up arrow clicked', async () => {
|
|
const onMoveUp = vi.fn();
|
|
renderBlock({ onMoveUp, isFirst: false });
|
|
const btn = page.getByRole('button', { name: 'Nach oben' });
|
|
await btn.click();
|
|
expect(onMoveUp).toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls onMoveDown when down arrow clicked', async () => {
|
|
const onMoveDown = vi.fn();
|
|
renderBlock({ onMoveDown, isLast: false });
|
|
const btn = page.getByRole('button', { name: 'Nach unten' });
|
|
await btn.click();
|
|
expect(onMoveDown).toHaveBeenCalled();
|
|
});
|
|
});
|