diff --git a/src/pages/dataElements/New.spec.tsx b/src/pages/dataElements/New.spec.tsx new file mode 100644 index 00000000..0e3c23d5 --- /dev/null +++ b/src/pages/dataElements/New.spec.tsx @@ -0,0 +1,196 @@ +import { RenderResult, act, fireEvent, render, waitFor } from '@testing-library/react' +import React from 'react' +import { RouterProvider, createMemoryRouter } from 'react-router-dom' +import dataElementSchemaMock from '../../__mocks__/schema/dataElementsSchema.json' +import { useSchemaStore } from '../../lib/schemas/schemaStore' +import { ModelSchemas } from '../../lib/useLoadApp' +import { ComponentWithProvider } from '../../testUtils/TestComponentWithRouter' +import attributes from './__mocks__/attributes.json' +import categoryCombosPage1 from './__mocks__/categoryCombosPage1.json' +import { Component as New } from './New' + +async function changeSingleSelect( + result: RenderResult, + selectLabel: string, + value: string +) { + const label = result.getByText(selectLabel) + expect(label).toBeTruthy() + + const field = label.parentNode?.parentNode as HTMLElement + expect(field).toBeTruthy() + + const trigger = field.querySelector('[data-test="dhis2-uicore-select-input"]') as HTMLElement + expect(trigger).toBeTruthy() + + fireEvent.click(trigger) + + await waitFor(() => { + expect(result.container.querySelector('[class*="layer"]')).toBeTruthy() + }) + + const optionSelector = `[data-value="${value}"]` + await waitFor(() => { + if (!result.container.querySelector(optionSelector)) { + throw new Error('Could not find option') + } + }) + + const optionElement = result.container.querySelector(optionSelector) as HTMLElement + fireEvent.click(optionElement) +} + +describe('Data Elements / New', () => { + useSchemaStore.getState().setSchemas({ + dataElement: dataElementSchemaMock, + } as unknown as ModelSchemas) + + const customData = { + attributes: attributes, + categoryCombos: categoryCombosPage1, + } + + it.only('should return to the list view when cancelling', async () => { + const List: React.FC = jest.fn(() =>
) + const router = createMemoryRouter( + [ + { path: '/new', element: }, + { path: '/dataElements', element: }, + ], + { initialEntries: ['/new'] } + ) + + const result = render( + + + + ) + + await waitFor(() => { + expect(result.queryByText('Exit without saving')).toBeTruthy() + }) + + expect(List).toHaveBeenCalledTimes(0) + + const cancelButton = result.queryByText('Exit without saving', { + selector: 'button', + }) + fireEvent.click(cancelButton as HTMLButtonElement) + + expect(List).toHaveBeenCalledTimes(1) + }) + + it('should not submit when required values are missing', async () => { + const router = createMemoryRouter([{ path: '/', element: }]) + const result = render( + + + + ) + + await waitFor(() => { + expect(result.queryByText('Create data element')).toBeTruthy() + }) + + const submitButton = result.queryByText('Create data element', { + selector: 'button', + }) + fireEvent.click(submitButton as HTMLButtonElement) + + await waitFor(() => { + expect(result.container.querySelector('.error')).toBeTruthy() + }) + + expect( + result.container.querySelectorAll( + '.error[data-test*="validation"]' + ).length + ).toBe(4) + + expect( + result.container.querySelector('label[for="name"] + div .error') + ).toBeTruthy() + + expect( + result.container.querySelector( + 'label[for="shortName"] + div .error' + ) + ).toBeTruthy() + + expect( + result.container.querySelector( + 'label[for="valueType"] + div .error' + ) + ).toBeTruthy() + + expect( + result.container.querySelector( + 'label[for="aggregationType"] + div .error' + ) + ).toBeTruthy() + }) + + it('should submit the data and return to the list view on success', async () => { + const List: React.FC = jest.fn(() =>
) + const dataElementCustomData = jest.fn(() => Promise.resolve({})) + const router = createMemoryRouter( + [ + { path: '/new', element: }, + { path: '/dataElements', element: }, + ], + { + initialIndex: 0, + initialEntries: ['/new'], + } + ) + const result = render( + <> +
+ + + + + ) + + await waitFor(() => { + expect(result.queryByText('Create data element')).toBeTruthy() + }) + + act(() => { + fireEvent.change( + result.getByLabelText('Name (required)*') as HTMLElement, + { target: { value: 'Data element name' } } + ) + }) + + act(() => { + fireEvent.change( + result.getByLabelText('Short name (required)*') as HTMLElement, + { target: { value: 'Data element name' } } + ) + }) + + await act(async () => { + await changeSingleSelect(result, 'Value type (required)', 'TEXT') + }) + + await act(async () => { + await changeSingleSelect(result, 'Aggregation type (required)', 'SUM') + }) + + act(() => { + fireEvent.click( + result.queryByText('Create data element', { + selector: 'button', + }) as HTMLButtonElement + ) + }) + + await waitFor(() => { + expect(List).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/src/pages/dataElements/New.tsx b/src/pages/dataElements/New.tsx index c4b7d3c3..a6ddf2ea 100644 --- a/src/pages/dataElements/New.tsx +++ b/src/pages/dataElements/New.tsx @@ -91,12 +91,12 @@ export const Component = () => { if (error && !loading) { // @TODO(Edit): Implement error screen - return `Error: ${error.toString()}` + return <>Error: {error.toString()} } if (loading) { // @TODO(Edit): Implement loading screen - return 'Loading...' + return <>Loading... } const initialValues = computeInitialValues(customAttributesQuery.data) diff --git a/src/pages/dataElements/__mocks__/attributes.json b/src/pages/dataElements/__mocks__/attributes.json new file mode 100644 index 00000000..2b550e4c --- /dev/null +++ b/src/pages/dataElements/__mocks__/attributes.json @@ -0,0 +1,66 @@ +[ + { + "valueType": "TEXT", + "mandatory": false, + "optionSet": { + "options": [ + { + "code": "INPUT", + "name": "Input", + "displayName": "Input", + "id": "Fr4igNnPYTv" + }, + { + "code": "ACTIVITY", + "name": "Activity", + "displayName": "Activity", + "id": "xuAA8BQjdZq" + }, + { + "code": "OUTPUT", + "name": "Output", + "displayName": "Output", + "id": "tpJ71rNdmeo" + }, + { + "code": "IMPACT", + "name": "Impact", + "displayName": "Impact", + "id": "QIbgv5M77In" + } + ] + }, + "displayFormName": "Classification", + "id": "Z4X3J7jMLYV" + }, + { + "valueType": "LONG_TEXT", + "mandatory": false, + "displayFormName": "Collection method", + "id": "qXS2NDUEAOS" + }, + { + "valueType": "TEXT", + "mandatory": false, + "displayFormName": "NGO ID", + "id": "n2xYlNbsfko" + }, + { + "valueType": "TEXT", + "mandatory": false, + "displayFormName": "PEPFAR ID", + "id": "dLHLR5O4YFI" + }, + { + "valueType": "TEXT", + "mandatory": false, + "displayFormName": "Rationale", + "id": "AhsCAtM3L0g" + }, + { + "valueType": "TEXT", + "mandatory": false, + "displayFormName": "Unit of measure", + "id": "Y1LUDU8sWBR" + } +] diff --git a/src/pages/dataElements/__mocks__/categoryCombosPage1.json b/src/pages/dataElements/__mocks__/categoryCombosPage1.json new file mode 100644 index 00000000..f3ae4338 --- /dev/null +++ b/src/pages/dataElements/__mocks__/categoryCombosPage1.json @@ -0,0 +1,45 @@ +{ + "pager": { + "page": 1, + "total": 23, + "pageSize": 10, + "nextPage": "https://debug.dhis2.org/dev/api/41/categoryCombos?page=2&pageSize=10&fields=id%2CdisplayName%2CisDefault&order=isDefault%3Adesc%2CdisplayName", + "pageCount": 3 + }, + "categoryCombos": [ + { "displayName": "default", "id": "bjDvmb4bfuf", "isDefault": true }, + { "displayName": "Births", "id": "m2jTvAj5kkm", "isDefault": false }, + { + "displayName": "Commodities", + "id": "gbvX3pogf7p", + "isDefault": false + }, + { + "displayName": "Fixed/Outreach", + "id": "KYYSTxeVw28", + "isDefault": false + }, + { "displayName": "Gender", "id": "dPmavA0qloX", "isDefault": false }, + { + "displayName": "HIV Paed age+gender", + "id": "v1K6CE6bmtw", + "isDefault": false + }, + { "displayName": "HIV age", "id": "Wfan7UwK8CQ", "isDefault": false }, + { + "displayName": "HIV age+gender", + "id": "jCNGsC2NawV", + "isDefault": false + }, + { + "displayName": "Implementing Partner", + "id": "nM3u9s5a52V", + "isDefault": false + }, + { + "displayName": "Implementing Partner and Projects", + "id": "O4VaNks6tta", + "isDefault": false + } + ] +} diff --git a/src/pages/dataElements/form/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index 9a826d7b..d5d81cb2 100644 --- a/src/pages/dataElements/form/DataElementFormFields.tsx +++ b/src/pages/dataElements/form/DataElementFormFields.tsx @@ -15,7 +15,7 @@ import { CategoryComboField, ColorAndIconField, DomainField, - LegendSetField, + // LegendSetField, OptionSetField, OptionSetCommentField, ValueTypeField, @@ -62,6 +62,7 @@ export function DataElementFormFields() { helpText={i18n.t( 'A data element name should be concise and easy to recognize.' )} + validate={(value) => (!value ? 'Required' : undefined)} /> @@ -69,6 +70,7 @@ export function DataElementFormFields() { (!value ? 'Required' : undefined)} inputWidth="400px" name="shortName" label={i18n.t('Short name (required)')} @@ -194,7 +196,7 @@ export function DataElementFormFields() { - + {/* */ ''} diff --git a/src/pages/dataElements/form/customFields.tsx b/src/pages/dataElements/form/customFields.tsx index 242f16ae..225247a8 100644 --- a/src/pages/dataElements/form/customFields.tsx +++ b/src/pages/dataElements/form/customFields.tsx @@ -48,8 +48,17 @@ export function ColorAndIconField() { export function DomainField() { const name = 'domainType' - const aggregateInput = useField(name, { type: 'radio', value: 'AGGREGATE' }) - const trackerInput = useField(name, { type: 'radio', value: 'TRACKER' }) + const validate = (value: string) => (!value ? 'Required' : undefined) + const aggregateInput = useField(name, { + type: 'radio', + value: 'AGGREGATE', + validate, + }) + const trackerInput = useField(name, { + type: 'radio', + value: 'TRACKER', + validate, + }) const error = aggregateInput.meta.error || trackerInput.meta.error return ( @@ -157,11 +166,11 @@ export function ValueTypeField() { label: VALUE_TYPE[constant as keyof typeof VALUE_TYPE], }) ) - return ( (!value ? 'Required' : undefined)} inputWidth="400px" name="valueType" label={i18n.t('Value type (required)')} @@ -184,6 +193,7 @@ export function AggregationTypeField() { (!value ? 'Required' : undefined)} inputWidth="400px" name="aggregationType" label={i18n.t('Aggregation type (required)')} @@ -212,6 +222,9 @@ export function CategoryComboField() {
+ !value ? 'Required' : undefined + } name="categoryCombo.id" label={i18n.t('Category combination (required)')} helpText={i18n.t( diff --git a/src/testUtils/index.ts b/src/testUtils/index.ts new file mode 100644 index 00000000..14013649 --- /dev/null +++ b/src/testUtils/index.ts @@ -0,0 +1 @@ +export * from './TestComponentWithRouter'