Files
supabase/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.test.tsx
T
Danny WhiteandJoshen Lim f45a6085f4 feat(studio): add auth email template reset flow (#45572)
## What kind of change does this PR introduce?

Feature. Resolves AUTH-879.

## What is the current behavior?

Studio does not provide a way to reset an Auth email template back to
the default subject and body once it has been customised.

## What is the new behavior?

Studio shows a `Reset template` action when Platform reports that the
selected Auth email template subject or body has been customised. The
action opens a confirmation dialog, calls the dedicated Platform reset
endpoint, and refreshes the editor with the default subject and body
returned by the API.

The Auth config save/reset mutations now run their user-facing success
handling before refreshing Auth lint data, so the success toast and
local editor cleanup are not delayed by lint refetches.

## Additional context

Depends on supabase/platform#32417.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Reset email templates to defaults via a confirmation dialog; button
appears when custom content is detected and respects update permissions.
* Typed email-template definitions and improved template variable
display and descriptions.

* **Tests**
* Added tests covering template reset visibility, confirmation flow,
state updates, permission handling, and toast notifications.

* **Documentation**
* Example email template placeholders updated for internationalization
and provider snippets.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45572)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-05-18 14:33:26 +10:00

260 lines
8.2 KiB
TypeScript

import { screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { toast } from 'sonner'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TEMPLATES_SCHEMAS } from './AuthTemplatesValidation'
import { TemplateEditor } from './TemplateEditor'
import { render } from '@/tests/helpers'
const {
resetTemplateMock,
updateAuthConfigMock,
useAuthConfigQueryMock,
useAuthTemplateResetMutationMock,
useAuthConfigUpdateMutationMock,
useAsyncCheckPermissionsMock,
validateSpamMock,
} = vi.hoisted(() => ({
resetTemplateMock: vi.fn(),
updateAuthConfigMock: vi.fn(),
useAuthConfigQueryMock: vi.fn(),
useAuthTemplateResetMutationMock: vi.fn(),
useAuthConfigUpdateMutationMock: vi.fn(),
useAsyncCheckPermissionsMock: vi.fn(),
validateSpamMock: vi.fn(),
}))
vi.mock(import('common'), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
useParams: vi.fn().mockReturnValue({ ref: 'project-ref' }),
}
})
vi.mock('@/components/ui/CodeEditor/CodeEditor', () => ({
CodeEditor: ({
value,
onInputChange,
}: {
value: string
onInputChange: (value: string) => void
}) => (
<textarea
aria-label="Body source"
value={value}
onChange={(event) => onInputChange(event.target.value)}
/>
),
}))
vi.mock('@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges', () => ({
PreventNavigationOnUnsavedChanges: () => null,
}))
vi.mock('@/data/auth/auth-config-query', () => ({
useAuthConfigQuery: useAuthConfigQueryMock,
}))
vi.mock('@/data/auth/auth-config-update-mutation', () => ({
useAuthConfigUpdateMutation: useAuthConfigUpdateMutationMock,
}))
vi.mock('@/data/auth/auth-template-reset-mutation', () => ({
useAuthTemplateResetMutation: useAuthTemplateResetMutationMock,
}))
vi.mock('@/data/auth/validate-spam-mutation', () => ({
useValidateSpamMutation: () => ({ mutate: validateSpamMock }),
}))
vi.mock('@/hooks/misc/useCheckPermissions', () => ({
useAsyncCheckPermissions: useAsyncCheckPermissionsMock,
}))
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}))
const confirmationTemplate = TEMPLATES_SCHEMAS.find((template) => template.id === 'CONFIRMATION')!
const createAuthConfig = ({
subject = 'Confirm your email address',
body,
hasCustomBody,
hasCustomSubject = false,
}: {
subject?: string
body: string
hasCustomBody: boolean
hasCustomSubject?: boolean
}) => ({
MAILER_SUBJECTS_CONFIRMATION: subject,
MAILER_SUBJECTS_CUSTOM_CONTENTS: {
MAILER_SUBJECTS_CONFIRMATION: hasCustomSubject,
},
MAILER_TEMPLATES_CONFIRMATION_CONTENT: body,
MAILER_TEMPLATES_CUSTOM_CONTENTS: {
MAILER_TEMPLATES_CONFIRMATION_CONTENT: hasCustomBody,
},
SMTP_HOST: 'smtp.example.com',
SMTP_PASS: 'password',
SMTP_USER: 'user',
})
const renderTemplateEditor = ({
body = '<p>Default template</p>',
canUpdateConfig = true,
hasCustomBody = false,
hasCustomSubject = false,
}: {
body?: string
canUpdateConfig?: boolean
hasCustomBody?: boolean
hasCustomSubject?: boolean
} = {}) => {
useAuthConfigQueryMock.mockReturnValue({
data: createAuthConfig({ body, hasCustomBody, hasCustomSubject }),
isSuccess: true,
})
useAsyncCheckPermissionsMock.mockReturnValue({ can: canUpdateConfig })
useAuthConfigUpdateMutationMock.mockReturnValue({ mutate: updateAuthConfigMock })
useAuthTemplateResetMutationMock.mockReturnValue({ mutate: resetTemplateMock })
return render(<TemplateEditor template={confirmationTemplate} />)
}
describe('TemplateEditor reset to default', () => {
beforeEach(() => {
vi.clearAllMocks()
validateSpamMock.mockImplementation((_vars, callbacks) => callbacks?.onSuccess?.({ rules: [] }))
})
const resetAuthConfig = createAuthConfig({
subject: 'Confirm your email address',
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>',
hasCustomBody: false,
hasCustomSubject: false,
})
it('hides reset when the API does not mark the body as custom', () => {
renderTemplateEditor()
expect(screen.queryByRole('button', { name: 'Reset template' })).not.toBeInTheDocument()
})
it('shows reset when the API marks the body as custom', () => {
renderTemplateEditor({ hasCustomBody: true })
expect(screen.getByRole('button', { name: 'Reset template' })).toBeInTheDocument()
})
it('shows reset when the API marks the subject as custom', () => {
renderTemplateEditor({ hasCustomSubject: true })
expect(screen.getByRole('button', { name: 'Reset template' })).toBeInTheDocument()
})
it('keeps reset visible while there are unsaved editor changes', async () => {
const user = userEvent.setup()
renderTemplateEditor({ hasCustomBody: true })
await user.clear(screen.getByLabelText('Body source'))
await user.type(screen.getByLabelText('Body source'), '<p>Unsaved body</p>')
expect(screen.getByRole('button', { name: 'Reset template' })).toBeInTheDocument()
})
it('warns that reset discards unsaved changes', async () => {
const user = userEvent.setup()
renderTemplateEditor({ hasCustomBody: true })
await user.clear(screen.getByLabelText('Body source'))
await user.type(screen.getByLabelText('Body source'), '<p>Unsaved body</p>')
await user.click(screen.getByRole('button', { name: 'Reset template' }))
expect(
await screen.findByText(
'This will discard your unsaved changes and use the default subject line and email body content.'
)
).toBeInTheDocument()
})
it('resets the template through the dedicated reset endpoint after confirmation', async () => {
const user = userEvent.setup()
resetTemplateMock.mockImplementation((_vars, callbacks) =>
callbacks?.onSuccess?.(resetAuthConfig)
)
renderTemplateEditor({ hasCustomBody: true })
await user.click(screen.getByRole('button', { name: 'Reset template' }))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'Reset' }))
await waitFor(() =>
expect(resetTemplateMock).toHaveBeenCalledWith(
{
projectRef: 'project-ref',
template: 'confirmation',
},
expect.any(Object)
)
)
expect(toast.success).toHaveBeenCalledWith('Email template reset to default')
})
it('does not reset through the auth config update payload', async () => {
const user = userEvent.setup()
resetTemplateMock.mockImplementation((_vars, callbacks) =>
callbacks?.onSuccess?.(resetAuthConfig)
)
renderTemplateEditor({ hasCustomBody: true })
await user.click(screen.getByRole('button', { name: 'Reset template' }))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'Reset' }))
await waitFor(() => expect(resetTemplateMock).toHaveBeenCalled())
expect(updateAuthConfigMock).not.toHaveBeenCalled()
})
it('uses the reset response as the new editor state', async () => {
const user = userEvent.setup()
resetTemplateMock.mockImplementation((_vars, callbacks) =>
callbacks?.onSuccess?.(resetAuthConfig)
)
renderTemplateEditor({
body: '<p>Custom body</p>',
hasCustomBody: true,
hasCustomSubject: true,
})
await user.click(screen.getByRole('button', { name: 'Reset template' }))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'Reset' }))
await waitFor(() => {
expect(screen.getByDisplayValue('Confirm your email address')).toBeInTheDocument()
expect(screen.getByLabelText('Body source')).toHaveValue(
'<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>'
)
})
})
it('disables reset when the user cannot update auth config', () => {
renderTemplateEditor({ hasCustomBody: true, canUpdateConfig: false })
expect(screen.getByRole('button', { name: 'Reset template' })).toBeDisabled()
})
})