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'