Files
webmail/components/templates/__tests__/template-dialogs.test.tsx
T

133 lines
5.1 KiB
TypeScript

import { fireEvent, render, screen, act } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useTemplateStore } from '@/stores/template-store';
import type { EmailTemplate } from '@/lib/template-types';
// The template dialogs sit on the Dialog shell (R4): close X, Escape,
// outside click, and "Discard changes?" when they hold unsaved input (R5).
const { TemplateManagerModal } = await import('../template-manager-modal');
const { PlaceholderFillModal } = await import('../placeholder-fill-modal');
const template: EmailTemplate = {
id: 'tpl-1',
name: 'Follow-up',
subject: 'Hello {{recipient_name}}',
body: 'Hi {{recipient_name}}',
isHTML: false,
category: '',
isFavorite: false,
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
} as unknown as EmailTemplate;
async function flush() {
await act(async () => {
await Promise.resolve();
});
}
describe('TemplateManagerModal', () => {
beforeEach(() => {
useTemplateStore.setState({ templates: [template] } as never);
});
it('has the Dialog close X and closes on it when nothing was changed', async () => {
const onClose = vi.fn();
render(<TemplateManagerModal isOpen onClose={onClose} />);
const dialog = screen.getByRole('dialog', { name: 'title' });
fireEvent.click(screen.getByRole('button', { name: 'close' }));
await flush();
expect(onClose).toHaveBeenCalledTimes(1);
expect(dialog).toBeTruthy();
});
it('asks before the X drops a template being written, and keeps it on Keep editing', async () => {
const onClose = vi.fn();
render(<TemplateManagerModal isOpen onClose={onClose} />);
fireEvent.click(screen.getByRole('button', { name: 'add' }));
fireEvent.change(screen.getByPlaceholderText('name_placeholder'), { target: { value: 'New one' } });
fireEvent.click(screen.getByRole('button', { name: 'close' }));
await flush();
expect(onClose).not.toHaveBeenCalled();
expect(screen.getByRole('alertdialog', { name: 'discard_changes' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'keep_editing' }));
await flush();
expect(onClose).not.toHaveBeenCalled();
expect((screen.getByPlaceholderText('name_placeholder') as HTMLInputElement).value).toBe('New one');
});
it('ignores a click outside while the form holds changes', async () => {
const onClose = vi.fn();
render(<TemplateManagerModal isOpen onClose={onClose} />);
fireEvent.click(screen.getByRole('button', { name: 'add' }));
fireEvent.change(screen.getByPlaceholderText('name_placeholder'), { target: { value: 'x' } });
const backdrop = screen.getByTestId('template-manager').parentElement!;
fireEvent.mouseDown(backdrop);
fireEvent.click(backdrop);
await flush();
expect(onClose).not.toHaveBeenCalled();
expect(screen.queryByRole('alertdialog')).toBeNull();
});
it('asks before deleting a template instead of an inline Delete strip', async () => {
render(<TemplateManagerModal isOpen onClose={vi.fn()} />);
fireEvent.click(screen.getByRole('button', { name: 'delete' }));
await flush();
const confirm = screen.getByRole('alertdialog', { name: 'delete_title' });
expect(useTemplateStore.getState().templates).toHaveLength(1);
fireEvent.click(confirm.querySelector('button.bg-destructive') as HTMLElement);
await flush();
expect(useTemplateStore.getState().templates).toHaveLength(0);
});
it('names its row buttons', () => {
render(<TemplateManagerModal isOpen onClose={vi.fn()} />);
expect(screen.getByRole('button', { name: 'edit' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'duplicate' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'favorite' })).toBeTruthy();
});
});
describe('PlaceholderFillModal', () => {
function renderFill(onClose = vi.fn()) {
render(
<PlaceholderFillModal
template={template}
placeholders={['recipient_name']}
autoFilled={{}}
onConfirm={vi.fn()}
onSkip={vi.fn()}
onClose={onClose}
/>,
);
return onClose;
}
it('closes on Escape at once while nothing was typed', async () => {
const onClose = renderFill();
fireEvent.keyDown(screen.getByRole('dialog', { name: 'fill_placeholders' }), { key: 'Escape' });
await flush();
expect(onClose).toHaveBeenCalledTimes(1);
});
it('asks before Escape drops typed values', async () => {
const onClose = renderFill();
fireEvent.change(screen.getByPlaceholderText('enter_value'), { target: { value: 'Ada' } });
fireEvent.keyDown(screen.getByRole('dialog', { name: 'fill_placeholders' }), { key: 'Escape' });
await flush();
expect(onClose).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: 'discard' }));
await flush();
expect(onClose).toHaveBeenCalledTimes(1);
});
it('has an outline secondary button and the close X', () => {
renderFill();
expect(screen.getByRole('button', { name: 'insert_raw' }).className).toContain('border-input');
expect(screen.getByRole('button', { name: 'close' })).toBeTruthy();
});
});