145 lines
5.3 KiB
TypeScript
145 lines
5.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { useEmailStore } from '../email-store';
|
|
import type { Mailbox } from '@/lib/jmap/types';
|
|
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
|
|
|
function makeMailbox(overrides: Partial<Mailbox> = {}): Mailbox {
|
|
return {
|
|
id: overrides.id ?? 'inbox',
|
|
name: overrides.name ?? 'Inbox',
|
|
sortOrder: 0,
|
|
totalEmails: 0,
|
|
unreadEmails: 0,
|
|
totalThreads: 0,
|
|
unreadThreads: 0,
|
|
myRights: {
|
|
mayReadItems: true,
|
|
mayAddItems: true,
|
|
mayRemoveItems: true,
|
|
maySetSeen: true,
|
|
maySetKeywords: true,
|
|
mayCreateChild: true,
|
|
mayRename: true,
|
|
mayDelete: true,
|
|
maySubmit: true,
|
|
},
|
|
isSubscribed: true,
|
|
isShared: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('useEmailStore multi-account state', () => {
|
|
beforeEach(() => {
|
|
useEmailStore.setState({
|
|
accountMailboxes: {},
|
|
viewingAccountId: null,
|
|
selectedMailbox: '',
|
|
selectedEmail: null,
|
|
selectedEmailIds: new Set(),
|
|
selectedKeyword: null,
|
|
expandedThreadIds: new Set(),
|
|
threadEmailsCache: new Map(),
|
|
isLoadingThread: null,
|
|
});
|
|
});
|
|
|
|
it('caches mailboxes per account via setAccountMailboxes', () => {
|
|
const accountA = [makeMailbox({ id: 'a-inbox', name: 'A Inbox' })];
|
|
const accountB = [makeMailbox({ id: 'b-inbox', name: 'B Inbox' })];
|
|
|
|
useEmailStore.getState().setAccountMailboxes('account-a', accountA);
|
|
useEmailStore.getState().setAccountMailboxes('account-b', accountB);
|
|
|
|
expect(useEmailStore.getState().accountMailboxes).toEqual({
|
|
'account-a': accountA,
|
|
'account-b': accountB,
|
|
});
|
|
});
|
|
|
|
it('replaces the cached entry when setAccountMailboxes is called again', () => {
|
|
const initial = [makeMailbox({ id: 'a-inbox' })];
|
|
const updated = [makeMailbox({ id: 'a-inbox' }), makeMailbox({ id: 'a-sent', name: 'Sent' })];
|
|
|
|
useEmailStore.getState().setAccountMailboxes('account-a', initial);
|
|
useEmailStore.getState().setAccountMailboxes('account-a', updated);
|
|
|
|
expect(useEmailStore.getState().accountMailboxes['account-a']).toEqual(updated);
|
|
});
|
|
|
|
it('clearAccountMailboxes wipes the entire cache', () => {
|
|
useEmailStore.getState().setAccountMailboxes('account-a', [makeMailbox()]);
|
|
useEmailStore.getState().setAccountMailboxes('account-b', [makeMailbox()]);
|
|
|
|
useEmailStore.getState().clearAccountMailboxes();
|
|
|
|
expect(useEmailStore.getState().accountMailboxes).toEqual({});
|
|
});
|
|
|
|
it('setViewingAccount updates viewingAccountId without touching the mailbox cache', () => {
|
|
useEmailStore.getState().setAccountMailboxes('account-a', [makeMailbox()]);
|
|
useEmailStore.getState().setViewingAccount('account-a');
|
|
expect(useEmailStore.getState().viewingAccountId).toBe('account-a');
|
|
expect(useEmailStore.getState().accountMailboxes['account-a']).toBeDefined();
|
|
|
|
useEmailStore.getState().setViewingAccount(null);
|
|
expect(useEmailStore.getState().viewingAccountId).toBeNull();
|
|
});
|
|
|
|
it('selectAccountMailbox sets viewing and selected together, and clears email selection state', () => {
|
|
useEmailStore.setState({
|
|
selectedEmail: { id: 'e1' } as unknown as ReturnType<typeof useEmailStore.getState>['selectedEmail'],
|
|
selectedEmailIds: new Set(['e1', 'e2']),
|
|
selectedKeyword: 'work',
|
|
expandedThreadIds: new Set(['thread-1']),
|
|
});
|
|
|
|
useEmailStore.getState().selectAccountMailbox('account-b', 'b-inbox');
|
|
|
|
const state = useEmailStore.getState();
|
|
expect(state.viewingAccountId).toBe('account-b');
|
|
expect(state.selectedMailbox).toBe('b-inbox');
|
|
expect(state.selectedEmail).toBeNull();
|
|
expect(state.selectedEmailIds.size).toBe(0);
|
|
expect(state.selectedKeyword).toBeNull();
|
|
expect(state.expandedThreadIds.size).toBe(0);
|
|
});
|
|
|
|
it('selectAccountMailbox with null accountId switches back to the active account', () => {
|
|
useEmailStore.getState().selectAccountMailbox('account-b', 'b-inbox');
|
|
expect(useEmailStore.getState().viewingAccountId).toBe('account-b');
|
|
|
|
useEmailStore.getState().selectAccountMailbox(null, 'a-inbox');
|
|
expect(useEmailStore.getState().viewingAccountId).toBeNull();
|
|
expect(useEmailStore.getState().selectedMailbox).toBe('a-inbox');
|
|
});
|
|
|
|
it('fetchAccountMailboxes caches the result keyed by accountId', async () => {
|
|
const mailboxes = [makeMailbox({ id: 'a-inbox' }), makeMailbox({ id: 'a-sent', name: 'Sent' })];
|
|
const client = {
|
|
getMailboxes: vi.fn().mockResolvedValue(mailboxes),
|
|
} as unknown as IJMAPClient;
|
|
|
|
await useEmailStore.getState().fetchAccountMailboxes(client, 'account-a');
|
|
|
|
expect(client.getMailboxes).toHaveBeenCalledTimes(1);
|
|
expect(useEmailStore.getState().accountMailboxes['account-a']).toEqual(mailboxes);
|
|
});
|
|
|
|
it('fetchAccountMailboxes leaves the cache untouched when the client throws', async () => {
|
|
useEmailStore.getState().setAccountMailboxes('account-a', [makeMailbox({ id: 'a-inbox' })]);
|
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const client = {
|
|
getMailboxes: vi.fn().mockRejectedValue(new Error('boom')),
|
|
} as unknown as IJMAPClient;
|
|
|
|
await useEmailStore.getState().fetchAccountMailboxes(client, 'account-a');
|
|
|
|
expect(useEmailStore.getState().accountMailboxes['account-a']).toEqual([
|
|
makeMailbox({ id: 'a-inbox' }),
|
|
]);
|
|
consoleError.mockRestore();
|
|
});
|
|
});
|