Files
SRCmail/lib/__tests__/message-list-tabs-store.test.ts
T

145 lines
5.8 KiB
TypeScript

// Contract tests for the plugin-facing message-list category tabs
// (api.tabs.set → stores/message-list-tabs-store.ts).
import { describe, it, expect, beforeEach } from 'vitest';
import {
useMessageListTabsStore,
validateTabsConfig,
resolveTabFilter,
} from '@/stores/message-list-tabs-store';
import type { MessageListTab } from '@/lib/plugin-types';
const GMAIL_STYLE = {
tabs: [
{ id: 'primary', label: 'Primary', keyword: null },
{ id: 'promotions', label: 'Promotions', keyword: '$category-promotions' },
{ id: 'social', label: 'Social', query: { operator: 'OR', conditions: [{ from: 'reddit.com' }, { from: 'linkedin.com' }] } },
{ id: 'updates', label: 'Updates', keyword: '$category-updates' },
],
};
function resetStore() {
useMessageListTabsStore.setState({
registrations: {},
tabs: [],
mailboxRoles: [],
activeTabId: null,
tabCounts: {},
isCountsLoading: false,
});
}
beforeEach(resetStore);
describe('validateTabsConfig', () => {
it('normalizes a valid config and defaults mailboxRoles to inbox', () => {
const v = validateTabsConfig(GMAIL_STYLE);
expect(v.tabs).toHaveLength(4);
expect(v.mailboxRoles).toEqual(['inbox']);
expect(v.tabs[1].keyword).toBe('$category-promotions');
expect(v.tabs[2].query).toEqual(GMAIL_STYLE.tabs[2].query);
});
it('lowercases keywords (IMAP keywords are case-insensitive)', () => {
const v = validateTabsConfig({
tabs: [
{ id: 'a', label: 'A', keyword: '$Category-Promotions' },
{ id: 'b', label: 'B', keyword: null },
],
});
expect(v.tabs[0].keyword).toBe('$category-promotions');
});
it('rejects reserved and tag keywords', () => {
expect(() => validateTabsConfig({
tabs: [{ id: 'a', label: 'A', keyword: '$seen' }, { id: 'b', label: 'B' }],
})).toThrow(/reserved/);
expect(() => validateTabsConfig({
tabs: [{ id: 'a', label: 'A', keyword: '$label.x' }, { id: 'b', label: 'B' }],
})).toThrow(/reserved/);
});
it('rejects duplicate ids, multiple default tabs, and single-tab configs', () => {
expect(() => validateTabsConfig({
tabs: [{ id: 'a', label: 'A' }, { id: 'a', label: 'B', keyword: '$x' }],
})).toThrow(/duplicate/);
expect(() => validateTabsConfig({
tabs: [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }],
})).toThrow(/default/);
expect(() => validateTabsConfig({
tabs: [{ id: 'a', label: 'A', keyword: '$x' }],
})).toThrow(/at least 2/);
});
it('rejects non-object and oversized queries', () => {
expect(() => validateTabsConfig({
tabs: [{ id: 'a', label: 'A', query: 'from:x' as unknown as Record<string, unknown> }, { id: 'b', label: 'B' }],
})).toThrow(/filter object/);
const huge = { from: 'x'.repeat(20000) };
expect(() => validateTabsConfig({
tabs: [{ id: 'a', label: 'A', query: huge }, { id: 'b', label: 'B' }],
})).toThrow(/exceeds/);
});
});
describe('resolveTabFilter', () => {
const tabs = validateTabsConfig(GMAIL_STYLE).tabs as MessageListTab[];
it('uses hasKeyword for keyword tabs and the raw query for search tabs', () => {
expect(resolveTabFilter(tabs[1], tabs)).toEqual({ hasKeyword: '$category-promotions' });
expect(resolveTabFilter(tabs[2], tabs)).toEqual(GMAIL_STYLE.tabs[2].query);
});
it('resolves the default tab to NOT(all other positive fragments)', () => {
const filter = resolveTabFilter(tabs[0], tabs) as { operator: string; conditions: unknown[] };
expect(filter.operator).toBe('NOT');
expect(filter.conditions).toHaveLength(3);
expect(filter.conditions).toContainEqual({ hasKeyword: '$category-promotions' });
expect(filter.conditions).toContainEqual(GMAIL_STYLE.tabs[2].query);
});
});
describe('store registration', () => {
it('registers tabs, activates the default tab, and scopes to the inbox role', () => {
const store = useMessageListTabsStore.getState();
store.registerTabs('category-tabs', GMAIL_STYLE);
const s = useMessageListTabsStore.getState();
expect(s.tabs.map(t => t.id)).toEqual(['primary', 'promotions', 'social', 'updates']);
expect(s.activeTabId).toBe('primary');
expect(s.isEnabledForRole('inbox')).toBe(true);
expect(s.isEnabledForRole('trash')).toBe(false);
// Default tab active → NOT filter.
const filter = s.getCategoryFilter('inbox') as { operator: string };
expect(filter.operator).toBe('NOT');
expect(s.getCategoryFilter('trash')).toBeNull();
});
it('keeps only the first default tab when two plugins both register one', () => {
const store = useMessageListTabsStore.getState();
store.registerTabs('p1', { tabs: [{ id: 'main', label: 'Main', order: 1 }, { id: 'x', label: 'X', keyword: '$x' }] });
store.registerTabs('p2', { tabs: [{ id: 'other', label: 'Other', order: 2 }, { id: 'y', label: 'Y', keyword: '$y' }] });
const defaults = useMessageListTabsStore.getState().tabs.filter(t => !t.query && !t.keyword);
expect(defaults.map(t => t.id)).toEqual(['main']);
});
it('clears a plugin registration and resets counts when the strip empties', () => {
const store = useMessageListTabsStore.getState();
store.registerTabs('category-tabs', GMAIL_STYLE);
useMessageListTabsStore.setState({ tabCounts: { promotions: 5 } });
store.clearTabs('category-tabs');
const s = useMessageListTabsStore.getState();
expect(s.tabs).toHaveLength(0);
expect(s.activeTabId).toBeNull();
expect(s.tabCounts).toEqual({});
expect(s.getCategoryFilter('inbox')).toBeNull();
});
it('switches the active tab and exposes its filter', () => {
const store = useMessageListTabsStore.getState();
store.registerTabs('category-tabs', GMAIL_STYLE);
store.setActiveTab('promotions', 'inbox-id');
expect(useMessageListTabsStore.getState().getCategoryFilter('inbox')).toEqual({ hasKeyword: '$category-promotions' });
});
});