Files
webmail/components/email/__tests__/folder-rules-menu.test.tsx
T

112 lines
4.7 KiB
TypeScript

import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { Mailbox } from '@/lib/jmap/types';
import type { FilterRule } from '@/lib/jmap/sieve-types';
import { generateScript } from '@/lib/sieve/generator';
import { mockSieveAccount } from '@/lib/filters/__tests__/sieve-mock';
import { FolderRulesSubMenu } from '../rules-menu';
import { ContextMenu } from '@/components/ui/context-menu';
import { useAuthStore } from '@/stores/auth-store';
import { useEmailStore } from '@/stores/email-store';
import { useToastStore } from '@/stores/toast-store';
const mailboxes = [
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
{ id: 'archive', name: 'Old stuff', role: null, parentId: null, myRights: { mayAddItems: true } },
{ id: 'news', name: 'News', role: null, parentId: null, myRights: { mayAddItems: true } },
] as unknown as Mailbox[];
function rule(id: string, overrides: Partial<FilterRule> = {}): FilterRule {
return {
id,
name: `Rule ${id}`,
enabled: true,
matchType: 'all',
conditions: [{ field: 'from', comparator: 'address_is', value: 'anna@acme.com' }],
actions: [{ type: 'move', value: 'News', mailboxId: 'news' }],
stopProcessing: true,
...overrides,
};
}
function setup(rules: FilterRule[], supportsSieve = true) {
const account = mockSieveAccount('a', [{ name: 'filters', content: generateScript(rules), isActive: true }]);
Object.assign(account.client, { supportsSieve: () => supportsSieve });
useAuthStore.setState({ client: account.client, activeAccountId: 'login-a' } as never);
return account;
}
function renderMenu(mailbox: Mailbox) {
return render(
<ContextMenu isOpen position={{ x: 0, y: 0 }} onClose={() => {}}>
<FolderRulesSubMenu mailbox={mailbox} onClose={() => {}} />
</ContextMenu>,
);
}
function openRules() {
fireEvent.mouseEnter(screen.getByTestId('mailbox-rules').parentElement!);
}
describe('Rules entry of the folder menu', () => {
beforeEach(() => {
useToastStore.getState().clearToasts();
useEmailStore.setState({ mailboxes, viewingAccountId: null, accountMailboxes: {}, error: null });
});
it('runs a saved rule on the mail already in the folder', async () => {
const account = setup([rule('r1')]);
account.client.queryEmailFields.mockResolvedValue([
{ id: 'e1', mailboxIds: { archive: true }, keywords: {}, from: [{ email: 'anna@acme.com' }] },
{ id: 'e2', mailboxIds: { archive: true }, keywords: {}, from: [{ email: 'bob@acme.com' }] },
]);
const batchMoveToMailbox = vi.fn(async () => true);
useEmailStore.setState({ batchMoveToMailbox });
renderMenu(mailboxes[1]);
openRules();
fireEvent.click(await screen.findByTestId('folder-rules:run:r1'));
// The progress toast names the rule and the folder while it runs.
expect(useToastStore.getState().toasts[0]).toMatchObject({ title: 'rules.running', progress: null });
await vi.waitFor(() => expect(useToastStore.getState().toasts.at(-1)?.title).toBe('rule_applied'));
expect(useToastStore.getState().toasts.some((t) => t.title === 'rules.running')).toBe(false);
expect(account.client.queryEmailFields).toHaveBeenCalledWith(
{ operator: 'AND', conditions: [{ inMailbox: 'archive' }, { from: 'anna@acme.com' }] },
['mailboxIds', 'keywords', 'from'],
'a',
);
expect(batchMoveToMailbox).toHaveBeenCalledWith(account.client, 'news', { emailIds: ['e1'], client: account.client, accountId: 'a' });
});
it('disables a rule that cannot run on existing mail', async () => {
const account = setup([rule('dated', { activeFrom: '2026-01-01T00:00:00Z' }), rule('ok')]);
renderMenu(mailboxes[1]);
openRules();
const dated = await screen.findByTestId('folder-rules:run:dated');
expect(dated).toHaveProperty('disabled', true);
expect(dated.textContent).toContain('rules.new_mail_only');
fireEvent.click(dated);
expect(account.client.queryEmailFields).not.toHaveBeenCalled();
expect(screen.getByTestId('folder-rules:run:ok')).toHaveProperty('disabled', false);
});
it('says so when the account has no rules', async () => {
setup([]);
renderMenu(mailboxes[1]);
openRules();
expect(await screen.findByTestId('folder-rules:empty')).toBeTruthy();
});
it('is hidden for an account without Sieve and for a shared folder', () => {
setup([rule('r1')], false);
const { unmount } = renderMenu(mailboxes[1]);
expect(screen.queryByTestId('mailbox-rules')).toBeNull();
unmount();
setup([rule('r1')]);
renderMenu({ ...mailboxes[1], id: 'owner:archive', isShared: true } as Mailbox);
expect(screen.queryByTestId('mailbox-rules')).toBeNull();
});
});