refactor: move shared components to lib/shared/ sub-packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import type { DashboardPulseDTO } from '$lib/generated/api';
|
||||
|
||||
interface Props {
|
||||
pulse: DashboardPulseDTO | null;
|
||||
}
|
||||
|
||||
const { pulse }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if pulse !== null}
|
||||
<section class="rounded-sm border border-line bg-surface p-5">
|
||||
<p class="font-sans text-[11px] font-bold tracking-[.12em] text-ink-3 uppercase">
|
||||
{m.pulse_eyebrow()}
|
||||
</p>
|
||||
|
||||
{#if pulse.pages > 0}
|
||||
<h2 class="mt-1 font-serif text-[1.375rem] leading-snug text-ink">
|
||||
{m.pulse_headline({ pages: pulse.pages })}
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
{#if pulse.yourPages > 0}
|
||||
<p class="font-serif text-sm text-ink-2">
|
||||
{m.pulse_you({ pages: pulse.yourPages })}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if pulse.contributors.length > 0}
|
||||
<div class="mt-3 flex items-center gap-1">
|
||||
<p class="mr-1 font-sans text-[11px] text-ink-3">{m.pulse_contributors()}</p>
|
||||
{#each pulse.contributors as c (c.initials + c.color)}
|
||||
<span
|
||||
class="-ml-2 inline-flex h-7 w-7 items-center justify-center rounded-full font-sans text-[11px] font-bold text-white ring-2 ring-white first:ml-0"
|
||||
style="background:{c.color}"
|
||||
title={c.name ?? ''}>{c.initials}</span
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 grid grid-cols-3 gap-2">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-serif text-[1.875rem] leading-none font-bold text-ink"
|
||||
>{pulse.annotated}</span
|
||||
>
|
||||
<span class="flex items-center gap-1 font-sans text-[11px] text-ink-3">
|
||||
<span class="text-[8px]" style="color:#00c7b1">●</span>{m.pulse_transcribed()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-serif text-[1.875rem] leading-none font-bold text-ink"
|
||||
>{pulse.transcribed}</span
|
||||
>
|
||||
<span class="flex items-center gap-1 font-sans text-[11px] text-ink-3">
|
||||
<span class="text-[8px]" style="color:#5a8a6a">●</span>{m.pulse_reviewed()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-serif text-[1.875rem] leading-none font-bold text-ink"
|
||||
>{pulse.uploaded}</span
|
||||
>
|
||||
<span class="flex items-center gap-1 font-sans text-[11px] text-ink-3">
|
||||
<span class="text-[8px]" style="color:#3060b0">●</span>{m.pulse_uploaded()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import { getLocale } from '$lib/paraglide/runtime.js';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type Document = {
|
||||
id: string;
|
||||
title: string;
|
||||
updatedAt?: string;
|
||||
sender?: { id: string; firstName?: string | null; lastName: string; displayName: string };
|
||||
};
|
||||
|
||||
type StatsDTO = components['schemas']['StatsDTO'];
|
||||
|
||||
interface Props {
|
||||
recentDocs: Document[];
|
||||
stats?: StatsDTO | null;
|
||||
}
|
||||
|
||||
let { recentDocs, stats = null }: Props = $props();
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
// updatedAt is a full ISO datetime — no T12:00:00 noon-anchor needed here
|
||||
return new Intl.DateTimeFormat(getLocale(), {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}).format(new Date(dateStr));
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if recentDocs.length > 0}
|
||||
<div data-testid="dashboard-recent-docs" class="rounded-sm border border-line bg-surface p-6">
|
||||
<h2 class="mb-4 font-sans text-xs font-bold tracking-widest text-gray-400 uppercase">
|
||||
{m.dashboard_recent_heading()}
|
||||
</h2>
|
||||
{#each recentDocs as doc (doc.id)}
|
||||
<div
|
||||
data-testid="doc-row-{doc.id}"
|
||||
class="flex min-h-[44px] items-center justify-between border-b border-line py-2 last:border-0"
|
||||
>
|
||||
<a
|
||||
href="/documents/{doc.id}"
|
||||
class="font-serif text-lg text-ink hover:text-ink-2 hover:underline"
|
||||
>
|
||||
{doc.title}
|
||||
</a>
|
||||
{#if doc.updatedAt}
|
||||
<span
|
||||
data-testid="doc-date-{doc.id}"
|
||||
class="ml-2 shrink-0 font-sans text-xs text-gray-400"
|
||||
>
|
||||
{formatDate(doc.updatedAt)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if stats?.totalDocuments != null}
|
||||
<p data-testid="dashboard-stats-footnote" class="mt-4 font-sans text-sm text-ink-3">
|
||||
{stats.totalDocuments}
|
||||
{m.dashboard_stats_documents()} · {stats.totalPersons}
|
||||
{m.dashboard_stats_persons()}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
|
||||
import DashboardRecentDocuments from './DashboardRecentDocuments.svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
type Document = {
|
||||
id: string;
|
||||
title: string;
|
||||
updatedAt?: string;
|
||||
sender?: { id: string; firstName: string; lastName: string };
|
||||
};
|
||||
|
||||
function makeDoc(id: string, title: string, updatedAt?: string): Document {
|
||||
return { id, title, updatedAt };
|
||||
}
|
||||
|
||||
describe('DashboardRecentDocuments', () => {
|
||||
it('renders nothing when recentDocs is empty', async () => {
|
||||
render(DashboardRecentDocuments, { recentDocs: [] });
|
||||
const widget = page.getByTestId('dashboard-recent-docs');
|
||||
await expect.element(widget).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the widget when recentDocs are present', async () => {
|
||||
render(DashboardRecentDocuments, { recentDocs: [makeDoc('d1', 'Taufschein')] });
|
||||
const widget = page.getByTestId('dashboard-recent-docs');
|
||||
await expect.element(widget).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a link to /documents/{id} for each document', async () => {
|
||||
const docs = [makeDoc('d1', 'Taufschein'), makeDoc('d2', 'Heiratsurkunde')];
|
||||
render(DashboardRecentDocuments, { recentDocs: docs });
|
||||
const links = page.getByRole('link');
|
||||
await expect.element(links.nth(0)).toHaveAttribute('href', '/documents/d1');
|
||||
await expect.element(links.nth(1)).toHaveAttribute('href', '/documents/d2');
|
||||
});
|
||||
|
||||
it('shows the document title in each row', async () => {
|
||||
render(DashboardRecentDocuments, {
|
||||
recentDocs: [makeDoc('d1', 'Sterbeurkunde 1930', '1930-05-12')]
|
||||
});
|
||||
await expect.element(page.getByText('Sterbeurkunde 1930')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats and displays the document date when present', async () => {
|
||||
render(DashboardRecentDocuments, { recentDocs: [makeDoc('d1', 'Dok', '1945-04-20')] });
|
||||
// The date should be visible in some formatted form
|
||||
const widget = page.getByTestId('dashboard-recent-docs');
|
||||
await expect.element(widget).toBeInTheDocument();
|
||||
// Just verify the date element exists (not exact format due to locale)
|
||||
const dateEl = page.getByTestId('doc-date-d1');
|
||||
await expect.element(dateEl).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardRecentDocuments — stats footnote', () => {
|
||||
it('renders stats footnote when stats.totalDocuments is provided', async () => {
|
||||
render(DashboardRecentDocuments, {
|
||||
recentDocs: [makeDoc('d1', 'Taufschein')],
|
||||
stats: { totalDocuments: 248, totalPersons: 34 }
|
||||
});
|
||||
const footnote = page.getByTestId('dashboard-stats-footnote');
|
||||
await expect.element(footnote).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('omits stats footnote when stats is null', async () => {
|
||||
render(DashboardRecentDocuments, {
|
||||
recentDocs: [makeDoc('d1', 'Taufschein')],
|
||||
stats: null
|
||||
});
|
||||
const footnote = page.getByTestId('dashboard-stats-footnote');
|
||||
await expect.element(footnote).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "0 Documents" when totalDocuments is 0', async () => {
|
||||
render(DashboardRecentDocuments, {
|
||||
recentDocs: [makeDoc('d1', 'Taufschein')],
|
||||
stats: { totalDocuments: 0, totalPersons: 0 }
|
||||
});
|
||||
const footnote = page.getByTestId('dashboard-stats-footnote');
|
||||
await expect.element(footnote).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardRecentDocuments — touch targets', () => {
|
||||
it('each doc row has min-h-[44px] class for WCAG touch target', async () => {
|
||||
render(DashboardRecentDocuments, { recentDocs: [makeDoc('d1', 'Taufschein')] });
|
||||
const row = page.getByTestId('doc-row-d1');
|
||||
await expect.element(row).toHaveClass('min-h-[44px]');
|
||||
});
|
||||
});
|
||||
133
frontend/src/lib/shared/dashboard/DashboardResumeStrip.svelte
Normal file
133
frontend/src/lib/shared/dashboard/DashboardResumeStrip.svelte
Normal file
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import type { DashboardResumeDTO } from '$lib/generated/api';
|
||||
|
||||
interface Props {
|
||||
resumeDoc: DashboardResumeDTO | null;
|
||||
}
|
||||
|
||||
const { resumeDoc }: Props = $props();
|
||||
|
||||
function safeColor(color: string): string {
|
||||
return /^#[0-9a-fA-F]{6}$/.test(color) ? color : '#8c9aa3';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if resumeDoc === null}
|
||||
<div
|
||||
data-testid="resume-strip-empty"
|
||||
class="rounded-sm border border-line bg-surface p-8 text-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="mx-auto text-ink-3"
|
||||
>
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
<h2 class="mt-3 font-serif text-xl text-ink">{m.dashboard_empty_title()}</h2>
|
||||
<p class="mt-1 font-sans text-sm text-ink-2">{m.dashboard_empty_body()}</p>
|
||||
<a
|
||||
href="/documents"
|
||||
class="mt-4 inline-block font-sans text-sm font-bold text-accent hover:underline"
|
||||
>
|
||||
{m.dashboard_empty_cta()}
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div data-testid="resume-strip" class="flex gap-4 rounded-sm border border-line bg-surface p-5">
|
||||
<div
|
||||
class="relative h-[252px] w-[180px] flex-shrink-0 overflow-hidden rounded-sm border border-line bg-white"
|
||||
>
|
||||
{#if resumeDoc.thumbnailUrl}
|
||||
<img
|
||||
data-testid="resume-thumbnail-img"
|
||||
src={resumeDoc.thumbnailUrl}
|
||||
alt=""
|
||||
class="h-full w-full object-cover object-top dark:mix-blend-multiply"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
data-testid="resume-thumbnail-fallback"
|
||||
class="flex h-full w-full items-center justify-center text-ink-3"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-24 w-24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z M9 12.75h6M9 15.75h6M9 18.75h3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<p class="flex items-center gap-1.5 font-sans text-xs text-ink-3">
|
||||
<span class="text-[#A6DAD8]">●</span>
|
||||
{m.dashboard_resume_label()}
|
||||
·
|
||||
{m.dashboard_blocks({ count: resumeDoc.totalBlocks })}
|
||||
</p>
|
||||
|
||||
<h2 class="font-serif text-[1.75rem] leading-tight text-ink">{resumeDoc.title}</h2>
|
||||
|
||||
<p class="font-sans text-sm text-ink-2 italic">{resumeDoc.caption}</p>
|
||||
|
||||
<blockquote
|
||||
class="border-l-[3px] border-accent pl-3 font-serif text-[1.0625rem] leading-relaxed text-ink-2"
|
||||
>
|
||||
{resumeDoc.excerpt}
|
||||
</blockquote>
|
||||
|
||||
<div class="mt-auto flex items-center gap-3 pt-2">
|
||||
<span class="font-sans text-xs font-bold text-ink">{resumeDoc.pct}%</span>
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-valuenow={resumeDoc.pct}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
class="h-1.5 flex-1 overflow-hidden rounded-full bg-line"
|
||||
>
|
||||
<div class="h-full rounded-full bg-accent" style="width:{resumeDoc.pct}%"></div>
|
||||
</div>
|
||||
{#each resumeDoc.collaborators.slice(0, 3) as collab (collab.initials + collab.color)}
|
||||
<span
|
||||
class="-ml-1 inline-flex h-6 w-6 items-center justify-center rounded-full font-sans text-[10px] font-bold text-white ring-2 ring-white"
|
||||
style="background:{safeColor(collab.color)}">{collab.initials}</span
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-4">
|
||||
<a
|
||||
href="/documents/{resumeDoc.documentId}"
|
||||
class="inline-block rounded-sm bg-accent px-4 py-1.5 font-sans text-sm font-bold text-white transition-opacity hover:opacity-90"
|
||||
>
|
||||
{m.dashboard_resume_cta()}
|
||||
</a>
|
||||
<a href="/documents" class="font-sans text-xs text-ink-3 transition-colors hover:text-ink">
|
||||
{m.dashboard_resume_other()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
|
||||
import DashboardResumeStrip from './DashboardResumeStrip.svelte';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type DashboardResumeDTO = components['schemas']['DashboardResumeDTO'];
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const mockResume: DashboardResumeDTO = {
|
||||
documentId: 'doc-123',
|
||||
title: 'Geburtsurkunde 1920',
|
||||
caption: 'Max Mustermann · 1920-01-01',
|
||||
excerpt: 'Hiermit wird beurkundet…',
|
||||
totalBlocks: 4,
|
||||
pct: 75,
|
||||
collaborators: []
|
||||
};
|
||||
|
||||
const mockResumeWithThumbnail: DashboardResumeDTO = {
|
||||
...mockResume,
|
||||
thumbnailUrl: '/api/documents/doc-123/thumbnail?v=2026-04-23T09%3A00'
|
||||
};
|
||||
|
||||
describe('DashboardResumeStrip', () => {
|
||||
it('renders empty state heading when resumeDoc is null', async () => {
|
||||
render(DashboardResumeStrip, { resumeDoc: null });
|
||||
const heading = page.getByRole('heading', { name: /Noch kein Dokument begonnen/i });
|
||||
await expect.element(heading).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders progressbar with correct aria-valuenow when resumeDoc is provided', async () => {
|
||||
render(DashboardResumeStrip, { resumeDoc: mockResume });
|
||||
const bar = page.getByRole('progressbar');
|
||||
await expect.element(bar).toBeInTheDocument();
|
||||
await expect.element(bar).toHaveAttribute('aria-valuenow', '75');
|
||||
});
|
||||
|
||||
it('shows document title when resumeDoc is provided', async () => {
|
||||
render(DashboardResumeStrip, { resumeDoc: mockResume });
|
||||
const title = page.getByRole('heading', { name: /Geburtsurkunde 1920/i });
|
||||
await expect.element(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('links to the document for the CTA', async () => {
|
||||
render(DashboardResumeStrip, { resumeDoc: mockResume });
|
||||
const link = page.getByRole('link', { name: /Weitertranskribieren/i });
|
||||
await expect.element(link).toHaveAttribute('href', '/documents/doc-123');
|
||||
});
|
||||
|
||||
it('shows block count label', async () => {
|
||||
render(DashboardResumeStrip, { resumeDoc: mockResume });
|
||||
const label = page.getByText(/4 Abschnitte/i);
|
||||
await expect.element(label).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders thumbnail img with expected attrs when thumbnailUrl is set', async () => {
|
||||
render(DashboardResumeStrip, { resumeDoc: mockResumeWithThumbnail });
|
||||
const img = page.getByTestId('resume-thumbnail-img');
|
||||
await expect.element(img).toBeInTheDocument();
|
||||
await expect
|
||||
.element(img)
|
||||
.toHaveAttribute('src', '/api/documents/doc-123/thumbnail?v=2026-04-23T09%3A00');
|
||||
await expect.element(img).toHaveAttribute('alt', '');
|
||||
await expect.element(img).toHaveAttribute('loading', 'lazy');
|
||||
await expect.element(img).toHaveAttribute('decoding', 'async');
|
||||
});
|
||||
|
||||
it('renders fallback icon when thumbnailUrl is null', async () => {
|
||||
render(DashboardResumeStrip, { resumeDoc: mockResume });
|
||||
const fallback = page.getByTestId('resume-thumbnail-fallback');
|
||||
await expect.element(fallback).toBeInTheDocument();
|
||||
await expect.element(page.getByTestId('resume-thumbnail-img')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
33
frontend/src/lib/shared/dashboard/MissionControlStrip.svelte
Normal file
33
frontend/src/lib/shared/dashboard/MissionControlStrip.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import type { components } from '$lib/generated/api';
|
||||
import SegmentationColumn from '$lib/document/SegmentationColumn.svelte';
|
||||
import TranscriptionColumn from '$lib/document/transcription/TranscriptionColumn.svelte';
|
||||
import ReadyColumn from '$lib/document/ReadyColumn.svelte';
|
||||
|
||||
type TranscriptionQueueItemDTO = components['schemas']['TranscriptionQueueItemDTO'];
|
||||
type TranscriptionWeeklyStatsDTO = components['schemas']['TranscriptionWeeklyStatsDTO'];
|
||||
|
||||
interface Props {
|
||||
segmentationDocs: TranscriptionQueueItemDTO[];
|
||||
transcriptionDocs: TranscriptionQueueItemDTO[];
|
||||
readyDocs: TranscriptionQueueItemDTO[];
|
||||
weeklyStats: TranscriptionWeeklyStatsDTO | null;
|
||||
}
|
||||
|
||||
let { segmentationDocs, transcriptionDocs, readyDocs, weeklyStats }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section class="mt-4 rounded-sm border border-line bg-surface p-6">
|
||||
<h2 class="mb-4 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.mission_control_heading()}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<SegmentationColumn docs={segmentationDocs} weeklyCount={weeklyStats?.segmentationCount ?? 0} />
|
||||
<TranscriptionColumn
|
||||
docs={transcriptionDocs}
|
||||
weeklyCount={weeklyStats?.transcriptionCount ?? 0}
|
||||
/>
|
||||
<ReadyColumn docs={readyDocs} />
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,107 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import MissionControlStrip from './MissionControlStrip.svelte';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type TranscriptionQueueItemDTO = components['schemas']['TranscriptionQueueItemDTO'];
|
||||
type TranscriptionWeeklyStatsDTO = components['schemas']['TranscriptionWeeklyStatsDTO'];
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function makeDoc(
|
||||
id: string,
|
||||
title: string,
|
||||
overrides: Partial<TranscriptionQueueItemDTO> = {}
|
||||
): TranscriptionQueueItemDTO {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
annotationCount: 0,
|
||||
textedBlockCount: 0,
|
||||
reviewedBlockCount: 0,
|
||||
contributors: [],
|
||||
hasMoreContributors: false,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
const emptyStats: TranscriptionWeeklyStatsDTO = {
|
||||
segmentationCount: 0,
|
||||
transcriptionCount: 0,
|
||||
readyCount: 0
|
||||
};
|
||||
|
||||
describe('MissionControlStrip', () => {
|
||||
it('renders section heading always', async () => {
|
||||
render(MissionControlStrip, {
|
||||
props: {
|
||||
segmentationDocs: [],
|
||||
transcriptionDocs: [],
|
||||
readyDocs: [],
|
||||
weeklyStats: null
|
||||
}
|
||||
});
|
||||
|
||||
await expect.element(page.getByText('Was braucht Aufmerksamkeit?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all three column headings', async () => {
|
||||
render(MissionControlStrip, {
|
||||
props: {
|
||||
segmentationDocs: [makeDoc('s1', 'Seg Dok')],
|
||||
transcriptionDocs: [makeDoc('t1', 'Trans Dok')],
|
||||
readyDocs: [makeDoc('r1', 'Ready Dok')],
|
||||
weeklyStats: emptyStats
|
||||
}
|
||||
});
|
||||
|
||||
await expect.element(page.getByText('Text markieren')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Text transkribieren')).toBeInTheDocument();
|
||||
await expect.element(page.getByText(/Lesefertig/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders document titles in correct columns', async () => {
|
||||
const segDoc = makeDoc('seg-1', 'Segmentierungs Brief');
|
||||
const transDoc = makeDoc('trans-1', 'Transkriptions Postkarte');
|
||||
const readyDoc = makeDoc('ready-1', 'Fertiger Tagebucheintrag');
|
||||
|
||||
render(MissionControlStrip, {
|
||||
props: {
|
||||
segmentationDocs: [segDoc],
|
||||
transcriptionDocs: [transDoc],
|
||||
readyDocs: [readyDoc],
|
||||
weeklyStats: emptyStats
|
||||
}
|
||||
});
|
||||
|
||||
await expect.element(page.getByText('Segmentierungs Brief')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Transkriptions Postkarte')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Fertiger Tagebucheintrag')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders section heading even when all arrays are empty and weeklyStats is null', async () => {
|
||||
render(MissionControlStrip, {
|
||||
props: {
|
||||
segmentationDocs: [],
|
||||
transcriptionDocs: [],
|
||||
readyDocs: [],
|
||||
weeklyStats: null
|
||||
}
|
||||
});
|
||||
|
||||
// Heading always visible
|
||||
await expect.element(page.getByText('Was braucht Aufmerksamkeit?')).toBeInTheDocument();
|
||||
|
||||
// All three empty states should also be visible
|
||||
await expect
|
||||
.element(page.getByText('Alle Dokumente haben bereits Segmentierungsblöcke.'))
|
||||
.toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByText('Keine Dokumente warten auf Transkription.'))
|
||||
.toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByText('Noch keine Dokumente vollständig transkribiert.'))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user