TemplateEditor.test.tsx259 lines · main
1import { screen, waitFor, within } from '@testing-library/react'
2import userEvent from '@testing-library/user-event'
3import { toast } from 'sonner'
4import { beforeEach, describe, expect, it, vi } from 'vitest'
5
6import { TEMPLATES_SCHEMAS } from './AuthTemplatesValidation'
7import { TemplateEditor } from './TemplateEditor'
8import { render } from '@/tests/helpers'
9
10const {
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
28vi.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
37vi.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
53vi.mock('@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges', () => ({
54 PreventNavigationOnUnsavedChanges: () => null,
55}))
56
57vi.mock('@/data/auth/auth-config-query', () => ({
58 useAuthConfigQuery: useAuthConfigQueryMock,
59}))
60
61vi.mock('@/data/auth/auth-config-update-mutation', () => ({
62 useAuthConfigUpdateMutation: useAuthConfigUpdateMutationMock,
63}))
64
65vi.mock('@/data/auth/auth-template-reset-mutation', () => ({
66 useAuthTemplateResetMutation: useAuthTemplateResetMutationMock,
67}))
68
69vi.mock('@/data/auth/validate-spam-mutation', () => ({
70 useValidateSpamMutation: () => ({ mutate: validateSpamMock }),
71}))
72
73vi.mock('@/hooks/misc/useCheckPermissions', () => ({
74 useAsyncCheckPermissions: useAsyncCheckPermissionsMock,
75}))
76
77vi.mock('sonner', () => ({
78 toast: {
79 success: vi.fn(),
80 error: vi.fn(),
81 },
82}))
83
84const confirmationTemplate = TEMPLATES_SCHEMAS.find((template) => template.id === 'CONFIRMATION')!
85
86const 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
110const 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
132describe('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})