Files
familienarchiv/frontend/src/lib/shared/discussion/CommentMessage.svelte.spec.ts
Marcel 567612761d refactor: move lib-root files to lib/shared/ and finalize domain structure
- Move api.server.ts, errors.ts, types.ts, utils.ts, relativeTime.ts to lib/shared/
- Move person relationship components to lib/person/relationship/
- Move Stammbaum components to lib/person/genealogy/
- Move HelpPopover to lib/shared/primitives/
- Update all import paths across routes, specs, and lib files
- Update vi.mock() paths in server-project test files
- Remove now-empty legacy directories (components/, hooks/, server/, etc.)
- Update vite.config.ts coverage include paths for new structure
- Update frontend/CLAUDE.md to reflect domain-based lib/ layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:53:31 +02:00

107 lines
3.5 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page, userEvent } from 'vitest/browser';
import CommentMessage from './CommentMessage.svelte';
import type { FlatMessage } from '$lib/shared/types';
afterEach(cleanup);
const baseMsg: FlatMessage = {
id: 'msg-1',
authorId: 'user-1',
authorName: 'Anna Müller',
content: 'Hello world',
createdAt: new Date(Date.now() - 5 * 60_000).toISOString(),
updatedAt: new Date(Date.now() - 5 * 60_000).toISOString()
};
function defaultProps(overrides: Partial<Parameters<typeof render>[1]> = {}) {
return {
message: baseMsg,
isOwn: false,
isEditing: false,
editText: '',
onEdit: vi.fn(),
onDelete: vi.fn(),
onEditTextChange: vi.fn(),
onEditKeydown: vi.fn(),
...overrides
};
}
describe('CommentMessage', () => {
it('renders author name', async () => {
render(CommentMessage, defaultProps());
await expect.element(page.getByText('Anna Müller')).toBeInTheDocument();
});
it('renders initials in avatar', async () => {
render(CommentMessage, defaultProps());
await expect.element(page.getByText('AM')).toBeInTheDocument();
});
it('renders message body', async () => {
render(CommentMessage, defaultProps());
await expect.element(page.getByText('Hello world')).toBeInTheDocument();
});
it('renders quoted section when content contains a quote', async () => {
render(
CommentMessage,
defaultProps({
message: { ...baseMsg, content: '> "Interesting passage"\n\nMy reply' }
})
);
await expect.element(page.getByText(/Interesting passage/)).toBeInTheDocument();
await expect.element(page.getByText('My reply')).toBeInTheDocument();
});
it('does not show delete button for messages not owned by current user', async () => {
render(CommentMessage, defaultProps({ isOwn: false }));
await expect.element(page.getByRole('button')).not.toBeInTheDocument();
});
it('shows delete button for own messages', async () => {
render(CommentMessage, defaultProps({ isOwn: true }));
await expect.element(page.getByRole('button')).toBeInTheDocument();
});
it('calls onDelete when delete button is clicked', async () => {
const onDelete = vi.fn();
render(CommentMessage, defaultProps({ isOwn: true, onDelete }));
await userEvent.click(page.getByRole('button'));
expect(onDelete).toHaveBeenCalled();
});
it('shows edit textarea when isEditing is true', async () => {
render(
CommentMessage,
defaultProps({ isOwn: true, isEditing: true, editText: 'current edit text' })
);
const textarea = page.getByRole('textbox');
await expect.element(textarea).toBeInTheDocument();
await expect.element(textarea).toHaveValue('current edit text');
});
it('exposes id="comment-{message.id}" on the article wrapper for deep-link scroll', async () => {
render(CommentMessage, defaultProps());
const article = page.getByRole('article').element();
expect(article.getAttribute('id')).toBe('comment-msg-1');
});
it('is focusable but not in tab order (tabindex="-1")', async () => {
render(CommentMessage, defaultProps());
const article = page.getByRole('article').element();
expect(article.getAttribute('tabindex')).toBe('-1');
});
it('shows a focus-visible ring when focused via keyboard', async () => {
render(CommentMessage, defaultProps());
const article = page.getByRole('article').element();
const classes = article.className;
expect(classes).toMatch(/focus-visible:ring-2/);
expect(classes).toMatch(/focus-visible:ring-brand-navy/);
expect(classes).toMatch(/outline-none/);
});
});