Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 3m51s
CI / OCR Service Tests (pull_request) Successful in 47s
CI / Backend Unit Tests (pull_request) Failing after 3m31s
- CommentData.java: add @Nullable on annotationId to match codebase convention - DashboardService: isEmpty() → isBlank() for commentPreview null-guard - ChronikRow.svelte: always set aria-label on comment rows (not only when preview present) - ChronikRow.svelte.spec.ts: add test for aria-label on comment row without preview Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
263 lines
9.1 KiB
TypeScript
263 lines
9.1 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page } from 'vitest/browser';
|
|
|
|
import ChronikRow from './ChronikRow.svelte';
|
|
import type { components } from '$lib/generated/api';
|
|
|
|
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
|
|
|
|
afterEach(cleanup);
|
|
|
|
const baseItem: ActivityFeedItemDTO = {
|
|
kind: 'TEXT_SAVED',
|
|
actor: { initials: 'MR', color: '#7a4f9a', name: 'Max Raddatz' },
|
|
documentId: 'doc-1',
|
|
documentTitle: 'Brief 1920',
|
|
happenedAt: '2026-04-19T10:00:00Z',
|
|
youMentioned: false,
|
|
count: 1
|
|
};
|
|
|
|
describe('ChronikRow', () => {
|
|
it('renders the document title', async () => {
|
|
render(ChronikRow, { item: baseItem });
|
|
await expect.element(page.getByText('Brief 1920')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders actor initials in avatar', async () => {
|
|
render(ChronikRow, { item: baseItem });
|
|
await expect.element(page.getByText('MR')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders "?" fallback avatar when actor is missing', async () => {
|
|
const item: ActivityFeedItemDTO = { ...baseItem, actor: undefined };
|
|
render(ChronikRow, { item });
|
|
const fallback = document.querySelector('[data-testid="chronik-avatar-fallback"]');
|
|
expect(fallback).not.toBeNull();
|
|
expect(fallback?.textContent?.trim()).toBe('?');
|
|
});
|
|
|
|
it('wraps the row in a link to the document', async () => {
|
|
render(ChronikRow, { item: baseItem });
|
|
const link = document.querySelector('a[href="/documents/doc-1"]');
|
|
expect(link).not.toBeNull();
|
|
});
|
|
|
|
// --- simple variant ---
|
|
it('renders simple variant when count === 1 and not a mention', async () => {
|
|
render(ChronikRow, { item: baseItem });
|
|
// No rollup count badge
|
|
expect(document.querySelector('[data-testid="chronik-count-badge"]')).toBeNull();
|
|
// No for-you marker
|
|
expect(document.querySelector('[data-testid="chronik-foryou-marker"]')).toBeNull();
|
|
// No comment preview
|
|
expect(document.querySelector('[data-testid="chronik-comment-preview"]')).toBeNull();
|
|
});
|
|
|
|
// --- rollup variant ---
|
|
it('renders rollup variant with count badge when count > 1', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'TEXT_SAVED',
|
|
count: 3,
|
|
happenedAt: '2026-04-19T10:00:00Z',
|
|
happenedAtUntil: '2026-04-19T11:30:00Z'
|
|
};
|
|
render(ChronikRow, { item });
|
|
const badge = document.querySelector('[data-testid="chronik-count-badge"]');
|
|
expect(badge).not.toBeNull();
|
|
expect(badge?.textContent).toContain('3');
|
|
});
|
|
|
|
it('renders a time range with an en-dash for rollup variant', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'FILE_UPLOADED',
|
|
count: 5,
|
|
happenedAt: '2026-04-19T10:00:00Z',
|
|
happenedAtUntil: '2026-04-19T11:30:00Z'
|
|
};
|
|
render(ChronikRow, { item });
|
|
// en-dash character U+2013
|
|
const body = document.body.textContent ?? '';
|
|
expect(body).toContain('\u2013');
|
|
});
|
|
|
|
// --- for-you variant ---
|
|
it('renders for-you marker when youMentioned is true', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'MENTION_CREATED',
|
|
youMentioned: true
|
|
};
|
|
render(ChronikRow, { item });
|
|
const marker = document.querySelector('[data-testid="chronik-foryou-marker"]');
|
|
expect(marker).not.toBeNull();
|
|
});
|
|
|
|
it('applies accent border to for-you variant outer wrapper', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'MENTION_CREATED',
|
|
youMentioned: true
|
|
};
|
|
render(ChronikRow, { item });
|
|
const wrapper = document.querySelector('[data-variant="for-you"]');
|
|
expect(wrapper).not.toBeNull();
|
|
expect(wrapper?.className).toContain('border-accent');
|
|
});
|
|
|
|
// --- comment variant ---
|
|
it('renders comment preview for COMMENT_ADDED kind', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED'
|
|
};
|
|
render(ChronikRow, { item });
|
|
const preview = document.querySelector('[data-testid="chronik-comment-preview"]');
|
|
expect(preview).not.toBeNull();
|
|
});
|
|
|
|
it('comment preview does NOT duplicate the document title verbatim', async () => {
|
|
// Leonie: user sees the title twice otherwise — looks like the comment is quoting itself.
|
|
// Until the backend exposes item.commentPreview, the placeholder must be distinct.
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED',
|
|
documentTitle: 'Brief vom 12. Juli 1920'
|
|
};
|
|
render(ChronikRow, { item });
|
|
const preview = document.querySelector('[data-testid="chronik-comment-preview"]');
|
|
expect(preview).not.toBeNull();
|
|
expect(preview?.textContent).not.toContain('Brief vom 12. Juli 1920');
|
|
});
|
|
|
|
// --- deep-link href for comment events ---
|
|
it('links to /documents/:id?commentId=…&annotationId=… for COMMENT_ADDED', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED',
|
|
commentId: 'comment-7',
|
|
annotationId: 'annot-9'
|
|
};
|
|
render(ChronikRow, { item });
|
|
const link = document.querySelector(
|
|
'a[href="/documents/doc-1?commentId=comment-7&annotationId=annot-9"]'
|
|
);
|
|
expect(link).not.toBeNull();
|
|
});
|
|
|
|
it('links to /documents/:id?commentId=…&annotationId=… for MENTION_CREATED', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'MENTION_CREATED',
|
|
youMentioned: true,
|
|
commentId: 'comment-8',
|
|
annotationId: 'annot-11'
|
|
};
|
|
render(ChronikRow, { item });
|
|
const link = document.querySelector(
|
|
'a[href="/documents/doc-1?commentId=comment-8&annotationId=annot-11"]'
|
|
);
|
|
expect(link).not.toBeNull();
|
|
});
|
|
|
|
it('falls back to bare document href when commentId is absent on a comment row', async () => {
|
|
// Back-compat for old/missing backend payloads. Still navigates sensibly.
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED'
|
|
};
|
|
render(ChronikRow, { item });
|
|
const link = document.querySelector('a[href="/documents/doc-1"]');
|
|
expect(link).not.toBeNull();
|
|
});
|
|
|
|
it('links to commentId-only URL when commentId is set but annotationId is absent', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED',
|
|
commentId: 'comment-7'
|
|
// annotationId absent — comment on a non-annotation block
|
|
};
|
|
render(ChronikRow, { item });
|
|
const link = document.querySelector('a[href="/documents/doc-1?commentId=comment-7"]');
|
|
expect(link).not.toBeNull();
|
|
});
|
|
|
|
// --- commentPreview content ---
|
|
it('renders commentPreview text when variant is comment and commentPreview is present', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED',
|
|
commentPreview: 'Hello family, great letter!'
|
|
};
|
|
render(ChronikRow, { item });
|
|
const preview = document.querySelector('[data-testid="chronik-comment-preview"]');
|
|
expect(preview).not.toBeNull();
|
|
expect(preview?.textContent).toContain('Hello family, great letter!');
|
|
});
|
|
|
|
it('renders placeholder ellipsis when variant is comment and commentPreview is null', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED',
|
|
commentPreview: undefined
|
|
};
|
|
render(ChronikRow, { item });
|
|
const preview = document.querySelector('[data-testid="chronik-comment-preview"]');
|
|
expect(preview).not.toBeNull();
|
|
expect(preview?.textContent?.trim()).toBe('„…"');
|
|
});
|
|
|
|
it('does not render preview paragraph for non-comment variants', async () => {
|
|
const item: ActivityFeedItemDTO = { ...baseItem, kind: 'TEXT_SAVED' };
|
|
render(ChronikRow, { item });
|
|
expect(document.querySelector('[data-testid="chronik-comment-preview"]')).toBeNull();
|
|
});
|
|
|
|
it('link has aria-label containing preview text for comment variant with preview', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED',
|
|
commentPreview: 'A wonderful letter from grandma'
|
|
};
|
|
render(ChronikRow, { item });
|
|
const link = document.querySelector('a[aria-label]');
|
|
expect(link).not.toBeNull();
|
|
expect(link?.getAttribute('aria-label')).toContain('A wonderful letter from grandma');
|
|
});
|
|
|
|
it('link still has aria-label for comment variant when commentPreview is absent', async () => {
|
|
const item: ActivityFeedItemDTO = {
|
|
...baseItem,
|
|
kind: 'COMMENT_ADDED',
|
|
commentPreview: undefined
|
|
};
|
|
render(ChronikRow, { item });
|
|
const link = document.querySelector('a[aria-label]');
|
|
expect(link).not.toBeNull();
|
|
expect(link?.getAttribute('aria-label')).not.toBeNull();
|
|
});
|
|
|
|
// --- robustness: title rendering for edge cases ---
|
|
it('still renders the row link when documentTitle is an empty string', async () => {
|
|
// Felix: verbText.indexOf(docTitle) returned 0 for empty titles — the span
|
|
// collapsed and before/after both emptied out. Swap to a sentinel-based
|
|
// approach so this case renders like every other row.
|
|
const empty: ActivityFeedItemDTO = { ...baseItem, documentTitle: '' };
|
|
render(ChronikRow, { item: empty });
|
|
const link = document.querySelector('a[href="/documents/doc-1"]');
|
|
expect(link).not.toBeNull();
|
|
});
|
|
|
|
it('renders a short document title that could substring-match the verb', async () => {
|
|
const short: ActivityFeedItemDTO = { ...baseItem, documentTitle: 'Brief' };
|
|
render(ChronikRow, { item: short });
|
|
const titleEls = document.querySelectorAll('[data-testid="chronik-doc-title"]');
|
|
expect(titleEls.length).toBe(1);
|
|
expect(titleEls[0].textContent).toBe('Brief');
|
|
});
|
|
});
|