import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { KeywordSettings } from '../keyword-settings'; import { useSettingsStore, DEFAULT_KEYWORDS } from '@/stores/settings-store'; // Mock SettingsSection to just render children, keeping the real controls vi.mock('../settings-section', async (importOriginal) => ({ ...(await importOriginal()), SettingsSection: ({ children }: { children: React.ReactNode }) =>
{children}
, })); describe('KeywordSettings', () => { beforeEach(() => { useSettingsStore.setState({ emailKeywords: [...DEFAULT_KEYWORDS], nestedTags: false }); }); it('renders all default keywords', () => { render(); DEFAULT_KEYWORDS.forEach((kw) => { expect(screen.getByText(kw.label)).toBeInTheDocument(); }); }); it('shows keyword JMAP id', () => { render(); expect(screen.getByText('$label:red')).toBeInTheDocument(); expect(screen.getByText('$label:blue')).toBeInTheDocument(); }); it('renders add keyword button', () => { render(); expect(screen.getByText('add_keyword')).toBeInTheDocument(); }); it('renders reset defaults button', () => { render(); expect(screen.getByText('reset_defaults')).toBeInTheDocument(); }); it('shows add form when add button clicked', () => { render(); fireEvent.click(screen.getByText('add_keyword')); expect(screen.getByPlaceholderText('label_placeholder')).toBeInTheDocument(); // Cancel and save buttons should appear expect(screen.getByText('cancel')).toBeInTheDocument(); expect(screen.getByText('add')).toBeInTheDocument(); }); it('adds a new keyword through the form', () => { render(); fireEvent.click(screen.getByText('add_keyword')); const input = screen.getByPlaceholderText('label_placeholder'); fireEvent.change(input, { target: { value: 'Important' } }); fireEvent.click(screen.getByText('add')); const keywords = useSettingsStore.getState().emailKeywords; expect(keywords).toHaveLength(DEFAULT_KEYWORDS.length + 1); expect(keywords[keywords.length - 1].label).toBe('Important'); expect(keywords[keywords.length - 1].id).toBe('important'); }); it('prevents adding keyword with duplicate id', () => { render(); fireEvent.click(screen.getByText('add_keyword')); const input = screen.getByPlaceholderText('label_placeholder'); fireEvent.change(input, { target: { value: 'Red' } }); // Should show duplicate warning expect(screen.getByText('id_exists')).toBeInTheDocument(); }); it('cancels add form when cancel clicked', () => { render(); fireEvent.click(screen.getByText('add_keyword')); expect(screen.getByPlaceholderText('label_placeholder')).toBeInTheDocument(); fireEvent.click(screen.getByText('cancel')); expect(screen.queryByPlaceholderText('label_placeholder')).not.toBeInTheDocument(); }); it('deletes keyword when delete button clicked', () => { render(); // Find delete buttons (title="delete") const deleteButtons = screen.getAllByTitle('delete'); expect(deleteButtons.length).toBe(DEFAULT_KEYWORDS.length); // Delete the first keyword fireEvent.click(deleteButtons[0]); expect(useSettingsStore.getState().emailKeywords).toHaveLength(DEFAULT_KEYWORDS.length - 1); expect(useSettingsStore.getState().emailKeywords.find((k) => k.id === 'red')).toBeUndefined(); }); it('shows edit form when edit button clicked', () => { render(); const editButtons = screen.getAllByTitle('edit'); fireEvent.click(editButtons[0]); // edit first keyword (Red) const input = screen.getByDisplayValue('Red'); expect(input).toBeInTheDocument(); expect(screen.getByText('save')).toBeInTheDocument(); }); it('updates keyword label through edit form', () => { render(); const editButtons = screen.getAllByTitle('edit'); fireEvent.click(editButtons[0]); // edit "Red" const input = screen.getByDisplayValue('Red'); fireEvent.change(input, { target: { value: 'Crimson' } }); fireEvent.click(screen.getByText('save')); const kw = useSettingsStore.getState().emailKeywords.find((k) => k.id === 'red'); expect(kw?.label).toBe('Crimson'); }); it('resets to defaults when reset button clicked', () => { // Modify keywords first useSettingsStore.getState().removeKeyword('red'); useSettingsStore.getState().removeKeyword('blue'); expect(useSettingsStore.getState().emailKeywords).toHaveLength(DEFAULT_KEYWORDS.length - 2); render(); fireEvent.click(screen.getByText('reset_defaults')); expect(useSettingsStore.getState().emailKeywords).toEqual(DEFAULT_KEYWORDS); }); it('normalizes label to id correctly', () => { render(); fireEvent.click(screen.getByText('add_keyword')); const input = screen.getByPlaceholderText('label_placeholder'); fireEvent.change(input, { target: { value: 'My Custom Tag!' } }); fireEvent.click(screen.getByText('add')); const keywords = useSettingsStore.getState().emailKeywords; const added = keywords[keywords.length - 1]; expect(added.id).toBe('my-custom-tag'); expect(added.label).toBe('My Custom Tag!'); }); it('offers no parent picker while nesting is off', () => { render(); fireEvent.click(screen.getByText('add_keyword')); expect(screen.queryByLabelText('parent_field')).not.toBeInTheDocument(); }); it('nests a new tag under the selected parent', () => { useSettingsStore.setState({ emailKeywords: [{ id: 'work', label: 'Work', color: 'blue' }], nestedTags: true, }); render(); fireEvent.click(screen.getByText('add_keyword')); fireEvent.change(screen.getByLabelText('parent_field'), { target: { value: 'work' } }); fireEvent.change(screen.getByPlaceholderText('label_placeholder'), { target: { value: 'Clients' } }); fireEvent.click(screen.getByText('add')); const keywords = useSettingsStore.getState().emailKeywords; expect(keywords[keywords.length - 1]).toMatchObject({ id: 'work/clients', label: 'Clients' }); }); it('shows nested tags by their full path', () => { useSettingsStore.setState({ emailKeywords: [ { id: 'work', label: 'Work', color: 'blue' }, { id: 'work/clients', label: 'Clients', color: 'green' }, ], nestedTags: true, }); render(); expect(screen.getByText('Work/Clients')).toBeInTheDocument(); expect(screen.getByText('$label:work/clients')).toBeInTheDocument(); }); it('rejects a path that would exceed the keyword length limit', () => { const deepId = 'a'.repeat(240); useSettingsStore.setState({ emailKeywords: [{ id: deepId, label: 'Deep', color: 'blue' }], nestedTags: true, }); render(); fireEvent.click(screen.getByText('add_keyword')); fireEvent.change(screen.getByLabelText('parent_field'), { target: { value: deepId } }); fireEvent.change(screen.getByPlaceholderText('label_placeholder'), { target: { value: 'Overflowing name' } }); expect(screen.getByText('too_long')).toBeInTheDocument(); expect(screen.getByText('add').closest('button')).toBeDisabled(); }); it('locks the name and the delete action of a tag that has nested tags', () => { useSettingsStore.setState({ emailKeywords: [ { id: 'work', label: 'Work', color: 'blue' }, { id: 'work/clients', label: 'Clients', color: 'green' }, ], nestedTags: true, }); render(); expect(screen.getByTitle('has_children_delete')).toBeDisabled(); fireEvent.click(screen.getAllByTitle('edit')[0]); expect(screen.getByDisplayValue('Work')).toBeDisabled(); expect(screen.getByText('has_children_locked')).toBeInTheDocument(); }); it('defaults every tag to always visible in the sidebar', () => { render(); const pickers = screen.getAllByLabelText('visibility_field'); expect(pickers).toHaveLength(DEFAULT_KEYWORDS.length); pickers.forEach((picker) => expect(picker).toHaveValue('show')); }); it('stores the visibility chosen for a tag', () => { render(); fireEvent.change(screen.getAllByLabelText('visibility_field')[0], { target: { value: 'unread' } }); expect(useSettingsStore.getState().emailKeywords.find((k) => k.id === 'red')?.visibility).toBe('unread'); }); });