diff --git a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateModal.stories.tsx b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateModal.stories.tsx new file mode 100644 index 0000000000..d5d1c99756 --- /dev/null +++ b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateModal.stories.tsx @@ -0,0 +1,107 @@ +import { MemoryRouter } from 'react-router-dom' +import { useDisclosure } from '@chakra-ui/react' +import { GrowthBook, GrowthBookProvider } from '@growthbook/growthbook-react' +import { Meta, StoryFn } from '@storybook/react' + +import { featureFlags } from 'formsg-shared/constants' + +import { getTemplateFormResponse } from '~/mocks/msw/handlers/admin-form/template-form' +import { getUser, MOCK_USER, userHandlers } from '~/mocks/msw/handlers/user' + +import { fullScreenDecorator, LoggedInDecorator } from '~utils/storybook' + +import { UseTemplateModal, UseTemplateModalProps } from './UseTemplateModal' + +const MOCK_SOURCE_FORM_ID = '61540ece3d4a6e50ac0cc6ff' + +const baseMsw = [ + ...userHandlers({ delay: 0 }), + getTemplateFormResponse({ delay: 0 }), +] + +export default { + title: 'Pages/AdminFormPage/UseTemplateModal', + component: UseTemplateModal, + decorators: [ + (storyFn) => {storyFn()}, + fullScreenDecorator, + LoggedInDecorator, + ], + parameters: { + layout: 'fullscreen', + chromatic: { pauseAnimationAtEnd: true }, + msw: baseMsw, + }, +} as Meta + +const Template: StoryFn = (args) => { + const modalProps = useDisclosure({ defaultIsOpen: true }) + return ( + console.log('close modal')} + /> + ) +} + +export const Default = Template.bind({}) + +const mrfCutoverOn = new GrowthBook({ + features: { [featureFlags.mrfCutover]: { defaultValue: true } }, +}) + +const withCutover = (Story: StoryFn) => ( + + + +) + +export const MrfCutoverOn = Template.bind({}) +MrfCutoverOn.decorators = [withCutover] + +export const MrfCutoverOnChildrenBeta = Template.bind({}) +MrfCutoverOnChildrenBeta.decorators = [withCutover] +MrfCutoverOnChildrenBeta.parameters = { + msw: [ + getUser({ + delay: 0, + mockUser: { ...MOCK_USER, betaFlags: { children: true } }, + }), + getTemplateFormResponse({ delay: 0 }), + ], +} + +export const MrfCutoverOnWebhookV1Beta = Template.bind({}) +MrfCutoverOnWebhookV1Beta.decorators = [withCutover] +MrfCutoverOnWebhookV1Beta.parameters = { + msw: [ + getUser({ + delay: 0, + mockUser: { + ...MOCK_USER, + betaFlags: { createStorageModeForV1Webhook: true }, + }, + }), + getTemplateFormResponse({ delay: 0 }), + ], +} + +export const MrfCutoverOnAllExceptions = Template.bind({}) +MrfCutoverOnAllExceptions.decorators = [withCutover] +MrfCutoverOnAllExceptions.parameters = { + msw: [ + getUser({ + delay: 0, + mockUser: { + ...MOCK_USER, + betaFlags: { + children: true, + createStorageModeForV1Webhook: true, + }, + }, + }), + getTemplateFormResponse({ delay: 0 }), + ], +} diff --git a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.cutover.test.tsx b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.cutover.test.tsx new file mode 100644 index 0000000000..6c76b8e858 --- /dev/null +++ b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.cutover.test.tsx @@ -0,0 +1,130 @@ +import { useFeatureIsOn } from '@growthbook/growthbook-react' +import { act, renderHook } from '@testing-library/react' + +import { FormResponseMode } from 'formsg-shared/types' + +import { useFormTemplate } from '~features/admin-form/common/queries' +import { useUser } from '~features/user/queries' +import { useEmailModeFeedbackMutation } from '~features/workspace/mutations' + +import { useUseTemplateMutations } from '../mutation' + +import { useUseTemplateWizardContext } from './UseTemplateWizardProvider' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (k: string) => k }), +})) +vi.mock('@growthbook/growthbook-react', () => ({ + useFeatureIsOn: vi.fn(), +})) +vi.mock('~features/admin-form/common/queries') +vi.mock('~features/user/queries') +vi.mock('~features/workspace/mutations') +vi.mock('../mutation') + +const MOCK_FORM_ID = 'form123' +const MOCK_USER_EMAIL = 'admin@example.com' + +const makeMockTemplateFormData = () => ({ + spcpSession: null, + form: { title: 'Source', form_fields: [] }, +}) + +describe('UseTemplateWizardProvider — cutover behaviour', () => { + const useStorageModeFormTemplateMutation = { + isLoading: false, + mutate: vi.fn(), + } + const useMultirespondentFormTemplateMutation = { + isLoading: false, + mutate: vi.fn(), + } + const useEmailModeFormTemplateMutation = { isLoading: false, mutate: vi.fn() } + + beforeEach(() => { + vi.clearAllMocks() + + vi.mocked(useFeatureIsOn).mockReturnValue(true) + + vi.mocked(useFormTemplate).mockReturnValue({ + data: makeMockTemplateFormData() as any, + isLoading: false, + } as any) + + vi.mocked(useUser).mockReturnValue({ + user: { email: MOCK_USER_EMAIL } as any, + } as any) + + vi.mocked(useUseTemplateMutations).mockReturnValue({ + useEmailModeFormTemplateMutation, + useStorageModeFormTemplateMutation, + useMultirespondentFormTemplateMutation, + } as any) + + vi.mocked(useEmailModeFeedbackMutation).mockReturnValue({ + emailModeFeedbackMutation: { isLoading: false, mutate: vi.fn() }, + } as any) + }) + + it('defaults responseMode to Multirespondent when cutover is on', () => { + const { result } = renderHook(() => + useUseTemplateWizardContext(MOCK_FORM_ID, vi.fn()), + ) + + expect(result.current.formMethods.getValues().responseMode).toBe( + FormResponseMode.Multirespondent, + ) + }) + + it('fires the multirespondent template mutation on default submit', async () => { + const { result } = renderHook(() => + useUseTemplateWizardContext(MOCK_FORM_ID, vi.fn()), + ) + + act(() => { + result.current.formMethods.setValue('title', 'New title') + }) + await act(async () => { + await result.current.handleCreateStorageModeOrMultirespondentForm() + }) + + expect(useMultirespondentFormTemplateMutation.mutate).toHaveBeenCalledTimes( + 1, + ) + expect(useMultirespondentFormTemplateMutation.mutate).toHaveBeenCalledWith( + expect.objectContaining({ + formIdToDuplicate: MOCK_FORM_ID, + title: 'New title', + responseMode: FormResponseMode.Multirespondent, + }), + expect.anything(), + ) + expect(useStorageModeFormTemplateMutation.mutate).not.toHaveBeenCalled() + }) + + it('fires the storage template mutation with [user.email] fallback after escape hatch + submit', async () => { + const { result } = renderHook(() => + useUseTemplateWizardContext(MOCK_FORM_ID, vi.fn()), + ) + + act(() => { + result.current.goToStorageModeDetails() + result.current.formMethods.setValue('title', 'New title') + }) + await act(async () => { + await result.current.handleCreateStorageModeOrMultirespondentForm() + }) + + expect(useStorageModeFormTemplateMutation.mutate).toHaveBeenCalledTimes(1) + expect(useStorageModeFormTemplateMutation.mutate).toHaveBeenCalledWith( + expect.objectContaining({ + formIdToDuplicate: MOCK_FORM_ID, + title: 'New title', + responseMode: FormResponseMode.Encrypt, + emails: [MOCK_USER_EMAIL], + }), + expect.anything(), + ) + expect(useMultirespondentFormTemplateMutation.mutate).not.toHaveBeenCalled() + }) +}) diff --git a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx index 4cc12b5c5c..639618a889 100644 --- a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx +++ b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx @@ -75,13 +75,14 @@ export const useUseTemplateWizardContext = ( if (!formId) return switch (responseMode) { case FormResponseMode.Encrypt: { + const cutoverDefaultEmails = adminEmail ? [adminEmail] : [] return useStorageModeFormTemplateMutation.mutate( { formIdToDuplicate: formId, title, responseMode, publicKey: keypair.publicKey, - emails: emails.filter(Boolean), + emails: emails ? emails.filter(Boolean) : cutoverDefaultEmails, }, { onSuccess: () => {