Notification deep-link scroll targets #comment-{id}. Add the id to
the article wrapper along with tabindex="-1" so scrollIntoView +
.focus({preventScroll:true}) can land screen-reader and keyboard
focus on the specific comment. A focus-visible ring appears only
for keyboard users so mouse clicks don't trigger a visible outline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
3.5 KiB
TypeScript
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/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/);
|
|
});
|
|
});
|