0 ? 'border-t border-line pt-4' : ''}>
diff --git a/frontend/src/lib/components/CommentThread.svelte.spec.ts b/frontend/src/lib/components/CommentThread.svelte.spec.ts
new file mode 100644
index 00000000..a444a7ce
--- /dev/null
+++ b/frontend/src/lib/components/CommentThread.svelte.spec.ts
@@ -0,0 +1,70 @@
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import CommentThread from './CommentThread.svelte';
+import type { Comment } from '$lib/types';
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+});
+
+function makeComment(id: string, content = 'Hello'): Comment {
+ return {
+ id,
+ authorId: 'user-1',
+ authorName: 'Alice',
+ content,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ replies: []
+ };
+}
+
+const baseProps = {
+ documentId: 'doc-1',
+ canComment: true,
+ currentUserId: 'user-1',
+ canAdmin: false
+};
+
+describe('CommentThread – empty state', () => {
+ it('shows empty state hint when there are no comments', async () => {
+ render(CommentThread, { ...baseProps, initialComments: [] });
+ await expect
+ .element(page.getByText('Noch keine Kommentare – starte die Diskussion!'))
+ .toBeInTheDocument();
+ });
+
+ it('does not show empty state hint when comments exist', async () => {
+ render(CommentThread, { ...baseProps, initialComments: [makeComment('c-1')] });
+ await expect
+ .element(page.getByText('Noch keine Kommentare – starte die Diskussion!'))
+ .not.toBeInTheDocument();
+ });
+});
+
+describe('CommentThread – onCountChange', () => {
+ it('calls onCountChange with initial SSR count on mount', async () => {
+ const onCountChange = vi.fn();
+ render(CommentThread, {
+ ...baseProps,
+ initialComments: [makeComment('c-1'), makeComment('c-2')],
+ onCountChange
+ });
+ expect(onCountChange).toHaveBeenCalledWith(2);
+ });
+
+ it('calls onCountChange with 0 when no initial comments', async () => {
+ const onCountChange = vi.fn();
+ render(CommentThread, { ...baseProps, initialComments: [], onCountChange });
+ expect(onCountChange).toHaveBeenCalledWith(0);
+ });
+
+ it('counts replies in the total', async () => {
+ const onCountChange = vi.fn();
+ const comment = { ...makeComment('c-1'), replies: [makeComment('r-1') as never] };
+ render(CommentThread, { ...baseProps, initialComments: [comment], onCountChange });
+ expect(onCountChange).toHaveBeenCalledWith(2);
+ });
+});
diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte
index 94acb09d..a2aba5b8 100644
--- a/frontend/src/lib/components/DocumentBottomPanel.svelte
+++ b/frontend/src/lib/components/DocumentBottomPanel.svelte
@@ -98,6 +98,12 @@ const tabs: { id: DocumentPanelTab; label: () => string }[] = [
];
const panelHeight = $derived(open ? height : MIN_HEIGHT);
+
+let discussionCount = $state((() => comments.reduce((s, c) => s + 1 + c.replies.length, 0))());
+
+function handleCountChange(count: number) {
+ discussionCount = count;
+}
{tab.label()}
+ {#if tab.id === 'discussion'}
+
{discussionCount}
+ {/if}
{/each}
@@ -165,6 +178,7 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT);
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
+ onCountChange={handleCountChange}
/>
{:else if activeTab === 'history'}
diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts b/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts
new file mode 100644
index 00000000..975d36e6
--- /dev/null
+++ b/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts
@@ -0,0 +1,47 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import DocumentBottomPanel from './DocumentBottomPanel.svelte';
+import type { Comment } from '$lib/types';
+
+afterEach(cleanup);
+
+function makeComment(id: string): Comment {
+ return {
+ id,
+ authorId: 'user-1',
+ authorName: 'Alice',
+ content: 'Hello',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ replies: []
+ };
+}
+
+const doc = { id: 'doc-1', title: 'Test' };
+
+const baseProps = {
+ doc,
+ canComment: true,
+ currentUserId: 'user-1',
+ canAdmin: false,
+ height: 300,
+ activeTab: 'discussion' as const
+};
+
+describe('DocumentBottomPanel – discussion badge', () => {
+ it('always shows a badge on the Discussion tab', async () => {
+ render(DocumentBottomPanel, { ...baseProps, comments: [], open: true });
+ await expect.element(page.getByTestId('discussion-count-badge')).toBeInTheDocument();
+ await expect.element(page.getByTestId('discussion-count-badge')).toHaveTextContent('0');
+ });
+
+ it('shows the correct count when comments exist', async () => {
+ render(DocumentBottomPanel, {
+ ...baseProps,
+ comments: [makeComment('c-1'), makeComment('c-2')],
+ open: true
+ });
+ await expect.element(page.getByTestId('discussion-count-badge')).toHaveTextContent('2');
+ });
+});
diff --git a/frontend/src/lib/components/PanelDiscussion.svelte b/frontend/src/lib/components/PanelDiscussion.svelte
index bbed758a..291cf5c1 100644
--- a/frontend/src/lib/components/PanelDiscussion.svelte
+++ b/frontend/src/lib/components/PanelDiscussion.svelte
@@ -8,9 +8,11 @@ type Props = {
canComment: boolean;
currentUserId: string | null;
canAdmin: boolean;
+ onCountChange?: (count: number) => void;
};
-let { documentId, initialComments, canComment, currentUserId, canAdmin }: Props = $props();
+let { documentId, initialComments, canComment, currentUserId, canAdmin, onCountChange }: Props =
+ $props();
@@ -20,5 +22,6 @@ let { documentId, initialComments, canComment, currentUserId, canAdmin }: Props
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
+ onCountChange={onCountChange}
/>
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte
index 57f40f8e..7fff1e51 100644
--- a/frontend/src/routes/+page.svelte
+++ b/frontend/src/routes/+page.svelte
@@ -90,7 +90,7 @@ $effect(() => {
{#if data.incompleteCount > 0}
![]()
{
class="h-6 w-6 opacity-60"
/>
-
+
{m.enrich_needs_metadata_title()}
-
+
{m.enrich_needs_metadata_count({ count: data.incompleteCount })}
{m.enrich_needs_metadata_cta()} →