diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a1600fc88c..1dd6238b50 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,6 +20,7 @@ jobs: - name: Build env: NODE_OPTIONS: '--max-old-space-size=4096 --openssl-legacy-provider' + REACT_APP_FORMSG_SDK_MODE: 'test' run: npm run build - name: Run Playwright tests run: npx playwright test diff --git a/__tests__/e2e/constants/field.ts b/__tests__/e2e/constants/field.ts index 4a1cc9175f..6689825aa7 100644 --- a/__tests__/e2e/constants/field.ts +++ b/__tests__/e2e/constants/field.ts @@ -166,7 +166,7 @@ export const ALL_FIELDS: E2eFieldMetadata[] = [ { title: 'Birthday', fieldType: BasicField.Date, - val: format(new Date(), DATE_INPUT_FORMAT), + val: format(new Date(952970366), DATE_INPUT_FORMAT), dateValidation: { customMinDate: null, customMaxDate: null, @@ -174,7 +174,7 @@ export const ALL_FIELDS: E2eFieldMetadata[] = [ }, }, { - title: 'Pi', + title: 'What is the value of pi?', fieldType: BasicField.Decimal, ValidationOptions: { customMin: null, @@ -211,7 +211,7 @@ export const ALL_FIELDS: E2eFieldMetadata[] = [ fieldType: BasicField.HomeNo, val: '61234567', }, - // Hide for now, because it doesn't work unless we spin up localstack. + // TODO: Images don't work unless we spin up localstack. // { // title: 'Image', // fieldType: BasicField.Image, @@ -271,7 +271,7 @@ export const ALL_FIELDS: E2eFieldMetadata[] = [ fieldType: BasicField.Section, }, { - title: 'Name', + title: 'Your name', fieldType: BasicField.ShortText, ValidationOptions: { selectedValidation: null, diff --git a/__tests__/e2e/constants/form.ts b/__tests__/e2e/constants/form.ts index 359571ae26..bcd70026be 100644 --- a/__tests__/e2e/constants/form.ts +++ b/__tests__/e2e/constants/form.ts @@ -1,3 +1,5 @@ +import { FormResponseMode } from 'shared/types' + import { E2eFieldMetadata } from './field' import { E2eLogic } from './logic' import { E2eSettingsOptions } from './settings' @@ -7,3 +9,20 @@ export type E2eForm = { formLogics: E2eLogic[] formSettings: E2eSettingsOptions } + +interface E2eFormResponseModeBase { + responseMode: FormResponseMode +} + +interface E2eFormResponseModeEmail extends E2eFormResponseModeBase { + responseMode: FormResponseMode.Email +} + +interface E2eFormResponseModeEncrypt extends E2eFormResponseModeBase { + responseMode: FormResponseMode.Encrypt + secretKey: string +} + +export type E2eFormResponseMode = + | E2eFormResponseModeEmail + | E2eFormResponseModeEncrypt diff --git a/__tests__/e2e/constants/index.ts b/__tests__/e2e/constants/index.ts index c094c80bb9..7969e6166b 100644 --- a/__tests__/e2e/constants/index.ts +++ b/__tests__/e2e/constants/index.ts @@ -2,5 +2,7 @@ export * from './field' export * from './form' export * from './links' export * from './logic' +export * from './response' export * from './settings' +export * from './tests' export * from './user' diff --git a/__tests__/e2e/constants/links.ts b/__tests__/e2e/constants/links.ts index 417a2fe3f6..8111d81b83 100644 --- a/__tests__/e2e/constants/links.ts +++ b/__tests__/e2e/constants/links.ts @@ -1,6 +1,21 @@ export const ROOT_PAGE = `${process.env.APP_URL}` export const LOGIN_PAGE = `${ROOT_PAGE}/login` + export const DASHBOARD_PAGE = `${ROOT_PAGE}/dashboard` + export const ADMIN_FORM_PAGE_PREFIX = `${ROOT_PAGE}/admin/form` -export const PUBLIC_FORM_PAGE_PREFIX = ROOT_PAGE +export const ADMIN_FORM_PAGE_CREATE = (formId: string) => + `${ADMIN_FORM_PAGE_PREFIX}/${formId}` +export const ADMIN_FORM_PAGE_SETTINGS = (formId: string) => + `${ADMIN_FORM_PAGE_CREATE(formId)}/settings` +export const ADMIN_FORM_PAGE_RESPONSES = (formId: string) => + `${ADMIN_FORM_PAGE_CREATE(formId)}/results` +export const ADMIN_FORM_PAGE_RESPONSES_INDIVIDUAL = ( + formId: string, + responseId: string, +) => `${ADMIN_FORM_PAGE_CREATE(formId)}/results/${responseId}` + +const PUBLIC_FORM_PAGE_PREFIX = ROOT_PAGE +export const PUBLIC_FORM_PAGE = (formId: string) => + `${PUBLIC_FORM_PAGE_PREFIX}/${formId}` diff --git a/__tests__/e2e/constants/response.ts b/__tests__/e2e/constants/response.ts new file mode 100644 index 0000000000..47e120c9dd --- /dev/null +++ b/__tests__/e2e/constants/response.ts @@ -0,0 +1,16 @@ +import { FormResponseMode } from 'shared/types' + +interface FormResponseViewBase { + mode: FormResponseMode +} + +interface FormResponseViewEmail extends FormResponseViewBase { + mode: FormResponseMode.Email +} + +interface FormResponseViewEncrypt extends FormResponseViewBase { + mode: FormResponseMode.Encrypt + csv: boolean +} + +export type FormResponseView = FormResponseViewEmail | FormResponseViewEncrypt diff --git a/__tests__/e2e/constants/tests.ts b/__tests__/e2e/constants/tests.ts new file mode 100644 index 0000000000..86a0e676f3 --- /dev/null +++ b/__tests__/e2e/constants/tests.ts @@ -0,0 +1,152 @@ +import { BasicField, LogicConditionState, LogicType } from 'shared/types' + +import { ALL_FIELDS, E2eFieldMetadata } from './field' +import { E2eLogic } from './logic' + +type E2eTestFormDefinition = { + formFields: E2eFieldMetadata[] + formLogics: E2eLogic[] +} + +/** + * Test where all fields are shown based on a single field logic + */ +const TEST_ALL_FIELDS_SHOWN_BY_LOGIC_FORMFIELDS: E2eFieldMetadata[] = [ + { + title: 'Do you want to hide the fields?', + fieldType: BasicField.YesNo, + val: 'No', + }, + // TODO: Attachment fields don't work on storage mode unless we spin up localstack. + ...ALL_FIELDS.filter((field) => field.fieldType !== BasicField.Attachment), +] +export const TEST_ALL_FIELDS_SHOWN_BY_LOGIC: E2eTestFormDefinition = { + formFields: TEST_ALL_FIELDS_SHOWN_BY_LOGIC_FORMFIELDS, + formLogics: [ + // Single logic block: if "yes", show the fields. + { + conditions: [ + { + field: 0, + state: LogicConditionState.Equal, + value: 'No', + }, + ], + logicType: LogicType.ShowFields, + show: Array.from( + TEST_ALL_FIELDS_SHOWN_BY_LOGIC_FORMFIELDS, + (_, i) => i, + ).slice(1), + }, + ], +} + +/** + * Test where a field is hidden based on a single field logic + */ +const TEST_FIELD_HIDDEN_BY_LOGIC_FORMFIELDS: E2eFieldMetadata[] = [ + { + title: 'Do you want to show the fields?', + fieldType: BasicField.YesNo, + val: 'No', + }, + { + title: 'This field should be hidden', + fieldType: BasicField.ShortText, + ValidationOptions: { + selectedValidation: null, + customVal: null, + }, + val: '', + hidden: true, + }, +] +export const TEST_FIELD_HIDDEN_BY_LOGIC: E2eTestFormDefinition = { + formFields: TEST_FIELD_HIDDEN_BY_LOGIC_FORMFIELDS, + formLogics: [ + // Single logic block: if "yes", show the fields. + { + conditions: [ + { + field: 0, + state: LogicConditionState.Equal, + value: 'Yes', + }, + ], + logicType: LogicType.ShowFields, + show: Array.from( + TEST_FIELD_HIDDEN_BY_LOGIC_FORMFIELDS, + (_, i) => i, + ).slice(1), + }, + ], +} + +/** + * Test where submission is disabled via chained logic + */ +const TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC_MESSAGE = 'You shall not pass!' +const TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC_FORMFIELDS: E2eFieldMetadata[] = + [ + { + title: 'Enter a number at least 10', + fieldType: BasicField.Number, + ValidationOptions: { + selectedValidation: null, + customVal: null, + }, + val: '10', + }, + { + title: 'Favorite food', + fieldType: BasicField.Dropdown, + fieldOptions: ['Rice', 'Chocolate', 'Ice-Cream'], + val: 'Chocolate', + }, + { + title: 'Do you want to submit this form?', + fieldType: BasicField.YesNo, + val: 'Yes', + }, + ] +export const TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC: E2eTestFormDefinition & { + preventSubmitMessage: string +} = { + formFields: TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC_FORMFIELDS, + formLogics: [ + { + conditions: [ + { + field: 0, + state: LogicConditionState.Gte, + value: '10', + }, + ], + logicType: LogicType.ShowFields, + show: [1], + }, + { + conditions: [ + { + field: 1, + state: LogicConditionState.Either, + value: ['Rice', 'Chocolate'], + }, + ], + logicType: LogicType.ShowFields, + show: [2], + }, + { + conditions: [ + { + field: 2, + state: LogicConditionState.Equal, + value: 'Yes', + }, + ], + logicType: LogicType.PreventSubmit, + message: TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC_MESSAGE, + }, + ], + preventSubmitMessage: TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC_MESSAGE, +} diff --git a/__tests__/e2e/email-submission.spec.ts b/__tests__/e2e/email-submission.spec.ts index 01d10ef2fc..88e8695495 100644 --- a/__tests__/e2e/email-submission.spec.ts +++ b/__tests__/e2e/email-submission.spec.ts @@ -1,25 +1,28 @@ -import { Page } from '@playwright/test' import mongoose from 'mongoose' import { BasicField, FormAuthType, - LogicConditionState, - LogicType, + FormResponseMode, MyInfoAttribute, } from 'shared/types' import { IFormModel } from 'src/types' -import { expect, test } from './fixtures/auth' import { ALL_FIELDS, E2eFieldMetadata, - E2eForm, - E2eLogic, NO_LOGIC, SAMPLE_FIELD, + TEST_ALL_FIELDS_SHOWN_BY_LOGIC, + TEST_FIELD_HIDDEN_BY_LOGIC, + TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC, } from './constants' -import { createForm, fillForm, submitForm, verifySubmission } from './helpers' +import { test } from './fixtures' +import { + createForm, + createSubmissionTestRunnerForResponseMode, + verifySubmissionDisabled, +} from './helpers' import { createBlankVersion, createMyInfoField, @@ -30,6 +33,10 @@ import { makeMongooseFixtures, } from './utils' +const runEmailSubmissionTest = createSubmissionTestRunnerForResponseMode( + FormResponseMode.Email, +) + let db: mongoose.Connection let Form: IFormModel @@ -54,7 +61,11 @@ test.describe('Email form submission', () => { const formSettings = getSettings() // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with all fields optional', async ({ @@ -68,7 +79,11 @@ test.describe('Email form submission', () => { const formSettings = getSettings() // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with identical attachment names', async ({ @@ -89,7 +104,11 @@ test.describe('Email form submission', () => { const formSettings = getSettings() // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with optional and required attachments', async ({ @@ -119,7 +138,11 @@ test.describe('Email form submission', () => { const formSettings = getSettings() // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with Singpass authentication', async ({ @@ -133,7 +156,11 @@ test.describe('Email form submission', () => { }) // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with Corppass authentication', async ({ @@ -147,7 +174,11 @@ test.describe('Email form submission', () => { }) // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with SGID authentication', async ({ @@ -161,7 +192,11 @@ test.describe('Email form submission', () => { }) // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with MyInfo fields', async ({ @@ -186,171 +221,62 @@ test.describe('Email form submission', () => { }) // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with all fields shown by logic', async ({ page, }) => { // Define - const formFields: E2eFieldMetadata[] = [ - { - title: 'Do you want to show the fields?', - fieldType: BasicField.YesNo, - val: 'Yes', - }, - ...ALL_FIELDS, - ] - - const formLogics: E2eLogic[] = [ - // Single logic block: if "yes", show the fields. - { - conditions: [ - { - field: 0, - state: LogicConditionState.Equal, - value: 'Yes', - }, - ], - logicType: LogicType.ShowFields, - show: Array.from(formFields, (_, i) => i).slice(1), - }, - ] + const { formFields, formLogics } = TEST_ALL_FIELDS_SHOWN_BY_LOGIC const formSettings = getSettings() // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create and submit email mode form with a field hidden by logic', async ({ page, }) => { // Define - const formFields: E2eFieldMetadata[] = [ - { - title: 'Do you want to show the fields?', - fieldType: BasicField.YesNo, - val: 'No', - }, - { - title: 'This field should be hidden', - fieldType: BasicField.ShortText, - ValidationOptions: { - selectedValidation: null, - customVal: null, - }, - val: '', - hidden: true, - }, - ] - const formLogics: E2eLogic[] = [ - // Single logic block: if "yes", show the fields. - { - conditions: [ - { - field: 0, - state: LogicConditionState.Equal, - value: 'Yes', - }, - ], - logicType: LogicType.ShowFields, - show: Array.from(formFields, (_, i) => i).slice(1), - }, - ] + const { formFields, formLogics } = TEST_FIELD_HIDDEN_BY_LOGIC const formSettings = getSettings() // Test - await runTest(page, { formFields, formLogics, formSettings }) + await runEmailSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) }) test('Create email mode form with submission disabled by chained logic', async ({ page, }) => { - const preventSubmitMessage = 'You shall not pass!' - // Define - const formFields: E2eFieldMetadata[] = [ - { - title: 'Enter a number at least 10', - fieldType: BasicField.Number, - ValidationOptions: { - selectedValidation: null, - customVal: null, - }, - val: '10', - }, - { - title: 'Favorite food', - fieldType: BasicField.Dropdown, - fieldOptions: ['Rice', 'Chocolate', 'Ice-Cream'], - val: 'Chocolate', - }, - { - title: 'Do you want to submit this form?', - fieldType: BasicField.YesNo, - val: 'Yes', - }, - ] - const formLogics: E2eLogic[] = [ - { - conditions: [ - { - field: 0, - state: LogicConditionState.Gte, - value: '10', - }, - ], - logicType: LogicType.ShowFields, - show: [1], - }, - { - conditions: [ - { - field: 1, - state: LogicConditionState.Either, - value: ['Rice', 'Chocolate'], - }, - ], - logicType: LogicType.ShowFields, - show: [2], - }, - { - conditions: [ - { - field: 2, - state: LogicConditionState.Equal, - value: 'Yes', - }, - ], - logicType: LogicType.PreventSubmit, - message: preventSubmitMessage, - }, - ] + const { formFields, formLogics, preventSubmitMessage } = + TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC const formSettings = getSettings() // Test - const form = await createForm(page, Form, { + const { form } = await createForm(page, Form, FormResponseMode.Email, { formFields, formLogics, formSettings, }) - await fillForm(page, { form, formFields, formSettings }) - - await expect( - page.locator('button:has-text("Submission disabled")'), - ).toBeDisabled() - await expect(page.getByText(preventSubmitMessage)).toBeVisible() - + await verifySubmissionDisabled( + page, + { form, formFields, formSettings }, + preventSubmitMessage, + ) await deleteDocById(Form, form._id) }) }) - -const runTest = async (page: Page, formDef: E2eForm): Promise => { - const form = await createForm(page, Form, formDef) - const responseId = await submitForm(page, { - form, - ...formDef, - }) - await verifySubmission(page, { form, responseId, ...formDef }) - await deleteDocById(Form, form._id) -} diff --git a/__tests__/e2e/encrypt-submission.spec.ts b/__tests__/e2e/encrypt-submission.spec.ts new file mode 100644 index 0000000000..e1516a2b59 --- /dev/null +++ b/__tests__/e2e/encrypt-submission.spec.ts @@ -0,0 +1,137 @@ +import mongoose from 'mongoose' +import { BasicField, FormResponseMode } from 'shared/types' + +import { IFormModel } from 'src/types' + +import { + ALL_FIELDS as _ALL_FIELDS, + NO_LOGIC, + TEST_ALL_FIELDS_SHOWN_BY_LOGIC, + TEST_FIELD_HIDDEN_BY_LOGIC, + TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC, +} from './constants' +import { test } from './fixtures' +import { + createForm, + createSubmissionTestRunnerForResponseMode, + verifySubmissionDisabled, +} from './helpers' +import { + createBlankVersion, + createOptionalVersion, + deleteDocById, + getSettings, + makeModel, + makeMongooseFixtures, +} from './utils' + +// TODO: Attachment fields don't work on storage mode unless we spin up localstack. +const ALL_FIELDS = _ALL_FIELDS.filter( + (field) => field.fieldType !== BasicField.Attachment, +) + +const runEncryptSubmissionTest = createSubmissionTestRunnerForResponseMode( + FormResponseMode.Encrypt, +) + +let db: mongoose.Connection +let Form: IFormModel + +test.describe('Storage form submission', () => { + test.beforeAll(async () => { + // Create models + db = await makeMongooseFixtures() + Form = makeModel(db, 'form.server.model', 'Form') + }) + test.afterAll(async () => { + // Clean up db + db.models = {} + await db.close() + }) + + test('Create and submit storage mode form with all fields', async ({ + page, + }) => { + // Define + const formFields = ALL_FIELDS + const formLogics = NO_LOGIC + const formSettings = getSettings() + + // Test + await runEncryptSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) + }) + + test('Create and submit storage mode form with all fields optional', async ({ + page, + }) => { + // Define + const formFields = ALL_FIELDS.map((ff) => + createBlankVersion(createOptionalVersion(ff)), + ) + const formLogics = NO_LOGIC + const formSettings = getSettings() + + // Test + await runEncryptSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) + }) + + test('Create and submit storage mode form with all fields shown by logic', async ({ + page, + }) => { + // Define + const { formFields, formLogics } = TEST_ALL_FIELDS_SHOWN_BY_LOGIC + const formSettings = getSettings() + + // Test + await runEncryptSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) + }) + + test('Create and submit storage mode form with a field hidden by logic', async ({ + page, + }) => { + // Define + const { formFields, formLogics } = TEST_FIELD_HIDDEN_BY_LOGIC + const formSettings = getSettings() + + // Test + await runEncryptSubmissionTest(page, Form, { + formFields, + formLogics, + formSettings, + }) + }) + + test('Create storage mode form with submission disabled by chained logic', async ({ + page, + }) => { + // Define + const { formFields, formLogics, preventSubmitMessage } = + TEST_SUBMISSION_DISABLED_BY_CHAINED_LOGIC + const formSettings = getSettings() + + // Test + const { form } = await createForm(page, Form, FormResponseMode.Encrypt, { + formFields, + formLogics, + formSettings, + }) + await verifySubmissionDisabled( + page, + { form, formFields, formSettings }, + preventSubmitMessage, + ) + await deleteDocById(Form, form._id) + }) +}) diff --git a/__tests__/e2e/fixtures/auth.ts b/__tests__/e2e/fixtures/auth.ts index 5c92239a30..6d9f51921b 100644 --- a/__tests__/e2e/fixtures/auth.ts +++ b/__tests__/e2e/fixtures/auth.ts @@ -34,9 +34,9 @@ export const test = baseTest.extend({ // Log in with OTP const otp = await extractOtp(ADMIN_EMAIL) - expect(otp).toBeTruthy() + if (!otp) throw new Error('OTP not found in email') - await page.locator('input[name="otp"]').fill(otp!) + await page.locator('input[name="otp"]').fill(otp) await page.getByRole('button', { name: 'Sign in' }).click() diff --git a/__tests__/e2e/fixtures/index.ts b/__tests__/e2e/fixtures/index.ts new file mode 100644 index 0000000000..f140b2ec7e --- /dev/null +++ b/__tests__/e2e/fixtures/index.ts @@ -0,0 +1 @@ +export * from './auth' diff --git a/__tests__/e2e/helpers/createForm.ts b/__tests__/e2e/helpers/createForm.ts index a2b0392586..340a4230de 100644 --- a/__tests__/e2e/helpers/createForm.ts +++ b/__tests__/e2e/helpers/createForm.ts @@ -5,10 +5,12 @@ import { BASICFIELD_TO_DRAWER_META, MYINFO_FIELD_TO_DRAWER_META, } from 'frontend/src/features/admin-form/create/constants' +import { readFileSync } from 'fs' import { BasicField, DateSelectedValidation, FormAuthType, + FormResponseMode, FormStatus, LogicConditionState, LogicType, @@ -18,10 +20,13 @@ import { import { IFormModel, IFormSchema } from 'src/types' import { + ADMIN_FORM_PAGE_CREATE, ADMIN_FORM_PAGE_PREFIX, + ADMIN_FORM_PAGE_SETTINGS, DASHBOARD_PAGE, E2eFieldMetadata, E2eForm, + E2eFormResponseMode, E2eLogic, E2eSettingsOptions, NON_INPUT_FIELD_TYPES, @@ -35,33 +40,50 @@ import { getTitleWithQuestionNumber, } from '../utils' +type CreateFormReturn = { + form: IFormSchema + formResponseMode: E2eFormResponseMode +} + /** * Navigates to the dashboard and creates a new form with all the associated form settings. * @param {Page} page Playwright page - * @returns {IFormSchema} the created form + * @param {IFormMode} Form the Form database model + * @param {FormResponseMode} responseMode the type of form to be created - Email or Encrypt + * @param {E2eForm} e2eForm the form details to be created + * @returns {CreateFormReturn} the created form as found in the db along with the secret key, if it is a storage form */ export const createForm = async ( page: Page, Form: IFormModel, + responseMode: FormResponseMode, { formFields, formLogics, formSettings }: E2eForm, -): Promise => { - const formId = await addForm(page) - await addSettings(page, { formId, formSettings }) - await addFieldsAndLogic(page, formFields, formLogics) +): Promise => { + const { formId, formResponseMode } = await addForm(page, responseMode) + await addSettings(page, { formId, formResponseMode, formSettings }) + await addFieldsAndLogic(page, { formId, formFields, formLogics }) const form = await Form.findById(formId) - if (!form) throw Error('Form not found in db') - return form + return { form, formResponseMode } +} + +type AddFormReturn = { + formId: string + formResponseMode: E2eFormResponseMode } /** * Navigates to the dashboard and creates a new form. Ends on the admin builder page. * @param {Page} page Playwright page - * @returns {string} the created form id + * @param {FormResponseMode} responseMode the type of form to be created - Email or Encrypt + * @returns {AddFormReturn} the created form id and the secret key, if it is a storage form */ -const addForm = async (page: Page): Promise => { +const addForm = async ( + page: Page, + responseMode: FormResponseMode, +): Promise => { await page.goto(DASHBOARD_PAGE) // Press escape 5 times to get rid of any banners @@ -75,8 +97,42 @@ const addForm = async (page: Page): Promise => { await page.getByLabel('Form name').fill(`e2e-test-${cuid()}`) - await page.getByText('Email Mode').click() + await page + .getByText( + `${responseMode === FormResponseMode.Email ? 'email' : 'storage'} mode`, + ) + .click() await page.getByRole('button', { name: 'Next step' }).click() + + let formResponseMode: E2eFormResponseMode = { + responseMode: FormResponseMode.Email, + } + if (responseMode === FormResponseMode.Encrypt) { + // Download the secret key and save it for the test. + const downloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: 'Download key' }).click() + const download = await downloadPromise + const path = await download.path() + if (!path) throw new Error('Secret key download failed') + formResponseMode = { + responseMode: FormResponseMode.Encrypt, + secretKey: readFileSync(path).toString(), + } + + // Double check that the secret key exists on the screen. + await expect( + page.getByText(formResponseMode.secretKey, { exact: true }), + ).toBeVisible() + + // Click acknowledgement buttons + await page.getByText(/If I lose my Secret Key/).click() + await page + .getByRole('button', { + name: 'I have saved my Secret Key safely', + }) + .click() + } + await expect(page).toHaveURL(new RegExp(`${ADMIN_FORM_PAGE_PREFIX}/.*`, 'i')) const l = ADMIN_FORM_PAGE_PREFIX.length + 1 @@ -85,12 +141,12 @@ const addForm = async (page: Page): Promise => { .match(new RegExp(`${ADMIN_FORM_PAGE_PREFIX}/[a-fA-F0-9]{24}`))?.[0] .slice(l, l + 24) - expect(formId).toBeTruthy() + if (!formId) throw new Error('FormId not found in page url') // Clear any banners await page.getByRole('button', { name: 'Next' }).press('Escape') - return formId! + return { formId, formResponseMode } } /** Goes to settings page and adds settings, and toggle form to be open. @@ -102,11 +158,16 @@ const addSettings = async ( page: Page, { formId, + formResponseMode, formSettings, - }: { formId: string; formSettings: E2eSettingsOptions }, + }: { + formId: string + formResponseMode: E2eFormResponseMode + formSettings: E2eSettingsOptions + }, ): Promise => { await page.getByText('Settings').click() - await expect(page).toHaveURL(`${ADMIN_FORM_PAGE_PREFIX}/${formId}/settings`) + await expect(page).toHaveURL(ADMIN_FORM_PAGE_SETTINGS(formId)) await addGeneralSettings(page, formSettings) await addAuthSettings(page, formSettings) @@ -129,8 +190,20 @@ const addSettings = async ( }) .click() + if (formResponseMode.responseMode === FormResponseMode.Encrypt) { + // Upload the secret key and confirm to open the form. + await page + .getByPlaceholder('Enter or upload your Secret Key to continue') + .fill(formResponseMode.secretKey) + await page + .locator('label') + .filter({ hasText: 'If I lose my key' }) + .click() + await page.getByRole('button', { name: 'Activate form' }).click() + } + // Check toast - await expect(page.getByText(/your form is now open/i)).toBeVisible() + await expectToast(page, /your form is now open/i) // Check new label await expect( @@ -297,11 +370,14 @@ const addCollaborators = async ( const addFieldsAndLogic = async ( page: Page, - formFields: E2eFieldMetadata[], - formLogics: E2eLogic[], + { + formId, + formFields, + formLogics, + }: { formId: string; formFields: E2eFieldMetadata[]; formLogics: E2eLogic[] }, ) => { await page.getByText('Create').click() - await expect(page).toHaveURL(new RegExp(`${ADMIN_FORM_PAGE_PREFIX}/.*`, 'i')) + await expect(page).toHaveURL(ADMIN_FORM_PAGE_CREATE(formId)) await addFields(page, formFields) await addLogics(page, formFields, formLogics) @@ -654,9 +730,6 @@ const addLogics = async ( // Save await page.getByText('Add logic').click() - - // Check toast - await expectToast(page, /the logic was successfully created/i) } await page.reload() diff --git a/__tests__/e2e/helpers/index.ts b/__tests__/e2e/helpers/index.ts index fa61a2dd2f..54e4f7d8c2 100644 --- a/__tests__/e2e/helpers/index.ts +++ b/__tests__/e2e/helpers/index.ts @@ -1,3 +1,37 @@ +import { Page } from '@playwright/test' +import { FormResponseMode } from 'shared/types' + +import { IFormModel } from 'src/types' + +import { E2eForm } from '../constants' +import { deleteDocById } from '../utils' + +import { createForm } from './createForm' +import { submitForm } from './submitForm' +import { verifySubmission } from './verifySubmission' + export * from './createForm' export * from './submitForm' export * from './verifySubmission' + +export const createSubmissionTestRunnerForResponseMode = + (responseMode: FormResponseMode) => + async (page: Page, Form: IFormModel, formDef: E2eForm): Promise => { + const { form, formResponseMode } = await createForm( + page, + Form, + responseMode, + formDef, + ) + const responseId = await submitForm(page, { + form, + ...formDef, + }) + await verifySubmission(page, { + form, + formResponseMode, + responseId, + ...formDef, + }) + await deleteDocById(Form, form._id) + } diff --git a/__tests__/e2e/helpers/submitForm.ts b/__tests__/e2e/helpers/submitForm.ts index 053b1043f3..bdfae91e7e 100644 --- a/__tests__/e2e/helpers/submitForm.ts +++ b/__tests__/e2e/helpers/submitForm.ts @@ -8,7 +8,7 @@ import { E2eFieldMetadata, E2eSettingsOptions, NON_INPUT_FIELD_TYPES, - PUBLIC_FORM_PAGE_PREFIX, + PUBLIC_FORM_PAGE, } from '../constants' import { extractOtp, fillDropdown, isMyInfoableFieldType } from '../utils' @@ -34,6 +34,27 @@ export const submitForm = async ( return await clickSubmitBtn(page) } +/** + * Navigate to the public form page, fill the form fields and verify that the submission has been disabled. + * @param {Page} page Playwright page + * @param {IFormSchema} form the form returned from the db + * @param {E2eFieldMetadata[]} formFields the fields used to create the form + * @param {E2eSettingsOptions} formSettings the settings used to create the form + * @param {string} preventSubmitMessage the message shown when submission is disabled + */ +export const verifySubmissionDisabled = async ( + page: Page, + { form, formFields, formSettings }: SubmitFormProps, + preventSubmitMessage: string, +): Promise => { + await fillForm(page, { form, formFields, formSettings }) + + await expect( + page.locator('button:has-text("Submission disabled")'), + ).toBeDisabled() + await expect(page.getByText(preventSubmitMessage)).toBeVisible() +} + /** * Navigate to the public form page and fill the form fields. * @param {Page} page Playwright page @@ -42,7 +63,7 @@ export const submitForm = async ( * @param {E2eSettingsOptions} formSettings the settings used to create the form * @returns {string} the responseId */ -export const fillForm = async ( +const fillForm = async ( page: Page, { form, formFields, formSettings }: SubmitFormProps, ): Promise => { @@ -60,7 +81,7 @@ const accessForm = async ( page: Page, { form }: { form: IFormSchema }, ): Promise => { - await page.goto(`${PUBLIC_FORM_PAGE_PREFIX}/${form._id}`) + await page.goto(PUBLIC_FORM_PAGE(form._id)) await expect(page.getByRole('heading', { name: form.title })).toBeVisible() } diff --git a/__tests__/e2e/helpers/verifySubmission.ts b/__tests__/e2e/helpers/verifySubmission.ts index d754893d03..c4655644ef 100644 --- a/__tests__/e2e/helpers/verifySubmission.ts +++ b/__tests__/e2e/helpers/verifySubmission.ts @@ -1,5 +1,4 @@ import { expect, Page } from '@playwright/test' -import { format, parse } from 'date-fns' import { readFileSync } from 'fs' import { BasicField, FormAuthType, FormResponseMode } from 'shared/types' @@ -7,25 +6,29 @@ import { IFormSchema, SgidFieldTitle, SPCPFieldTitle } from 'src/types' import { ADMIN_EMAIL, - DATE_INPUT_FORMAT, - DATE_RESPONSE_FORMAT, + ADMIN_FORM_PAGE_RESPONSES, + ADMIN_FORM_PAGE_RESPONSES_INDIVIDUAL, E2eFieldMetadata, + E2eFormResponseMode, E2eSettingsOptions, } from '../constants' import { + expectAttachment, + expectContains, + expectToast, getAutoreplyEmail, + getResponseArray, + getResponseTitle, getSubmission, - isMyInfoableFieldType, - isVerifiableFieldType, } from '../utils' const MAIL_FROM = 'donotreply@mail.form.gov.sg' -export type VerifySubmissionProps = { +export type VerifySubmissionBaseInputs = { form: IFormSchema + responseId: string formFields: E2eFieldMetadata[] formSettings: E2eSettingsOptions - responseId: string } /** @@ -33,22 +36,27 @@ export type VerifySubmissionProps = { * match what is submitted. * @param {Page} page the Playwright page * @param {IFormSchema} form the form from the database - * @param {E2eFieldMetadata[]} formFields the field metadata used to create and fill the form + * @param {E2eFormResponseMode} formResponseMode the response mode of the form, including the secret key if the form is in encrypt mode * @param {string} responseId the response id of the submission to be verified + * @param {E2eFieldMetadata[]} formFields the field metadata used to create and fill the form + * @param {E2eSettingsOptions} formSettings the form settings used to create the form */ export const verifySubmission = async ( page: Page, - verifySubmissionProps: VerifySubmissionProps, + data: VerifySubmissionBaseInputs & { formResponseMode: E2eFormResponseMode }, ): Promise => { - const { form, formFields, responseId } = verifySubmissionProps + const { formResponseMode, responseId, formFields } = data // Verify the submission content - switch (form.responseMode) { + switch (formResponseMode.responseMode) { case FormResponseMode.Email: - await verifyEmailSubmission(page, verifySubmissionProps) + await verifyEmailSubmission(data) break case FormResponseMode.Encrypt: - // TODO: add verifier for Encrypt submissions + await verifyEncryptSubmission(page, { + ...data, + ...formResponseMode, + }) break } @@ -74,15 +82,17 @@ export const verifySubmission = async ( /** * Get the submission email from maildev, and ensure that the contents and attachments * match what is submitted. - * @param {Page} page the Playwright page * @param {IFormSchema} form the form from the database - * @param {E2eFieldMetadata[]} formFields the field metadata used to create and fill the form * @param {string} responseId the response id of the submission to be verified + * @param {E2eFieldMetadata[]} formFields the field metadata used to create and fill the form + * @param {E2eSettingsOptions} formSettings the form settings used to create the form */ -export const verifyEmailSubmission = async ( - page: Page, - { form, formFields, formSettings, responseId }: VerifySubmissionProps, -): Promise => { +export const verifyEmailSubmission = async ({ + form, + responseId, + formFields, + formSettings, +}: VerifySubmissionBaseInputs): Promise => { // Get the submission from the email, via the subject. const submission = await getSubmission(form.title, responseId) @@ -102,10 +112,12 @@ export const verifyEmailSubmission = async ( // Verify form responses in email for (const field of formFields) { - const responseArray = getResponseArray(field, FormResponseMode.Email) + const responseArray = getResponseArray(field, { + mode: FormResponseMode.Email, + }) if (!responseArray) continue expectSubmissionContains([ - getResponseTitle(field, FormResponseMode.Email), + getResponseTitle(field, { mode: FormResponseMode.Email }), ...responseArray, ]) expectAttachment(field, submission.attachments) @@ -131,151 +143,79 @@ export const verifyEmailSubmission = async ( } } -// Utility for getting responses for tables -const TABLE_HANDLER = { - getName: (field: E2eFieldMetadata, formMode: FormResponseMode): string => { - if (field.fieldType !== BasicField.Table) return '' - let tableTitle = `${field.title} (${field.columns - .map((x) => x.title) - .join(', ')})` - if (formMode === FormResponseMode.Email) { - tableTitle = '[table] ' + tableTitle - } - return tableTitle - }, - getValues: ( - field: E2eFieldMetadata, - formMode: FormResponseMode, - ): string[] => { - if (field.fieldType !== BasicField.Table) return [] - switch (formMode) { - case FormResponseMode.Email: - return field.val.map((row) => row.join(',')) - case FormResponseMode.Encrypt: - // storage mode has a space - return field.val.map((row) => row.join(', ')) - } - }, -} - /** - * Gets the title of a field as it is displayed in a response. - * @param {E2eFieldMetadata} field field used to create and fill form - * @param {FormResponseMode} formMode form response mode - * @returns {string} the field title displayed in the response. + * Read the submission from the individual response page, and ensure that the + * contents and attachments match what is submitted. + * @param {Page} page the Playwright page + * @param {IFormSchema} form the form from the database + * @param {string} secretKey the secret key for the encrypt form + * @param {string} responseId the response id of the submission to be verified + * @param {E2eFieldMetadata[]} formFields the field metadata used to create and fill the form + * @param {E2eSettingsOptions} formSettings the form settings used to create the form */ -const getResponseTitle = ( - field: E2eFieldMetadata, - formMode: FormResponseMode, -): string => { - // MyInfo fields - if (isMyInfoableFieldType(field) && field.myInfo) { - if (field.myInfo.verified) return `[MyInfo] ${field.title}` - return field.title - } +export const verifyEncryptSubmission = async ( + page: Page, + { + form, + secretKey, + responseId, + formFields, + }: VerifySubmissionBaseInputs & { secretKey: string }, +): Promise => { + // Go to the responses summary page and enter the secret key + await page.goto(ADMIN_FORM_PAGE_RESPONSES(form._id)) + await page.getByLabel(/Enter or upload Secret Key/).fill(secretKey) + await page.getByRole('button', { name: 'Unlock responses' }).click() - // Basic fields - if (field.fieldType === BasicField.Table) { - // Delegate the work to the table handler - return TABLE_HANDLER.getName(field, formMode) - } - if (field.fieldType === BasicField.Attachment) { - switch (formMode) { - case FormResponseMode.Email: - return `[attachment] ${field.title}` - case FormResponseMode.Encrypt: - return field.title - } - } - if (isVerifiableFieldType(field)) { - if (field.isVerifiable && field.val) return `[verified] ${field.title}` - } - return field.title -} + // Try downloading CSV and checking contents + const downloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: 'Download' }).click() + await page.getByRole('menuitem', { name: 'CSV only' }).click() + const download = await downloadPromise + const path = await download.path() + if (!path) throw new Error('CSV download failed') -/** - * Gets answers for a field as displayed in response email or decrypted submission. - * Always returns an array, so caller must always loop through result. - * @param {E2eFieldMetadata} field field used to create and fill form - * @param {FormResponseMode} formMode form response mode - * @returns {string[] | null} string array of responses, or null if the field is a non-response field and should not be represented - */ -const getResponseArray = ( - field: E2eFieldMetadata, - formMode: FormResponseMode, -): string[] | null => { - // Deal with table first to avoid special cases later - if (field.fieldType === BasicField.Table) { - return TABLE_HANDLER.getValues(field, formMode) - } + await expectToast(page, /Success\. 1\/1 response was decrypted\./) - switch (field.fieldType) { - case BasicField.Section: { - return [''] - } - case BasicField.Image: - case BasicField.Statement: { - return null - } - case BasicField.Radio: - case BasicField.Checkbox: { - if (!field.val || (field.val instanceof Array && !field.val.length)) { - return [''] - } - return [ - (field.val instanceof Array ? field.val : [field.val]) - .map((selected) => { - return field.fieldOptions.includes(selected) - ? selected - : `Others: ${selected}` - }) - .join(', '), - ] - } - case BasicField.Date: { - // Need to re-parse, because input is in dd/mm/yyyy format whereas response is in dd MMM yyyy format. - return [ - field.val - ? format( - parse(field.val, DATE_INPUT_FORMAT, new Date()), - DATE_RESPONSE_FORMAT, - ) - : '', - ] - } - default: { - return [field.val] - } - } -} + const content = readFileSync(path).toString() + const expectSubmissionContains = expectContains(content) -/** - * Tests that container contains all the values in contained. - * @param {string} container string in which to search - * @param {string[]} containedArray Array of values to search for - */ -const expectContains = (container: string) => (containedArray: string[]) => { - for (const contained of containedArray) { - expect(container).toContain(contained) + expectSubmissionContains([responseId]) + for (const field of formFields) { + const responseArray = getResponseArray(field, { + mode: FormResponseMode.Encrypt, + csv: true, + }) + if (!responseArray) continue + expectSubmissionContains([field.title, ...responseArray]) } -} -/** - * Checks that an attachment field's attachment is contained in the email. - * @param {E2eFieldMetadata} field field used to create and fill form - * @param {Record} attachments map of attachment names to content - */ -const expectAttachment = ( - field: E2eFieldMetadata, - attachments: Record, -): void => { - if (field.fieldType !== BasicField.Attachment) return + // TODO: Attachments don't work in storage mode tests, so no need to download CSV with attachments. - // Attachments will not exist if it is unfilled. - if (!field.val) return + // Ensure there is a cell with the response ID and click into it + await page.getByRole('cell', { name: responseId }).click() - const content = attachments[field.val] + // We should be at the individual response page now. + await expect(page).toHaveURL( + ADMIN_FORM_PAGE_RESPONSES_INDIVIDUAL(form._id, responseId), + ) - // Check that contents match - expect(content).toEqual(readFileSync(field.path).toString()) + // Expect all the content of the page + for (const field of formFields) { + const responseArray = getResponseArray(field, { + mode: FormResponseMode.Encrypt, + csv: false, + }) + if (!responseArray) continue + const responseTitle = getResponseTitle(field, { + mode: FormResponseMode.Encrypt, + csv: false, + }) + await expect(page.getByText(responseTitle)).toBeVisible() + for (const response of responseArray) { + if (response) { + await expect(page.getByText(response, { exact: true })).toBeVisible() + } + } + } } diff --git a/__tests__/e2e/login.spec.ts b/__tests__/e2e/login.spec.ts index 0d6e52c47b..cd2192320b 100644 --- a/__tests__/e2e/login.spec.ts +++ b/__tests__/e2e/login.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test' import cuid from 'cuid' -import { ROOT_PAGE } from './constants' +import { DASHBOARD_PAGE, LOGIN_PAGE, ROOT_PAGE } from './constants' import { extractOtp } from './utils' test.describe('login', () => { @@ -13,7 +13,7 @@ test.describe('login', () => { page, }) => { await page.getByRole('link', { name: /log in/i }).click() - await expect(page).toHaveURL(`${ROOT_PAGE}/login`) + await expect(page).toHaveURL(LOGIN_PAGE) // Enter log in email. await page @@ -33,7 +33,7 @@ test.describe('login', () => { const legitUserEmail = `totally-legit-user${cuid()}@data.gov.sg` await page.getByRole('link', { name: 'Log in' }).click() - await expect(page).toHaveURL(`${ROOT_PAGE}/login`) + await expect(page).toHaveURL(LOGIN_PAGE) await page.getByRole('textbox', { name: /log in/i }).fill(legitUserEmail) await page.getByRole('button', { name: /log in/i }).click() @@ -50,7 +50,7 @@ test.describe('login', () => { await page.locator('input[name="otp"]').fill(otp!) await page.getByRole('button', { name: 'Sign in' }).click() - await expect(page).toHaveURL(`${ROOT_PAGE}/dashboard`) + await expect(page).toHaveURL(DASHBOARD_PAGE) }) test('Prevent login if OTP is incorrect', async ({ page }) => { @@ -58,7 +58,7 @@ test.describe('login', () => { const legitUserEmail = `totally-legit-user${cuid()}@data.gov.sg` await page.getByRole('link', { name: 'Log in' }).click() - await expect(page).toHaveURL(`${ROOT_PAGE}/login`) + await expect(page).toHaveURL(LOGIN_PAGE) await page.getByRole('textbox', { name: /log in/i }).fill(legitUserEmail) await page.getByRole('button', { name: /log in/i }).click() diff --git a/__tests__/e2e/utils/index.ts b/__tests__/e2e/utils/index.ts index 1b8d7dd750..d5ceb1165e 100644 --- a/__tests__/e2e/utils/index.ts +++ b/__tests__/e2e/utils/index.ts @@ -1,5 +1,6 @@ +export * from './database' export * from './field' export * from './mail' +export * from './response' export * from './settings' export * from './toast' -export * from './database' diff --git a/__tests__/e2e/utils/response.ts b/__tests__/e2e/utils/response.ts new file mode 100644 index 0000000000..26b548b9fb --- /dev/null +++ b/__tests__/e2e/utils/response.ts @@ -0,0 +1,170 @@ +import { expect } from '@playwright/test' +import { format, parse } from 'date-fns' +import { readFileSync } from 'fs' +import { BasicField, FormResponseMode } from 'shared/types' + +import { + DATE_INPUT_FORMAT, + DATE_RESPONSE_FORMAT, + E2eFieldMetadata, + FormResponseView, +} from '../constants' + +import { isMyInfoableFieldType, isVerifiableFieldType } from './field' + +// Utility for getting responses for tables +const TABLE_HANDLER = { + getName: ( + field: E2eFieldMetadata, + responseView: FormResponseView, + ): string => { + if (field.fieldType !== BasicField.Table) return '' + let tableTitle = `${field.title} (${field.columns + .map((x) => x.title) + .join(', ')})` + if (responseView.mode === FormResponseMode.Email) { + tableTitle = '[table] ' + tableTitle + } + return tableTitle + }, + getValues: ( + field: E2eFieldMetadata, + responseView: FormResponseView, + ): string[] => { + if (field.fieldType !== BasicField.Table) return [] + switch (responseView.mode) { + case FormResponseMode.Email: + return field.val.map((row) => row.join(',')) + case FormResponseMode.Encrypt: + // storage mode has a space + if (responseView.csv) return field.val.map((row) => row.join(';')) + return field.val.flat() + } + }, +} + +/** + * Gets the title of a field as it is displayed in a response. + * @param {E2eFieldMetadata} field field used to create and fill form + * @param {FormResponseView} formMode form response mode + * @returns {string} the field title displayed in the response. + */ +export const getResponseTitle = ( + field: E2eFieldMetadata, + responseView: FormResponseView, +): string => { + const isCsv = + responseView.mode === FormResponseMode.Encrypt && responseView.csv + // MyInfo fields + if (isMyInfoableFieldType(field) && field.myInfo) { + if (field.myInfo.verified) return `[MyInfo] ${field.title}` + return field.title + } + + // Basic fields + if (field.fieldType === BasicField.Table) { + // Delegate the work to the table handler + return TABLE_HANDLER.getName(field, responseView) + } + if (field.fieldType === BasicField.Attachment) { + switch (responseView.mode) { + case FormResponseMode.Email: + return `[attachment] ${field.title}` + case FormResponseMode.Encrypt: + return field.title + } + } + if (isVerifiableFieldType(field)) { + if (!isCsv && field.isVerifiable && field.val) + return `[verified] ${field.title}` + } + return field.title +} + +/** + * Gets answers for a field as displayed in response email or decrypted submission. + * Always returns an array, so caller must always loop through result. + * @param {E2eFieldMetadata} field field used to create and fill form + * @param {FormResponseView} formMode form response mode + * @returns {string[] | null} string array of responses, or null if the field is a non-response field and should not be represented + */ +export const getResponseArray = ( + field: E2eFieldMetadata, + responseView: FormResponseView, +): string[] | null => { + const isCsv = + responseView.mode === FormResponseMode.Encrypt && responseView.csv + + // Deal with table first to avoid special cases later + if (field.fieldType === BasicField.Table) { + return TABLE_HANDLER.getValues(field, responseView) + } + + switch (field.fieldType) { + case BasicField.Section: + return isCsv ? null : [''] + case BasicField.Image: + case BasicField.Statement: + return null + case BasicField.Radio: + case BasicField.Checkbox: + if (!field.val || (field.val instanceof Array && !field.val.length)) { + return [''] + } + return [ + (field.val instanceof Array ? field.val : [field.val]) + .map((selected) => { + return field.fieldOptions.includes(selected) + ? selected + : `Others: ${selected}` + }) + .join(isCsv ? ';' : ', '), + ] + case BasicField.Date: + // Need to re-parse, because input is in dd/mm/yyyy format whereas response is in dd MMM yyyy format. + return [ + field.val + ? format( + parse(field.val, DATE_INPUT_FORMAT, new Date()), + DATE_RESPONSE_FORMAT, + ) + : '', + ] + case BasicField.HomeNo: + return [field.val && `+65${field.val}`] + default: + return [field.val] + } +} + +/** + * Tests that container contains all the values in contained. + * @param {string} container string in which to search + * @param {string[]} containedArray Array of values to search for + */ +export const expectContains = + (container: string) => (containedArray: string[]) => { + for (const contained of containedArray) { + expect(container).toContain(contained) + } + } + +/** + * Checks that an attachment field's attachment is contained in the email. + * @param {E2eFieldMetadata} field field used to create and fill form + * @param {Record} attachments map of attachment names to content + */ +export const expectAttachment = ( + field: E2eFieldMetadata, + attachments: Record, +): void => { + if (field.fieldType !== BasicField.Attachment) return + + // Attachments will not exist if it is unfilled. + if (!field.val) return + + const content = attachments[field.val] + + // Check that contents match + expect(content).toEqual(readFileSync(field.path).toString()) +}