mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
import { Profiler } from 'react';
|
|
import { act, render, screen } 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 { useUIStore } from '@/stores/ui-store';
|
|
import { groupEmailsByThread } from '@/lib/thread-utils';
|
|
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
|
import { SEARCH_SCOPE_ALL_FOLDERS } from '@/lib/search-scope-folders';
|
|
import type { Email } from '@/lib/jmap/types';
|
|
|
|
vi.mock('@/hooks/use-email-drag', () => ({
|
|
useEmailDrag: () => ({ dragHandlers: {}, isDragging: false }),
|
|
}));
|
|
|
|
vi.mock('@/stores/auth-store', () => {
|
|
const state = { identities: [], username: null };
|
|
return {
|
|
useAuthStore: (selector?: (s: typeof state) => unknown) => (selector ? selector(state) : state),
|
|
};
|
|
});
|
|
|
|
const accountState = { getAccountById: () => undefined };
|
|
vi.mock('@/stores/account-store', () => ({
|
|
useAccountStore: Object.assign(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(selector: any) => (selector ? selector(accountState) : accountState),
|
|
{ getState: () => accountState },
|
|
),
|
|
}));
|
|
|
|
const makeEmail = (overrides: Partial<Email> = {}): Email => ({
|
|
id: 'email-1',
|
|
threadId: 'thread-1',
|
|
mailboxIds: { archive: true },
|
|
keywords: { $seen: true },
|
|
size: 1000,
|
|
receivedAt: '2024-01-15T10:00:00Z',
|
|
from: [{ name: 'Alice', email: 'alice@example.com' }],
|
|
subject: 'Invoice',
|
|
hasAttachment: false,
|
|
// Stamped by the all-folders search fan-out (searchAcrossAccounts).
|
|
sourceFolder: 'Archive',
|
|
...overrides,
|
|
});
|
|
|
|
function renderThread(emails: Email[]) {
|
|
const [thread] = groupEmailsByThread(emails);
|
|
return render(
|
|
<ThreadListItem
|
|
thread={thread}
|
|
isExpanded={false}
|
|
onToggleExpand={() => {}}
|
|
onEmailSelect={() => {}}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
const noop = () => {};
|
|
const single = () => [makeEmail()];
|
|
const conversation = () => [
|
|
makeEmail({ id: 'email-1', receivedAt: '2024-01-15T10:00:00Z' }),
|
|
makeEmail({ id: 'email-2', receivedAt: '2024-01-16T10:00:00Z' }),
|
|
];
|
|
|
|
describe('ThreadListItem folder chip in search results (#1172)', () => {
|
|
beforeEach(() => {
|
|
useUIStore.setState({ isMobile: false, isTablet: false, isDesktop: true });
|
|
useSettingsStore.setState({
|
|
emailKeywords: [...DEFAULT_KEYWORDS],
|
|
showPreview: false,
|
|
mailLayout: 'split',
|
|
interfaceLayout: 'default',
|
|
});
|
|
useEmailStore.setState({
|
|
selectedEmailKeys: new Set<string>(),
|
|
selectedMailbox: 'inbox',
|
|
isUnifiedView: false,
|
|
selectedKeyword: null,
|
|
searchQuery: '',
|
|
searchFilters: { ...DEFAULT_SEARCH_FILTERS },
|
|
searchMailboxId: '',
|
|
});
|
|
});
|
|
|
|
describe.each([
|
|
['single message', single],
|
|
['conversation', conversation],
|
|
])('%s row', (_label, emails) => {
|
|
it('shows the folder when the search covers all folders except Spam and Trash', () => {
|
|
useEmailStore.setState({ searchQuery: 'invoice', searchMailboxId: '' });
|
|
renderThread(emails());
|
|
expect(screen.getByTitle('Archive')).toBeTruthy();
|
|
});
|
|
|
|
it('shows the folder when the search covers all folders', () => {
|
|
useEmailStore.setState({ searchQuery: 'invoice', searchMailboxId: SEARCH_SCOPE_ALL_FOLDERS });
|
|
renderThread(emails());
|
|
expect(screen.getByTitle('Archive')).toBeTruthy();
|
|
});
|
|
|
|
it('shows the folder when only advanced filters drive the search', () => {
|
|
useEmailStore.setState({
|
|
searchFilters: { ...DEFAULT_SEARCH_FILTERS, from: 'alice' },
|
|
searchMailboxId: '',
|
|
});
|
|
renderThread(emails());
|
|
expect(screen.getByTitle('Archive')).toBeTruthy();
|
|
});
|
|
|
|
it('hides the folder when the search is scoped to one folder', () => {
|
|
useEmailStore.setState({ searchQuery: 'invoice', searchMailboxId: 'archive' });
|
|
renderThread(emails());
|
|
expect(screen.queryByTitle('Archive')).toBeNull();
|
|
});
|
|
|
|
it('hides the folder outside a search', () => {
|
|
renderThread(emails());
|
|
expect(screen.queryByTitle('Archive')).toBeNull();
|
|
});
|
|
|
|
it('still shows the folder in the unified view', () => {
|
|
useEmailStore.setState({ isUnifiedView: true });
|
|
renderThread(emails());
|
|
expect(screen.getByTitle('Archive')).toBeTruthy();
|
|
});
|
|
|
|
it('does not re-render a row without a folder when a search starts', () => {
|
|
// The folder's own list stays on screen until the results arrive; its
|
|
// rows carry no sourceFolder and have no chip to show.
|
|
const [thread] = groupEmailsByThread(emails().map((e) => ({ ...e, sourceFolder: undefined })));
|
|
const onRender = vi.fn();
|
|
render(
|
|
<Profiler id="row" onRender={onRender}>
|
|
<ThreadListItem thread={thread} isExpanded={false} onToggleExpand={noop} onEmailSelect={noop} />
|
|
</Profiler>,
|
|
);
|
|
onRender.mockClear();
|
|
act(() => useEmailStore.setState({ searchQuery: 'invoice', searchMailboxId: '' }));
|
|
expect(onRender).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|