Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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) => <MemoryRouter>{storyFn()}</MemoryRouter>,
fullScreenDecorator,
LoggedInDecorator,
],
parameters: {
layout: 'fullscreen',
chromatic: { pauseAnimationAtEnd: true },
msw: baseMsw,
},
} as Meta

const Template: StoryFn<UseTemplateModalProps> = (args) => {
const modalProps = useDisclosure({ defaultIsOpen: true })
return (
<UseTemplateModal
{...args}
{...modalProps}
formId={MOCK_SOURCE_FORM_ID}
onClose={() => console.log('close modal')}
/>
)
}

export const Default = Template.bind({})

const mrfCutoverOn = new GrowthBook({
features: { [featureFlags.mrfCutover]: { defaultValue: true } },
})

const withCutover = (Story: StoryFn) => (
<GrowthBookProvider growthbook={mrfCutoverOn}>
<Story />
</GrowthBookProvider>
)

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 }),
],
}
Original file line number Diff line number Diff line change
@@ -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()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useFeatureIsOn } from '@growthbook/growthbook-react'
import { act, renderHook } from '@testing-library/react'

import { FormResponseMode } from 'formsg-shared/types'

import { usePreviewForm } from '~features/admin-form/common/queries'
import { useUser } from '~features/user/queries'
import {
useDuplicateFormMutations,
useEmailModeFeedbackMutation,
} from '~features/workspace/mutations'
import { useDashboard } from '~features/workspace/queries'

import { useDupeFormWizardContext } from './DupeFormWizardProvider'

vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (k: string) => k }),
}))
vi.mock('@growthbook/growthbook-react', () => ({
useFeatureIsOn: vi.fn(),
}))
vi.mock('~features/workspace/queries')
vi.mock('~features/admin-form/common/queries')
vi.mock('~features/workspace/mutations')
vi.mock('~features/user/queries')

const MOCK_FORM_ID = 'form123'
const MOCK_USER_EMAIL = 'admin@example.com'

const makeMockPreviewFormData = (responseMode = FormResponseMode.Encrypt) => ({
spcpSession: null,
form: { title: 'Source', form_fields: [], responseMode },
})

describe('DupeFormWizardProvider — cutover behaviour', () => {
const dupeStorageModeFormMutation = { isLoading: false, mutate: vi.fn() }
const dupeMultirespondentModeFormMutation = {
isLoading: false,
mutate: vi.fn(),
}
const dupeEmailModeFormMutation = { isLoading: false, mutate: vi.fn() }

beforeEach(() => {
vi.clearAllMocks()

vi.mocked(useFeatureIsOn).mockReturnValue(true)

vi.mocked(usePreviewForm).mockReturnValue({
data: makeMockPreviewFormData(FormResponseMode.Encrypt) as any,
isLoading: false,
} as any)

vi.mocked(useDashboard).mockReturnValue({
data: [],
isLoading: false,
} as any)

vi.mocked(useUser).mockReturnValue({
user: { email: MOCK_USER_EMAIL } as any,
} as any)

vi.mocked(useDuplicateFormMutations).mockReturnValue({
dupeEmailModeFormMutation,
dupeStorageModeFormMutation,
dupeMultirespondentModeFormMutation,
} as any)

vi.mocked(useEmailModeFeedbackMutation).mockReturnValue({
emailModeFeedbackMutation: { isLoading: false, mutate: vi.fn() },
} as any)
})

it('defaults responseMode to Multirespondent when cutover is on, regardless of source mode', () => {
const { result } = renderHook(() =>
useDupeFormWizardContext(vi.fn(), {
formIdToDuplicate: MOCK_FORM_ID as any,
}),
)

expect(result.current.formMethods.getValues().responseMode).toBe(
FormResponseMode.Multirespondent,
)
})

it('fires the multirespondent dupe mutation on default submit', async () => {
const { result } = renderHook(() =>
useDupeFormWizardContext(vi.fn(), {
formIdToDuplicate: MOCK_FORM_ID as any,
}),
)

act(() => {
result.current.formMethods.setValue('title', 'New title')
})
await act(async () => {
await result.current.handleCreateStorageModeOrMultirespondentForm()
})

expect(dupeMultirespondentModeFormMutation.mutate).toHaveBeenCalledTimes(1)
expect(dupeMultirespondentModeFormMutation.mutate).toHaveBeenCalledWith(
expect.objectContaining({
formIdToDuplicate: MOCK_FORM_ID,
title: 'New title',
responseMode: FormResponseMode.Multirespondent,
}),
expect.anything(),
)
expect(dupeStorageModeFormMutation.mutate).not.toHaveBeenCalled()
})

it('fires the storage dupe mutation with [user.email] fallback after escape hatch + submit', async () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (spec gap, confidence 85): the "back to MRF default" path is untested.

What's happening

  • This test (and the matching one in UseTemplateWizardProvider.cutover.test.tsx) calls goToStorageModeDetails() then submits — only the forward path.
  • Neither test calls goToMrfDetails() (defined in CreateFormWizardProvider.tsx:64-67, which resets responseMode to Multirespondent and returns to the Details step) nor asserts the step returns.

Why it matters
Issue #9454 lists "a back affordance returns the user to the MRF default screen" as acceptance criteria. The behaviour exists but is unpinned at the unit layer.

Suggestion
Add one assertion: after goToStorageModeDetails(), call goToMrfDetails() and expect result.current.currentStep to equal CreateFormFlowStates.Details.

🤖 This comment was generated by an AI code review. Please verify before acting on it.

const { result } = renderHook(() =>
useDupeFormWizardContext(vi.fn(), {
formIdToDuplicate: MOCK_FORM_ID as any,
}),
)

act(() => {
result.current.goToStorageModeDetails()
result.current.formMethods.setValue('title', 'New title')
})
await act(async () => {
await result.current.handleCreateStorageModeOrMultirespondentForm()
})

expect(dupeStorageModeFormMutation.mutate).toHaveBeenCalledTimes(1)
expect(dupeStorageModeFormMutation.mutate).toHaveBeenCalledWith(
expect.objectContaining({
formIdToDuplicate: MOCK_FORM_ID,
title: 'New title',
responseMode: FormResponseMode.Encrypt,
emails: [MOCK_USER_EMAIL],
}),
expect.anything(),
)
expect(dupeMultirespondentModeFormMutation.mutate).not.toHaveBeenCalled()
})
})
Loading
Loading