Files
SRCmail/components/email/__tests__/thread-list-item.test.tsx
T
Mathy Vanvoorden 013ef7d557 fix: remove unused code
This code is nowhere used, so to prevent extra work during an upcoming refactor
of tags, it is removed and some related tests are now actually made useful
2026-07-29 17:18:42 +02:00

185 lines
5.9 KiB
TypeScript

import { render, screen, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ThreadListItem } from '../thread-list-item';
import { useSettingsStore, DEFAULT_KEYWORDS } from '@/stores/settings-store';
import { useEmailStore } from '@/stores/email-store';
import { groupEmailsByThread } from '@/lib/thread-utils';
import type { Email } from '@/lib/jmap/types';
vi.mock('@/hooks/use-email-drag', () => ({
useEmailDrag: () => ({ dragHandlers: {}, isDragging: false }),
}));
vi.mock('@/stores/auth-store', () => ({
useAuthStore: () => ({ identities: [] }),
}));
const makeEmail = (overrides: Partial<Email> = {}): Email => ({
id: 'email-1',
threadId: 'thread-1',
mailboxIds: { inbox: true },
keywords: { $seen: true },
size: 1000,
receivedAt: '2024-01-15T10:00:00Z',
from: [{ name: 'Alice', email: 'alice@example.com' }],
subject: 'Test Subject',
hasAttachment: false,
...overrides,
});
/**
* A one-message thread, built through the real grouping so the fixture cannot
* drift from what the list actually feeds this component. `ThreadListItem`
* delegates to `SingleEmailItem` at that size, which is what draws every
* single-message row in the app.
*/
function renderRow(email: Email) {
const [thread] = groupEmailsByThread([email]);
return render(
<ThreadListItem
thread={thread}
isExpanded={false}
onToggleExpand={() => {}}
onEmailSelect={() => {}}
/>,
);
}
describe('ThreadListItem tag badge', () => {
beforeEach(() => {
useSettingsStore.setState({
emailKeywords: [...DEFAULT_KEYWORDS],
showPreview: false,
mailLayout: 'split',
});
useEmailStore.setState({
selectedEmailIds: new Set<string>(),
selectedMailbox: 'inbox',
});
});
it('does not show a tag badge when the email has no label keyword', () => {
renderRow(makeEmail({ keywords: { $seen: true } }));
expect(screen.getByText('Test Subject')).toBeInTheDocument();
DEFAULT_KEYWORDS.forEach((kw) => {
expect(screen.queryByText(kw.label)).not.toBeInTheDocument();
});
});
it('shows a tag badge for a $label: keyword', () => {
renderRow(makeEmail({ keywords: { $seen: true, '$label:red': true } }));
expect(screen.getByText('Red')).toBeInTheDocument();
});
it('shows a tag badge for the legacy $color: keyword', () => {
renderRow(makeEmail({ keywords: { $seen: true, '$color:blue': true } }));
expect(screen.getByText('Blue')).toBeInTheDocument();
});
it('falls back to the raw id when the tag is not in settings', () => {
// A keyword created by another client, or one whose definition was deleted.
renderRow(makeEmail({ keywords: { $seen: true, '$label:unknown-tag': true } }));
expect(screen.getByText('unknown-tag')).toBeInTheDocument();
});
it('shows a custom tag label', () => {
useSettingsStore.setState({
emailKeywords: [...DEFAULT_KEYWORDS, { id: 'work', label: 'Work', color: 'teal' }],
});
renderRow(makeEmail({ keywords: { $seen: true, '$label:work': true } }));
expect(screen.getByText('Work')).toBeInTheDocument();
});
it('follows a renamed tag definition', () => {
const email = makeEmail({ keywords: { $seen: true, '$label:red': true } });
const { rerender } = renderRow(email);
expect(screen.getByText('Red')).toBeInTheDocument();
act(() => {
useSettingsStore.getState().updateKeyword('red', { label: 'Urgent' });
});
const [thread] = groupEmailsByThread([email]);
rerender(
<ThreadListItem
thread={thread}
isExpanded={false}
onToggleExpand={() => {}}
onEmailSelect={() => {}}
/>,
);
expect(screen.getByText('Urgent')).toBeInTheDocument();
expect(screen.queryByText('Red')).not.toBeInTheDocument();
});
});
describe('ThreadListItem row content', () => {
beforeEach(() => {
useSettingsStore.setState({
emailKeywords: [...DEFAULT_KEYWORDS],
showPreview: false,
mailLayout: 'split',
});
useEmailStore.setState({
selectedEmailIds: new Set<string>(),
selectedMailbox: 'inbox',
});
});
it('renders the subject without a tag', () => {
renderRow(makeEmail({ subject: 'Hello World' }));
expect(screen.getByText('Hello World')).toBeInTheDocument();
});
it('renders preview text inline in the focused layout', () => {
useSettingsStore.setState({ showPreview: true, mailLayout: 'focus' });
const { container } = renderRow(makeEmail({ preview: 'Inline preview content' }));
expect(screen.getByText('Test Subject')).toBeInTheDocument();
expect(screen.getByText(/Inline preview content/)).toBeInTheDocument();
// Focused rows are one line: the preview shares the subject's element
// rather than getting a paragraph of its own.
expect(container.querySelector('p')).toBeNull();
});
});
describe('ThreadListItem shift-range checkbox', () => {
beforeEach(() => {
useSettingsStore.setState({
emailKeywords: [...DEFAULT_KEYWORDS],
showPreview: false,
mailLayout: 'split',
});
});
it('shift-clicking the checkbox extends the selection from the anchor', () => {
const e1 = makeEmail({ id: 'e1', threadId: 't1' });
const e2 = makeEmail({ id: 'e2', threadId: 't2' });
const e3 = makeEmail({ id: 'e3', threadId: 't3' });
// Selection mode active so the checkbox renders, with the anchor on e1.
useEmailStore.setState({
emails: [e1, e2, e3],
selectedEmailIds: new Set(['e1']),
lastSelectedEmailId: 'e1',
selectedMailbox: 'inbox',
});
renderRow(e3);
const checkbox = screen.getAllByRole('button')[0];
act(() => {
checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true, shiftKey: true }));
});
const selected = useEmailStore.getState().selectedEmailIds;
expect(selected.has('e1')).toBe(true);
expect(selected.has('e2')).toBe(true); // the row in between got filled in
expect(selected.has('e3')).toBe(true);
});
});