TemplateEditor.test.tsx 8.2 KB

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