TemplateEditor.test.tsx259 lines · main
| 1 | import { screen, waitFor, within } from '@testing-library/react' |
| 2 | import userEvent from '@testing-library/user-event' |
| 3 | import { toast } from 'sonner' |
| 4 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 5 | |
| 6 | import { TEMPLATES_SCHEMAS } from './AuthTemplatesValidation' |
| 7 | import { TemplateEditor } from './TemplateEditor' |
| 8 | import { render } from '@/tests/helpers' |
| 9 | |
| 10 | const { |
| 11 | resetTemplateMock, |
| 12 | updateAuthConfigMock, |
| 13 | useAuthConfigQueryMock, |
| 14 | useAuthTemplateResetMutationMock, |
| 15 | useAuthConfigUpdateMutationMock, |
| 16 | useAsyncCheckPermissionsMock, |
| 17 | validateSpamMock, |
| 18 | } = vi.hoisted(() => ({ |
| 19 | resetTemplateMock: vi.fn(), |
| 20 | updateAuthConfigMock: vi.fn(), |
| 21 | useAuthConfigQueryMock: vi.fn(), |
| 22 | useAuthTemplateResetMutationMock: vi.fn(), |
| 23 | useAuthConfigUpdateMutationMock: vi.fn(), |
| 24 | useAsyncCheckPermissionsMock: vi.fn(), |
| 25 | validateSpamMock: vi.fn(), |
| 26 | })) |
| 27 | |
| 28 | vi.mock(import('common'), async (importOriginal) => { |
| 29 | const actual = await importOriginal() |
| 30 | |
| 31 | return { |
| 32 | ...actual, |
| 33 | useParams: vi.fn().mockReturnValue({ ref: 'project-ref' }), |
| 34 | } |
| 35 | }) |
| 36 | |
| 37 | vi.mock('@/components/ui/CodeEditor/CodeEditor', () => ({ |
| 38 | CodeEditor: ({ |
| 39 | value, |
| 40 | onInputChange, |
| 41 | }: { |
| 42 | value: string |
| 43 | onInputChange: (value: string) => void |
| 44 | }) => ( |
| 45 | <textarea |
| 46 | aria-label="Body source" |
| 47 | value={value} |
| 48 | onChange={(event) => onInputChange(event.target.value)} |
| 49 | /> |
| 50 | ), |
| 51 | })) |
| 52 | |
| 53 | vi.mock('@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges', () => ({ |
| 54 | PreventNavigationOnUnsavedChanges: () => null, |
| 55 | })) |
| 56 | |
| 57 | vi.mock('@/data/auth/auth-config-query', () => ({ |
| 58 | useAuthConfigQuery: useAuthConfigQueryMock, |
| 59 | })) |
| 60 | |
| 61 | vi.mock('@/data/auth/auth-config-update-mutation', () => ({ |
| 62 | useAuthConfigUpdateMutation: useAuthConfigUpdateMutationMock, |
| 63 | })) |
| 64 | |
| 65 | vi.mock('@/data/auth/auth-template-reset-mutation', () => ({ |
| 66 | useAuthTemplateResetMutation: useAuthTemplateResetMutationMock, |
| 67 | })) |
| 68 | |
| 69 | vi.mock('@/data/auth/validate-spam-mutation', () => ({ |
| 70 | useValidateSpamMutation: () => ({ mutate: validateSpamMock }), |
| 71 | })) |
| 72 | |
| 73 | vi.mock('@/hooks/misc/useCheckPermissions', () => ({ |
| 74 | useAsyncCheckPermissions: useAsyncCheckPermissionsMock, |
| 75 | })) |
| 76 | |
| 77 | vi.mock('sonner', () => ({ |
| 78 | toast: { |
| 79 | success: vi.fn(), |
| 80 | error: vi.fn(), |
| 81 | }, |
| 82 | })) |
| 83 | |
| 84 | const confirmationTemplate = TEMPLATES_SCHEMAS.find((template) => template.id === 'CONFIRMATION')! |
| 85 | |
| 86 | const createAuthConfig = ({ |
| 87 | subject = 'Confirm your email address', |
| 88 | body, |
| 89 | hasCustomBody, |
| 90 | hasCustomSubject = false, |
| 91 | }: { |
| 92 | subject?: string |
| 93 | body: string |
| 94 | hasCustomBody: boolean |
| 95 | hasCustomSubject?: boolean |
| 96 | }) => ({ |
| 97 | MAILER_SUBJECTS_CONFIRMATION: subject, |
| 98 | MAILER_SUBJECTS_CUSTOM_CONTENTS: { |
| 99 | MAILER_SUBJECTS_CONFIRMATION: hasCustomSubject, |
| 100 | }, |
| 101 | MAILER_TEMPLATES_CONFIRMATION_CONTENT: body, |
| 102 | MAILER_TEMPLATES_CUSTOM_CONTENTS: { |
| 103 | MAILER_TEMPLATES_CONFIRMATION_CONTENT: hasCustomBody, |
| 104 | }, |
| 105 | SMTP_HOST: 'smtp.example.com', |
| 106 | SMTP_PASS: 'password', |
| 107 | SMTP_USER: 'user', |
| 108 | }) |
| 109 | |
| 110 | const renderTemplateEditor = ({ |
| 111 | body = '<p>Default template</p>', |
| 112 | canUpdateConfig = true, |
| 113 | hasCustomBody = false, |
| 114 | hasCustomSubject = false, |
| 115 | }: { |
| 116 | body?: string |
| 117 | canUpdateConfig?: boolean |
| 118 | hasCustomBody?: boolean |
| 119 | hasCustomSubject?: boolean |
| 120 | } = {}) => { |
| 121 | useAuthConfigQueryMock.mockReturnValue({ |
| 122 | data: createAuthConfig({ body, hasCustomBody, hasCustomSubject }), |
| 123 | isSuccess: true, |
| 124 | }) |
| 125 | useAsyncCheckPermissionsMock.mockReturnValue({ can: canUpdateConfig }) |
| 126 | useAuthConfigUpdateMutationMock.mockReturnValue({ mutate: updateAuthConfigMock }) |
| 127 | useAuthTemplateResetMutationMock.mockReturnValue({ mutate: resetTemplateMock }) |
| 128 | |
| 129 | return render(<TemplateEditor template={confirmationTemplate} />) |
| 130 | } |
| 131 | |
| 132 | describe('TemplateEditor reset to default', () => { |
| 133 | beforeEach(() => { |
| 134 | vi.clearAllMocks() |
| 135 | validateSpamMock.mockImplementation((_vars, callbacks) => callbacks?.onSuccess?.({ rules: [] })) |
| 136 | }) |
| 137 | |
| 138 | const resetAuthConfig = createAuthConfig({ |
| 139 | subject: 'Confirm your email address', |
| 140 | body: '<h2>Confirm your email address</h2>\n\n<p>Follow the link below to confirm this email address and finish signing up.</p>\n<p><a href="{{ .ConfirmationURL }}">Confirm email address</a></p>', |
| 141 | hasCustomBody: false, |
| 142 | hasCustomSubject: false, |
| 143 | }) |
| 144 | |
| 145 | it('hides reset when the API does not mark the body as custom', () => { |
| 146 | renderTemplateEditor() |
| 147 | |
| 148 | expect(screen.queryByRole('button', { name: 'Reset template' })).not.toBeInTheDocument() |
| 149 | }) |
| 150 | |
| 151 | it('shows reset when the API marks the body as custom', () => { |
| 152 | renderTemplateEditor({ hasCustomBody: true }) |
| 153 | |
| 154 | expect(screen.getByRole('button', { name: 'Reset template' })).toBeInTheDocument() |
| 155 | }) |
| 156 | |
| 157 | it('shows reset when the API marks the subject as custom', () => { |
| 158 | renderTemplateEditor({ hasCustomSubject: true }) |
| 159 | |
| 160 | expect(screen.getByRole('button', { name: 'Reset template' })).toBeInTheDocument() |
| 161 | }) |
| 162 | |
| 163 | it('keeps reset visible while there are unsaved editor changes', async () => { |
| 164 | const user = userEvent.setup() |
| 165 | renderTemplateEditor({ hasCustomBody: true }) |
| 166 | |
| 167 | await user.clear(screen.getByLabelText('Body source')) |
| 168 | await user.type(screen.getByLabelText('Body source'), '<p>Unsaved body</p>') |
| 169 | |
| 170 | expect(screen.getByRole('button', { name: 'Reset template' })).toBeInTheDocument() |
| 171 | }) |
| 172 | |
| 173 | it('warns that reset discards unsaved changes', async () => { |
| 174 | const user = userEvent.setup() |
| 175 | renderTemplateEditor({ hasCustomBody: true }) |
| 176 | |
| 177 | await user.clear(screen.getByLabelText('Body source')) |
| 178 | await user.type(screen.getByLabelText('Body source'), '<p>Unsaved body</p>') |
| 179 | await user.click(screen.getByRole('button', { name: 'Reset template' })) |
| 180 | |
| 181 | expect( |
| 182 | await screen.findByText( |
| 183 | 'This will discard your unsaved changes and use the default subject line and email body content.' |
| 184 | ) |
| 185 | ).toBeInTheDocument() |
| 186 | }) |
| 187 | |
| 188 | it('resets the template through the dedicated reset endpoint after confirmation', async () => { |
| 189 | const user = userEvent.setup() |
| 190 | resetTemplateMock.mockImplementation((_vars, callbacks) => |
| 191 | callbacks?.onSuccess?.(resetAuthConfig) |
| 192 | ) |
| 193 | |
| 194 | renderTemplateEditor({ hasCustomBody: true }) |
| 195 | |
| 196 | await user.click(screen.getByRole('button', { name: 'Reset template' })) |
| 197 | const dialog = await screen.findByRole('alertdialog') |
| 198 | await user.click(within(dialog).getByRole('button', { name: 'Reset' })) |
| 199 | |
| 200 | await waitFor(() => |
| 201 | expect(resetTemplateMock).toHaveBeenCalledWith( |
| 202 | { |
| 203 | projectRef: 'project-ref', |
| 204 | template: 'confirmation', |
| 205 | }, |
| 206 | expect.any(Object) |
| 207 | ) |
| 208 | ) |
| 209 | |
| 210 | expect(toast.success).toHaveBeenCalledWith('Email template reset to default') |
| 211 | }) |
| 212 | |
| 213 | it('does not reset through the auth config update payload', async () => { |
| 214 | const user = userEvent.setup() |
| 215 | resetTemplateMock.mockImplementation((_vars, callbacks) => |
| 216 | callbacks?.onSuccess?.(resetAuthConfig) |
| 217 | ) |
| 218 | |
| 219 | renderTemplateEditor({ hasCustomBody: true }) |
| 220 | |
| 221 | await user.click(screen.getByRole('button', { name: 'Reset template' })) |
| 222 | const dialog = await screen.findByRole('alertdialog') |
| 223 | await user.click(within(dialog).getByRole('button', { name: 'Reset' })) |
| 224 | |
| 225 | await waitFor(() => expect(resetTemplateMock).toHaveBeenCalled()) |
| 226 | |
| 227 | expect(updateAuthConfigMock).not.toHaveBeenCalled() |
| 228 | }) |
| 229 | |
| 230 | it('uses the reset response as the new editor state', async () => { |
| 231 | const user = userEvent.setup() |
| 232 | resetTemplateMock.mockImplementation((_vars, callbacks) => |
| 233 | callbacks?.onSuccess?.(resetAuthConfig) |
| 234 | ) |
| 235 | |
| 236 | renderTemplateEditor({ |
| 237 | body: '<p>Custom body</p>', |
| 238 | hasCustomBody: true, |
| 239 | hasCustomSubject: true, |
| 240 | }) |
| 241 | |
| 242 | await user.click(screen.getByRole('button', { name: 'Reset template' })) |
| 243 | const dialog = await screen.findByRole('alertdialog') |
| 244 | await user.click(within(dialog).getByRole('button', { name: 'Reset' })) |
| 245 | |
| 246 | await waitFor(() => { |
| 247 | expect(screen.getByDisplayValue('Confirm your email address')).toBeInTheDocument() |
| 248 | expect(screen.getByLabelText('Body source')).toHaveValue( |
| 249 | '<h2>Confirm your email address</h2>\n\n<p>Follow the link below to confirm this email address and finish signing up.</p>\n<p><a href="{{ .ConfirmationURL }}">Confirm email address</a></p>' |
| 250 | ) |
| 251 | }) |
| 252 | }) |
| 253 | |
| 254 | it('disables reset when the user cannot update auth config', () => { |
| 255 | renderTemplateEditor({ hasCustomBody: true, canUpdateConfig: false }) |
| 256 | |
| 257 | expect(screen.getByRole('button', { name: 'Reset template' })).toBeDisabled() |
| 258 | }) |
| 259 | }) |