import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query' import { mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { defineComponent, h, ref } from 'vue' vi.mock('@/lib/axios', () => { const post = vi.fn() const put = vi.fn() const get = vi.fn() return { apiClient: { post, put, get } } }) import { apiClient } from '@/lib/axios' import { draftSubmitterStorageKey, useFormDraft } from '@/composables/useFormDraft' interface MockedApi { post: ReturnType put: ReturnType get: ReturnType } const mocked = apiClient as unknown as MockedApi function mountWithDraft(token: string, options?: Parameters[1]) { const draftRef: { value: ReturnType | null } = { value: null } const Host = defineComponent({ setup() { draftRef.value = useFormDraft(ref(token), options) return () => h('div') }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }) const wrapper = mount(Host, { global: { plugins: [[VueQueryPlugin, { queryClient }]] }, }) return { wrapper, draftRef } } function submissionFixture(id: string) { return { id, form_schema_id: 'sc_1', status: 'draft', auto_save_count: 0, submitted_in_locale: 'nl', schema_version_at_submit: null, schema_drift: false, values: {}, identity_match: null, opened_at: null, first_interacted_at: null, submitted_at: null, submission_duration_seconds: null, created_at: null, updated_at: null, } } function apiSuccess(body: T) { return { data: { data: body, message: 'ok', success: true } } } function flush(): Promise { return new Promise(resolve => setTimeout(resolve, 0)) } describe('useFormDraft - submitter details', () => { const TOKEN = 'TKN-sub' beforeEach(() => { sessionStorage.clear() vi.clearAllMocks() }) afterEach(() => { sessionStorage.clear() }) it('starts with empty submitterName and submitterEmail', () => { const { draftRef, wrapper } = mountWithDraft(TOKEN) const draft = draftRef.value! expect(draft.submitterName.value).toBe('') expect(draft.submitterEmail.value).toBe('') wrapper.unmount() }) it('setSubmitterName marks the draft dirty — subsequent flush includes public_submitter_name in the PUT body', async () => { mocked.post.mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) mocked.put.mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) const { draftRef, wrapper } = mountWithDraft(TOKEN, { debounceMs: 200 }) const draft = draftRef.value! await draft.start() await flush() draft.setSubmitterName('Test User') await draft.saveDraftNow() expect(mocked.put).toHaveBeenCalledTimes(1) const [, body] = mocked.put.mock.calls[0] expect(body.public_submitter_name).toBe('Test User') wrapper.unmount() }) it('setSubmitterEmail flows through to the PUT body on flush', async () => { mocked.post.mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) mocked.put.mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) const { draftRef, wrapper } = mountWithDraft(TOKEN, { debounceMs: 200 }) const draft = draftRef.value! await draft.start() await flush() draft.setSubmitterEmail('user@example.nl') await draft.saveDraftNow() expect(mocked.put).toHaveBeenCalledTimes(1) const [, body] = mocked.put.mock.calls[0] expect(body.public_submitter_email).toBe('user@example.nl') wrapper.unmount() }) it('submitForm sends submitter name and email in the POST /submit body', async () => { mocked.post .mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) // start .mockResolvedValueOnce(apiSuccess({ ...submissionFixture('s1'), status: 'submitted' })) // submit const { draftRef, wrapper } = mountWithDraft(TOKEN) const draft = draftRef.value! await draft.start() await flush() draft.setSubmitterName('Alice') draft.setSubmitterEmail('alice@example.nl') const result = await draft.submitForm() await flush() expect(result).toBeTruthy() expect(mocked.post).toHaveBeenCalledTimes(2) const [, submitBody] = mocked.post.mock.calls[1] expect(submitBody.public_submitter_name).toBe('Alice') expect(submitBody.public_submitter_email).toBe('alice@example.nl') wrapper.unmount() }) // Contract: if submitter fields are empty at submit time, submitForm // returns null, sets submitError with code 'MISSING_SUBMITTER', and // does NOT hit the submit endpoint — the page is expected to read // submitError and bounce the user back to the Contactgegevens step. it('submitForm returns null and sets submitError when submitter fields are empty', async () => { mocked.post.mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) // start only const { draftRef, wrapper } = mountWithDraft(TOKEN) const draft = draftRef.value! await draft.start() await flush() expect(mocked.post).toHaveBeenCalledTimes(1) const result = await draft.submitForm() expect(result).toBeNull() const err = draft.submitError.value as { code?: string } | null expect(err).toBeTruthy() expect(err?.code).toBe('MISSING_SUBMITTER') // Submit endpoint was NOT called — only the start POST is in the log. expect(mocked.post).toHaveBeenCalledTimes(1) wrapper.unmount() }) it('submitForm refuses to submit when only one of name/email is set', async () => { mocked.post.mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) const { draftRef, wrapper } = mountWithDraft(TOKEN) const draft = draftRef.value! await draft.start() await flush() draft.setSubmitterName('Bob') // email intentionally left empty const result = await draft.submitForm() expect(result).toBeNull() const err = draft.submitError.value as { code?: string } | null expect(err?.code).toBe('MISSING_SUBMITTER') expect(mocked.post).toHaveBeenCalledTimes(1) wrapper.unmount() }) it('restores submitter details from sessionStorage on start and includes them in the initial POST body', async () => { sessionStorage.setItem( draftSubmitterStorageKey(TOKEN), JSON.stringify({ name: 'Resumed', email: 'resumed@example.nl' }), ) mocked.post.mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) const { draftRef, wrapper } = mountWithDraft(TOKEN) const draft = draftRef.value! await draft.start() await flush() expect(draft.submitterName.value).toBe('Resumed') expect(draft.submitterEmail.value).toBe('resumed@example.nl') const [, body] = mocked.post.mock.calls[0] expect(body.public_submitter_name).toBe('Resumed') expect(body.public_submitter_email).toBe('resumed@example.nl') wrapper.unmount() }) it('persists submitter details to sessionStorage when set', () => { const { draftRef, wrapper } = mountWithDraft(TOKEN) const draft = draftRef.value! draft.setSubmitterName('Persist Me') draft.setSubmitterEmail('persist@example.nl') const raw = sessionStorage.getItem(draftSubmitterStorageKey(TOKEN)) expect(raw).toBeTruthy() const parsed = JSON.parse(raw as string) expect(parsed).toEqual({ name: 'Persist Me', email: 'persist@example.nl' }) wrapper.unmount() }) it('clears the submitter sessionStorage entry after a successful submit', async () => { mocked.post .mockResolvedValueOnce(apiSuccess(submissionFixture('s1'))) .mockResolvedValueOnce(apiSuccess({ ...submissionFixture('s1'), status: 'submitted' })) const { draftRef, wrapper } = mountWithDraft(TOKEN) const draft = draftRef.value! await draft.start() await flush() draft.setSubmitterName('Cleaner') draft.setSubmitterEmail('cleaner@example.nl') expect(sessionStorage.getItem(draftSubmitterStorageKey(TOKEN))).toBeTruthy() await draft.submitForm() await flush() expect(sessionStorage.getItem(draftSubmitterStorageKey(TOKEN))).toBeNull() wrapper.unmount() }) })