Files
SRCmail/stores/__tests__/settings-store-keywords.test.ts

236 lines
9.4 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { useSettingsStore, DEFAULT_KEYWORDS, DEV_KEYWORDS, KEYWORD_PALETTE, KEYWORD_PALETTE_ROWS, getKeywordVisibility } from '../settings-store';
import type { KeywordDefinition } from '../settings-store';
describe('settings-store keywords', () => {
beforeEach(() => {
useSettingsStore.setState({ emailKeywords: [...DEFAULT_KEYWORDS] });
});
describe('DEFAULT_KEYWORDS', () => {
it('has 7 default keywords', () => {
expect(DEFAULT_KEYWORDS).toHaveLength(7);
});
it('each default keyword has a valid palette color', () => {
DEFAULT_KEYWORDS.forEach((kw) => {
expect(KEYWORD_PALETTE[kw.color]).toBeDefined();
expect(KEYWORD_PALETTE[kw.color].dot).toBeTruthy();
expect(KEYWORD_PALETTE[kw.color].fill).toBeTruthy();
});
});
it('all default keyword ids are unique', () => {
const ids = DEFAULT_KEYWORDS.map((k) => k.id);
expect(new Set(ids).size).toBe(ids.length);
});
it('ships no nested tag, which is opt-in', () => {
DEFAULT_KEYWORDS.forEach((kw) => expect(kw.id).not.toContain('/'));
});
});
describe('DEV_KEYWORDS', () => {
it('every nested tag has its parent defined, so the tree has no gaps', () => {
const ids = new Set(DEV_KEYWORDS.map((k) => k.id));
DEV_KEYWORDS.forEach((kw) => {
const cut = kw.id.lastIndexOf('/');
if (cut > 0) expect(ids, `orphan: ${kw.id}`).toContain(kw.id.slice(0, cut));
});
});
it('nests deeply enough to exercise the tree', () => {
const depths = DEV_KEYWORDS.map((k) => k.id.split('/').length);
expect(Math.max(...depths)).toBeGreaterThanOrEqual(3);
});
it('each dev keyword has a valid palette color and a unique id', () => {
const ids = DEV_KEYWORDS.map((k) => k.id);
expect(new Set(ids).size).toBe(ids.length);
DEV_KEYWORDS.forEach((kw) => expect(KEYWORD_PALETTE[kw.color]).toBeDefined());
});
});
describe('KEYWORD_PALETTE', () => {
it('has a lighter, base and darker shade of every hue', () => {
expect(KEYWORD_PALETTE_ROWS).toHaveLength(3);
KEYWORD_PALETTE_ROWS.forEach((row) => expect(row).toHaveLength(13));
expect(Object.keys(KEYWORD_PALETTE)).toHaveLength(39);
});
it('lays every row out in the same hue order', () => {
const [light, base, dark] = KEYWORD_PALETTE_ROWS;
expect(light).toEqual(base.map((key) => `${key}-light`));
expect(dark).toEqual(base.map((key) => `${key}-dark`));
});
it('keeps the bare hue name on the base row, so saved tags still resolve', () => {
// A tag stored as `red` predates the lighter and darker rows.
expect(KEYWORD_PALETTE_ROWS[1]).toContain('red');
expect(KEYWORD_PALETTE.red).toBeDefined();
});
it('spells every class out so Tailwind can find it', () => {
// A composed class name would compile to nothing, so none may be built
// at runtime and each has to carry its own utility prefix.
Object.values(KEYWORD_PALETTE).forEach((entry) => {
expect(entry.dot).toMatch(/^bg-/);
expect(entry.fill).toMatch(/^bg-/);
expect(entry.border).toMatch(/^border-/);
expect(entry.text).toMatch(/^text-.* dark:text-/);
expect(entry.rowTint).toMatch(/^bg-.* dark:bg-/);
});
});
it('resolves every row key', () => {
KEYWORD_PALETTE_ROWS.flat().forEach((key) => {
expect(KEYWORD_PALETTE[key]).toBeDefined();
});
});
});
describe('addKeyword', () => {
it('adds a new keyword to the list', () => {
const newKw: KeywordDefinition = { id: 'custom', label: 'Custom', color: 'teal' };
useSettingsStore.getState().addKeyword(newKw);
const keywords = useSettingsStore.getState().emailKeywords;
expect(keywords).toHaveLength(DEFAULT_KEYWORDS.length + 1);
expect(keywords[keywords.length - 1]).toEqual(newKw);
});
it('does not add duplicate keyword id', () => {
const duplicate: KeywordDefinition = { id: 'red', label: 'Another Red', color: 'red' };
useSettingsStore.getState().addKeyword(duplicate);
expect(useSettingsStore.getState().emailKeywords).toHaveLength(DEFAULT_KEYWORDS.length);
});
it('allows adding keyword after removing one with same id', () => {
useSettingsStore.getState().removeKeyword('red');
const newRed: KeywordDefinition = { id: 'red', label: 'New Red', color: 'red' };
useSettingsStore.getState().addKeyword(newRed);
const kw = useSettingsStore.getState().emailKeywords.find((k) => k.id === 'red');
expect(kw?.label).toBe('New Red');
});
});
describe('updateKeyword', () => {
it('updates label of existing keyword', () => {
useSettingsStore.getState().updateKeyword('red', { label: 'Crimson' });
const kw = useSettingsStore.getState().emailKeywords.find((k) => k.id === 'red');
expect(kw?.label).toBe('Crimson');
expect(kw?.color).toBe('red'); // color unchanged
});
it('updates color of existing keyword', () => {
useSettingsStore.getState().updateKeyword('blue', { color: 'cyan' });
const kw = useSettingsStore.getState().emailKeywords.find((k) => k.id === 'blue');
expect(kw?.color).toBe('cyan');
expect(kw?.label).toBe('Blue'); // label unchanged
});
it('updates both label and color', () => {
useSettingsStore.getState().updateKeyword('green', { label: 'Emerald', color: 'teal' });
const kw = useSettingsStore.getState().emailKeywords.find((k) => k.id === 'green');
expect(kw?.label).toBe('Emerald');
expect(kw?.color).toBe('teal');
});
it('does not affect other keywords', () => {
useSettingsStore.getState().updateKeyword('red', { label: 'Crimson' });
const blue = useSettingsStore.getState().emailKeywords.find((k) => k.id === 'blue');
expect(blue?.label).toBe('Blue');
});
it('is a no-op for non-existent id', () => {
const before = useSettingsStore.getState().emailKeywords;
useSettingsStore.getState().updateKeyword('nonexistent', { label: 'Test' });
const after = useSettingsStore.getState().emailKeywords;
expect(after).toHaveLength(before.length);
});
});
describe('removeKeyword', () => {
it('removes a keyword by id', () => {
useSettingsStore.getState().removeKeyword('red');
const keywords = useSettingsStore.getState().emailKeywords;
expect(keywords).toHaveLength(DEFAULT_KEYWORDS.length - 1);
expect(keywords.find((k) => k.id === 'red')).toBeUndefined();
});
it('is a no-op for non-existent id', () => {
useSettingsStore.getState().removeKeyword('nonexistent');
expect(useSettingsStore.getState().emailKeywords).toHaveLength(DEFAULT_KEYWORDS.length);
});
it('preserves order of remaining keywords', () => {
useSettingsStore.getState().removeKeyword('green');
const ids = useSettingsStore.getState().emailKeywords.map((k) => k.id);
expect(ids).toEqual(['red', 'orange', 'yellow', 'blue', 'purple', 'pink']);
});
});
describe('reorderKeywords', () => {
it('replaces keyword list with new ordering', () => {
const reversed = [...DEFAULT_KEYWORDS].reverse();
useSettingsStore.getState().reorderKeywords(reversed);
const ids = useSettingsStore.getState().emailKeywords.map((k) => k.id);
expect(ids).toEqual(reversed.map((k) => k.id));
});
it('can set to empty list', () => {
useSettingsStore.getState().reorderKeywords([]);
expect(useSettingsStore.getState().emailKeywords).toHaveLength(0);
});
it('can reset to defaults', () => {
useSettingsStore.getState().removeKeyword('red');
useSettingsStore.getState().removeKeyword('blue');
useSettingsStore.getState().reorderKeywords(DEFAULT_KEYWORDS);
expect(useSettingsStore.getState().emailKeywords).toEqual(DEFAULT_KEYWORDS);
});
});
describe('getKeywordById', () => {
it('finds keyword by id', () => {
const kw = useSettingsStore.getState().getKeywordById('blue');
expect(kw).toEqual({ id: 'blue', label: 'Blue', color: 'blue' });
});
it('returns undefined for non-existent id', () => {
expect(useSettingsStore.getState().getKeywordById('nonexistent')).toBeUndefined();
});
it('returns updated keyword data after updateKeyword', () => {
useSettingsStore.getState().updateKeyword('red', { label: 'Scarlet' });
const kw = useSettingsStore.getState().getKeywordById('red');
expect(kw?.label).toBe('Scarlet');
});
});
describe('getKeywordVisibility', () => {
it('treats a tag stored before visibility was configurable as always shown', () => {
expect(getKeywordVisibility({ id: 'red', label: 'Red', color: 'red' })).toBe('show');
});
it('returns the stored choice when there is one', () => {
expect(getKeywordVisibility({ id: 'red', label: 'Red', color: 'red', visibility: 'unread' })).toBe('unread');
expect(getKeywordVisibility({ id: 'red', label: 'Red', color: 'red', visibility: 'hide' })).toBe('hide');
});
});
describe('nestedTags', () => {
it('is off by default', () => {
useSettingsStore.getState().resetToDefaults();
expect(useSettingsStore.getState().nestedTags).toBe(false);
});
it('is included in exported settings', () => {
useSettingsStore.getState().updateSetting('nestedTags', true);
const exported = JSON.parse(useSettingsStore.getState().exportSettings()) as {
nestedTags?: boolean;
};
expect(exported.nestedTags).toBe(true);
});
});
});