From e7021116721845e8c00659d9aee6d159dbeefc77 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Thu, 9 Mar 2023 11:47:44 +0100 Subject: [PATCH 01/13] Passing node object to component renderer from GenericComponent (this means overrides might disappear, which might break Likert) --- src/layout/Address/AddressComponent.test.tsx | 217 ++++++++++------- src/layout/Address/AddressComponent.tsx | 8 +- .../AttachmentListComponent.test.tsx | 93 +++----- .../AttachmentListComponent.tsx | 10 +- src/layout/Button/ButtonComponent.test.tsx | 50 ++-- src/layout/Button/ButtonComponent.tsx | 15 +- src/layout/Button/GoToTaskButton.test.tsx | 63 +++-- src/layout/Button/GoToTaskButton.tsx | 3 +- .../CheckboxesContainerComponent.test.tsx | 192 ++++++++------- .../CheckboxesContainerComponent.tsx | 10 +- src/layout/Custom/CustomWebComponent.test.tsx | 87 ++++--- src/layout/Custom/CustomWebComponent.tsx | 14 +- .../Datepicker/DatepickerComponent.test.tsx | 48 ++-- src/layout/Datepicker/DatepickerComponent.tsx | 16 +- .../Dropdown/DropdownComponent.test.tsx | 93 +++++--- src/layout/Dropdown/DropdownComponent.tsx | 8 +- .../FileUpload/FileUploadComponent.test.tsx | 91 +++----- src/layout/FileUpload/FileUploadComponent.tsx | 29 ++- .../FileUploadWithTag/EditWindowComponent.tsx | 13 +- .../FileUploadWithTag/FileListComponent.tsx | 11 +- .../FileUploadWithTagComponent.test.tsx | 184 +++++++-------- .../FileUploadWithTagComponent.tsx | 32 +-- src/layout/GenericComponent.tsx | 5 +- src/layout/Header/HeaderComponent.test.tsx | 56 +++-- src/layout/Header/HeaderComponent.tsx | 3 +- src/layout/Image/ImageComponent.tsx | 24 +- src/layout/Input/InputComponent.test.tsx | 70 +++--- src/layout/Input/InputComponent.tsx | 16 +- .../InstanceInformationComponent.tsx | 3 +- .../InstantiationButton.test.tsx | 56 ++--- .../InstantiationButton.tsx | 4 +- .../InstantiationButtonComponent.tsx | 12 +- src/layout/LayoutComponent.tsx | 11 +- src/layout/Likert/LikertComponent.tsx | 13 +- src/layout/Likert/index.tsx | 2 +- src/layout/List/ListComponent.test.tsx | 64 ++--- src/layout/List/ListComponent.tsx | 7 +- src/layout/Map/MapComponent.test.tsx | 65 +++--- src/layout/Map/MapComponent.tsx | 12 +- .../MultipleSelectComponent.test.tsx | 74 +++--- .../MultipleSelectComponent.tsx | 8 +- .../NavigationBar/NavigationBarComponent.tsx | 3 +- .../NavigationButtonsComponent.test.tsx | 219 ++++++++---------- .../NavigationButtonsComponent.tsx | 21 +- src/layout/Panel/PanelComponent.tsx | 3 +- .../Paragraph/ParagraphComponent.test.tsx | 58 +++-- src/layout/Paragraph/ParagraphComponent.tsx | 19 +- .../RadioButtons/ControlledRadioGroup.tsx | 5 +- .../RadioButtonsContainerComponent.test.tsx | 184 ++++++++------- .../RadioButtonsContainerComponent.tsx | 2 +- src/layout/RadioButtons/radioButtonsUtils.ts | 11 +- .../TextArea/TextAreaComponent.test.tsx | 51 ++-- src/layout/TextArea/TextAreaComponent.tsx | 12 +- src/layout/index.ts | 10 +- src/layout/layout.d.ts | 7 +- src/testUtils.tsx | 56 ++++- 56 files changed, 1246 insertions(+), 1207 deletions(-) diff --git a/src/layout/Address/AddressComponent.test.tsx b/src/layout/Address/AddressComponent.test.tsx index bf13878447..4b7863aa8a 100644 --- a/src/layout/Address/AddressComponent.test.tsx +++ b/src/layout/Address/AddressComponent.test.tsx @@ -1,17 +1,14 @@ import React from 'react'; -import { Provider } from 'react-redux'; -import { act, fireEvent, render as rtlRender, screen } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import mockAxios from 'jest-mock-axios'; -import configureStore from 'redux-mock-store'; import { AddressComponent } from 'src/layout/Address/AddressComponent'; -import { mockComponentProps } from 'src/testUtils'; -import type { IAddressComponentProps } from 'src/layout/Address/AddressComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; -const render = (props: Partial = {}) => { - const createStore = configureStore(); +const render = ({ component, genericProps }: Partial> = {}) => { const mockLanguage = { ux_editor: { modal_configure_address_component_address: 'Adresse', @@ -27,28 +24,26 @@ const render = (props: Partial = {}) => { }, }; - const allProps: IAddressComponentProps = { - ...mockComponentProps, - formData: { - address: 'adresse 1', + renderGenericComponentTest({ + type: 'AddressComponent', + renderer: (props) => , + component: { + simplified: true, + dataModelBindings: {}, + readOnly: false, + required: false, + textResourceBindings: {}, + ...component, }, - isValid: true, - simplified: true, - dataModelBindings: {}, - language: mockLanguage, - readOnly: false, - required: false, - textResourceBindings: {}, - ...props, - }; - - const mockStore = createStore({ language: { language: mockLanguage } }); - - rtlRender( - - - , - ); + genericProps: { + formData: { + address: 'adresse 1', + }, + isValid: true, + language: mockLanguage, + ...genericProps, + }, + }); }; const getField = ({ method, regex }) => @@ -125,7 +120,9 @@ describe('AddressComponent', () => { it('should return simplified version when simplified is true', () => { render({ - simplified: true, + component: { + simplified: true, + }, }); expect(getAddressField()).toBeInTheDocument(); @@ -138,7 +135,9 @@ describe('AddressComponent', () => { it('should return complex version when simplified is false', () => { render({ - simplified: false, + component: { + simplified: false, + }, }); expect(getAddressField()).toBeInTheDocument(); @@ -152,11 +151,15 @@ describe('AddressComponent', () => { const handleDataChange = jest.fn(); render({ - formData: { - address: '', + component: { + simplified: false, + }, + genericProps: { + formData: { + address: '', + }, + handleDataChange, }, - simplified: false, - handleDataChange, }); const address = getAddressField(); @@ -174,12 +177,16 @@ describe('AddressComponent', () => { const handleDataChange = jest.fn(); render({ - formData: { - address: 'initial address', + genericProps: { + formData: { + address: 'initial address', + }, + handleDataChange, + }, + component: { + simplified: false, + readOnly: true, }, - simplified: false, - readOnly: true, - handleDataChange, }); const address = getAddressField(); @@ -194,12 +201,16 @@ describe('AddressComponent', () => { it('should show error message on blur if zipcode is invalid, and not call handleDataChange', async () => { const handleDataChange = jest.fn(); render({ - formData: { - address: 'a', + component: { + required: true, + simplified: false, + }, + genericProps: { + formData: { + address: 'a', + }, + handleDataChange, }, - required: true, - simplified: false, - handleDataChange, }); const field = getZipCodeField({ required: true }); @@ -217,13 +228,17 @@ describe('AddressComponent', () => { it('should update postplace on mount', async () => { const handleDataChange = jest.fn(); render({ - formData: { - address: 'a', - zipCode: '0001', + component: { + required: true, + simplified: false, + }, + genericProps: { + formData: { + address: 'a', + zipCode: '0001', + }, + handleDataChange, }, - required: true, - simplified: false, - handleDataChange, }); mockAxios.mockResponseFor( @@ -245,14 +260,18 @@ describe('AddressComponent', () => { const handleDataChange = jest.fn(); render({ - formData: { - address: 'a', - zipCode: '1', - postPlace: '', + genericProps: { + formData: { + address: 'a', + zipCode: '1', + postPlace: '', + }, + handleDataChange, + }, + component: { + required: true, + simplified: false, }, - required: true, - simplified: false, - handleDataChange, }); const field = getZipCodeField({ required: true }); @@ -268,12 +287,14 @@ describe('AddressComponent', () => { it('should call handleDataChange for post place when zip code is cleared', async () => { const handleDataChange = jest.fn(); render({ - formData: { - address: 'a', - zipCode: '0001', - postPlace: 'Oslo', + genericProps: { + formData: { + address: 'a', + zipCode: '0001', + postPlace: 'Oslo', + }, + handleDataChange, }, - handleDataChange, }); expect(screen.getByDisplayValue('0001')).toBeInTheDocument(); @@ -293,16 +314,20 @@ describe('AddressComponent', () => { const errorMessage = 'cannot be empty;'; const handleDataChange = jest.fn(); render({ - formData: { - address: '', - }, - required: true, - simplified: false, - handleDataChange, - componentValidations: { - address: { - errors: [errorMessage], + genericProps: { + formData: { + address: '', }, + handleDataChange, + componentValidations: { + address: { + errors: [errorMessage], + }, + }, + }, + component: { + required: true, + simplified: false, }, }); @@ -311,8 +336,10 @@ describe('AddressComponent', () => { it('should display no extra markings when required is false, and labelSettings.optionalIndicator is not true', () => { render({ - required: false, - simplified: false, + component: { + required: false, + simplified: false, + }, }); expect(getAddressField()).toBeInTheDocument(); expect(getZipCodeField()).toBeInTheDocument(); @@ -323,8 +350,10 @@ describe('AddressComponent', () => { it('should display required labels when required is true', () => { render({ - required: true, - simplified: false, + component: { + required: true, + simplified: false, + }, }); expect(getAddressField({ required: true })).toBeInTheDocument(); @@ -341,9 +370,11 @@ describe('AddressComponent', () => { it('should display optional labels when optionalIndicator is true', () => { render({ - simplified: false, - labelSettings: { - optionalIndicator: true, + component: { + simplified: false, + labelSettings: { + optionalIndicator: true, + }, }, }); @@ -361,7 +392,9 @@ describe('AddressComponent', () => { it('should not display optional labels by default', () => { render({ - simplified: false, + component: { + simplified: false, + }, }); expect(getAddressField({ useQuery: true, optional: true })).not.toBeInTheDocument(); @@ -378,8 +411,10 @@ describe('AddressComponent', () => { it('should not display optional labels when readonly is true', () => { render({ - readOnly: true, - simplified: false, + component: { + readOnly: true, + simplified: false, + }, }); expect(getAddressField()).toBeInTheDocument(); @@ -396,10 +431,12 @@ describe('AddressComponent', () => { it('should not display optional labels when readonly is true, even when optionalIndicator is true', () => { render({ - readOnly: true, - simplified: false, - labelSettings: { - optionalIndicator: true, + component: { + readOnly: true, + simplified: false, + labelSettings: { + optionalIndicator: true, + }, }, }); @@ -417,10 +454,12 @@ describe('AddressComponent', () => { it('should not display optional labels when required is true, even when optionalIndicator is true', () => { render({ - required: true, - simplified: false, - labelSettings: { - optionalIndicator: true, + component: { + required: true, + simplified: false, + labelSettings: { + optionalIndicator: true, + }, }, }); diff --git a/src/layout/Address/AddressComponent.tsx b/src/layout/Address/AddressComponent.tsx index 0c2c251cab..a0c9fb4228 100644 --- a/src/layout/Address/AddressComponent.tsx +++ b/src/layout/Address/AddressComponent.tsx @@ -35,16 +35,12 @@ export function AddressComponent({ language, handleDataChange, componentValidations, - id, - required, - readOnly, - labelSettings, - simplified, - saveWhileTyping, + node, }: IAddressComponentProps) { // eslint-disable-next-line import/no-named-as-default-member const cancelToken = axios.CancelToken; const source = cancelToken.source(); + const { id, required, readOnly, labelSettings, simplified, saveWhileTyping } = node.item; const handleDataChangeOverride = (key: AddressKeys): IAddressComponentProps['handleDataChange'] => diff --git a/src/layout/AttachmentList/AttachmentListComponent.test.tsx b/src/layout/AttachmentList/AttachmentListComponent.test.tsx index 81233acc12..78aa2633a0 100644 --- a/src/layout/AttachmentList/AttachmentListComponent.test.tsx +++ b/src/layout/AttachmentList/AttachmentListComponent.test.tsx @@ -2,75 +2,46 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { applicationMetadataMock } from 'src/__mocks__/applicationMetadataMock'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; -import { getInstanceDataStateMock } from 'src/__mocks__/instanceDataStateMock'; import { AttachmentListComponent } from 'src/layout/AttachmentList/AttachmentListComponent'; -import { renderWithProviders } from 'src/testUtils'; -import type { IAttachmentListProps } from 'src/layout/AttachmentList/AttachmentListComponent'; -import type { IInstanceDataState } from 'src/shared/resources/instanceData'; +import { renderGenericComponentTest } from 'src/testUtils'; import type { IData } from 'src/types/shared'; describe('FileUploadComponent', () => { it('should render default AttachmentList component', () => { - render({ text: 'Attachments' }); + render(); expect(screen.getByText('Attachments')).toBeInTheDocument(); }); }); -function render(props: Partial = {}) { - const mockId = 'mockId'; - - const mockApplicationMetadata = applicationMetadataMock; - const instanceDataMock: IInstanceDataState = getInstanceDataStateMock(); - if (!instanceDataMock.instance) { - throw new Error('Missing data in mock'); - } - - const dataElement: IData = { - id: 'test-data-element-1', - instanceGuid: instanceDataMock.instance.id, - dataType: 'test-data-type-1', - filename: 'testData1.pdf', - contentType: 'application/pdf', - blobStoragePath: '', - size: 1234, - locked: false, - refs: [], - created: new Date('2021-01-01').toISOString(), - createdBy: 'testUser', - lastChanged: new Date('2021-01-01').toISOString(), - lastChangedBy: 'testUser', - }; - const mockInstanceData = { - ...instanceDataMock, - }; - - if (mockInstanceData.instance) { - mockInstanceData.instance.data = [dataElement]; - } - - const mockInitialState = getInitialStateMock({ - applicationMetadata: { - applicationMetadata: mockApplicationMetadata, - error: null, +const render = () => { + renderGenericComponentTest({ + type: 'AttachmentList', + renderer: (props) => , + component: { + dataTypeIds: ['test-data-type-1'], }, - instanceData: mockInstanceData, - }); - - const defaultProps = { - id: mockId, - text: 'Attachments', - dataTypeIds: ['test-data-type-1'], - } as IAttachmentListProps; - - return renderWithProviders( - , - { - preloadedState: mockInitialState, + genericProps: { + text: 'Attachments', }, - ); -} + manipulateState: (state) => { + if (state.instanceData.instance) { + const dataElement: IData = { + id: 'test-data-element-1', + instanceGuid: state.instanceData.instance.id, + dataType: 'test-data-type-1', + filename: 'testData1.pdf', + contentType: 'application/pdf', + blobStoragePath: '', + size: 1234, + locked: false, + refs: [], + created: new Date('2021-01-01').toISOString(), + createdBy: 'testUser', + lastChanged: new Date('2021-01-01').toISOString(), + lastChangedBy: 'testUser', + }; + state.instanceData.instance.data = [dataElement]; + } + }, + }); +}; diff --git a/src/layout/AttachmentList/AttachmentListComponent.tsx b/src/layout/AttachmentList/AttachmentListComponent.tsx index e323f669a5..640d9d6caa 100644 --- a/src/layout/AttachmentList/AttachmentListComponent.tsx +++ b/src/layout/AttachmentList/AttachmentListComponent.tsx @@ -9,20 +9,20 @@ import type { PropsFromGenericComponent } from 'src/layout'; export type IAttachmentListProps = PropsFromGenericComponent<'AttachmentList'>; -export function AttachmentListComponent(props: IAttachmentListProps) { +export function AttachmentListComponent({ node, text }: IAttachmentListProps) { + const { dataTypeIds, includePDF } = node.item; const currentTaskId = useAppSelector((state) => state.instanceData.instance?.process.currentTask?.elementId); const dataForTask = useAppSelector((state) => { const dataTypes = state.applicationMetadata.applicationMetadata?.dataTypes.filter((type) => { return type.taskId === state.instanceData.instance?.process.currentTask?.elementId; }); return state.instanceData.instance?.data.filter((dataElement) => { - if (props.dataTypeIds) { - return props.dataTypeIds.findIndex((id) => dataElement.dataType === id) > -1; + if (dataTypeIds) { + return dataTypeIds.findIndex((id) => dataElement.dataType === id) > -1; } return dataTypes && dataTypes.findIndex((type) => dataElement.dataType === type.id) > -1; }); }); - const includePDF = props.includePDF; const attachments = useAppSelector((state) => { const appLogicDataTypes = state.applicationMetadata.applicationMetadata?.dataTypes.filter((dataType) => { return dataType.appLogic && dataType.taskId === currentTaskId; @@ -41,7 +41,7 @@ export function AttachmentListComponent(props: IAttachmentListProps) { className='attachmentList-title' component='span' > - {props.text || ''} + {text || ''} diff --git a/src/layout/Button/ButtonComponent.test.tsx b/src/layout/Button/ButtonComponent.test.tsx index 0051b3ddc4..3612fe6ff1 100644 --- a/src/layout/Button/ButtonComponent.test.tsx +++ b/src/layout/Button/ButtonComponent.test.tsx @@ -2,55 +2,43 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { ButtonComponent } from 'src/layout/Button/ButtonComponent'; -import { renderWithProviders } from 'src/testUtils'; -import type { IButtonProvidedProps } from 'src/layout/Button/ButtonComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; const submitBtnText = 'Submit form'; describe('ButtonComponent', () => { it('should render button when submittingId is falsy', () => { - render({ submittingId: '' }); + render(''); expect(screen.getByRole('button', { name: submitBtnText })).toBeInTheDocument(); expect(screen.queryByText('general.loading')).not.toBeInTheDocument(); }); it('should render loader when submittingId is truthy', () => { - render({ submittingId: 'some-id' }); + render('some-id'); expect(screen.queryByRole('button')).toBeInTheDocument(); expect(screen.getByText('general.loading')).toBeInTheDocument(); }); }); -const render = ({ submittingId }: { submittingId: string }) => { - const initialState = getInitialStateMock(); - const preloadedState = { - ...initialState, - formData: { - ...initialState.formData, - submittingId, +const render = (submittingId: string) => { + renderGenericComponentTest({ + type: 'Button', + renderer: (props) => , + component: { + id: 'some-id', + disabled: false, }, - formLayout: { - ...initialState.formLayout, - uiConfig: { - ...initialState.formLayout.uiConfig, - autoSave: true, - }, + genericProps: { + text: submitBtnText, + handleDataChange: jest.fn(), + language: {}, }, - }; - - renderWithProviders( - , - { preloadedState }, - ); + manipulateState: (state) => { + state.formData.submittingId = submittingId; + state.formLayout.uiConfig.autoSave = true; + }, + }); }; diff --git a/src/layout/Button/ButtonComponent.tsx b/src/layout/Button/ButtonComponent.tsx index ea6a59b967..793de4948c 100644 --- a/src/layout/Button/ButtonComponent.tsx +++ b/src/layout/Button/ButtonComponent.tsx @@ -11,10 +11,17 @@ import { SubmitButton } from 'src/layout/Button/SubmitButton'; import { ProcessActions } from 'src/shared/resources/process/processSlice'; import type { PropsFromGenericComponent } from 'src/layout'; import type { IAltinnWindow } from 'src/types'; +import type { HComponent } from 'src/utils/layout/hierarchy.types'; -export type IButtonProvidedProps = PropsFromGenericComponent<'Button'>; +export type IButtonReceivedProps = PropsFromGenericComponent<'Button'>; +export type IButtonProvidedProps = + | (PropsFromGenericComponent<'Button'> & HComponent<'Button'>) + | (PropsFromGenericComponent<'InstantiationButton'> & HComponent<'InstantiationButton'>); + +export const ButtonComponent = ({ node, ...componentProps }: IButtonReceivedProps) => { + const { id, mode } = node.item; + const props: IButtonProvidedProps = { ...componentProps, ...node.item, node }; -export const ButtonComponent = ({ mode, ...props }: IButtonProvidedProps) => { const dispatch = useAppDispatch(); const autoSave = useAppSelector((state) => state.formLayout.uiConfig.autoSave); const submittingId = useAppSelector((state) => state.formData.submittingId); @@ -73,8 +80,8 @@ export const ButtonComponent = ({ mode, ...props }: IButtonProvidedProps) => { )} submitTask({ componentId: props.id })} - id={props.id} + onClick={() => submitTask({ componentId: id })} + id={id} language={props.language} busyWithId={busyWithId} > diff --git a/src/layout/Button/GoToTaskButton.test.tsx b/src/layout/Button/GoToTaskButton.test.tsx index 02131d1355..19ef98cca6 100644 --- a/src/layout/Button/GoToTaskButton.test.tsx +++ b/src/layout/Button/GoToTaskButton.test.tsx @@ -3,57 +3,50 @@ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; -import { GoToTaskButton } from 'src/layout/Button/GoToTaskButton'; -import { setupStore } from 'src/store'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; -import type { IButtonProvidedProps } from 'src/layout/Button/ButtonComponent'; +import { ButtonComponent } from 'src/layout/Button/ButtonComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; -interface RenderProps { - props: Partial; - dispatch: (...args: any[]) => any; -} - -const render = ({ props, dispatch }: RenderProps) => { - const stateMock = getInitialStateMock(); - stateMock.process.availableNextTasks = ['a', 'b']; - const store = setupStore(stateMock); - - store.dispatch = dispatch; - - renderWithProviders( - - Go to task - , - { - store, +const render = ({ component, genericProps }: Partial> = {}) => { + let spy; + renderGenericComponentTest({ + type: 'Button', + renderer: (props) => , + component: { + mode: 'go-to-task', + ...component, + }, + genericProps: { + text: 'Go to task', + ...genericProps, }, - ); + manipulateState: (state) => { + state.process.availableNextTasks = ['a', 'b']; + }, + manipulateStore: (store) => { + spy = jest.spyOn(store, 'dispatch').mockImplementation(() => undefined); + }, + }); + + return spy; }; describe('GoToTaskButton', () => { it('should show button and it should be possible to click', async () => { - const dispatch = jest.fn(); - render({ - props: { + const dispatch = render({ + component: { taskId: 'a', }, - dispatch, }); expect(screen.getByText('Go to task')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button')); expect(dispatch).toHaveBeenCalled(); }); it('should show button and it should not be possible to click', async () => { - const dispatch = jest.fn(); - render({ - props: { + const dispatch = render({ + component: { taskId: 'c', }, - dispatch, }); expect(screen.getByText('Go to task')).toBeInTheDocument(); expect(screen.getByRole('button')).toBeDisabled(); diff --git a/src/layout/Button/GoToTaskButton.tsx b/src/layout/Button/GoToTaskButton.tsx index 83da230c85..e90c54ed4a 100644 --- a/src/layout/Button/GoToTaskButton.tsx +++ b/src/layout/Button/GoToTaskButton.tsx @@ -9,8 +9,9 @@ import { ProcessActions } from 'src/shared/resources/process/processSlice'; import { ProcessTaskType } from 'src/types'; import type { IButtonProvidedProps } from 'src/layout/Button/ButtonComponent'; -export const GoToTaskButton = ({ children, taskId, ...props }: React.PropsWithChildren) => { +export const GoToTaskButton = ({ children, ...props }: React.PropsWithChildren) => { const dispatch = useAppDispatch(); + const taskId = props.node.item.type === 'Button' ? props.node.item.taskId : undefined; const availableProcessTasks = useAppSelector((state) => state.process.availableNextTasks); const canGoToTask = availableProcessTasks && availableProcessTasks.includes(taskId || ''); const navigateToTask = () => { diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.test.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.test.tsx index ce00932f84..f2a69d075d 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.test.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.test.tsx @@ -2,15 +2,12 @@ import React from 'react'; import { act, fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import type { PreloadedState } from 'redux'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { CheckboxContainerComponent } from 'src/layout/Checkboxes/CheckboxesContainerComponent'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; +import { renderGenericComponentTest } from 'src/testUtils'; import { LayoutStyle } from 'src/types'; -import type { ICheckboxContainerProps } from 'src/layout/Checkboxes/CheckboxesContainerComponent'; import type { IOptionsState } from 'src/shared/resources/options'; -import type { RootState } from 'src/store'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; const threeOptions = [ { @@ -29,23 +26,29 @@ const threeOptions = [ const twoOptions = threeOptions.slice(1); -const render = (props: Partial = {}, customState: PreloadedState = {}) => { - const allProps: ICheckboxContainerProps = { - ...mockComponentProps, - options: [], - optionsId: 'countries', - preselectedOptionIndex: undefined, - legend: () => legend, - handleDataChange: jest.fn(), - getTextResource: (value) => value, - getTextResourceAsString: (value) => value, - ...props, - }; - - const { container } = renderWithProviders(, { - preloadedState: { - ...getInitialStateMock(), - optionState: { +interface Props extends Partial> { + optionState?: IOptionsState; +} + +const render = ({ component, genericProps, optionState }: Props = {}) => { + return renderGenericComponentTest({ + type: 'Checkboxes', + renderer: (props) => , + component: { + options: [], + optionsId: 'countries', + preselectedOptionIndex: undefined, + ...component, + }, + genericProps: { + legend: () => legend, + handleDataChange: jest.fn(), + getTextResource: (value) => value, + getTextResourceAsString: (value) => value, + ...genericProps, + }, + manipulateState: (state) => { + state.optionState = optionState || { options: { countries: { id: 'countries', @@ -62,12 +65,9 @@ const render = (props: Partial = {}, customState: Prelo message: '', }, loading: true, - }, - ...customState, + }; }, }); - - return { container }; }; const getCheckbox = ({ name, isChecked = false }) => { @@ -90,10 +90,14 @@ describe('CheckboxContainerComponent', () => { it('should call handleDataChange with value of preselectedOptionIndex when simpleBinding is not set', () => { const handleChange = jest.fn(); render({ - handleDataChange: handleChange, - preselectedOptionIndex: 1, - formData: { - simpleBinding: undefined, + component: { + preselectedOptionIndex: 1, + }, + genericProps: { + handleDataChange: handleChange, + formData: { + simpleBinding: undefined, + }, }, }); @@ -103,10 +107,14 @@ describe('CheckboxContainerComponent', () => { it('should not call handleDataChange when simpleBinding is set and preselectedOptionIndex', () => { const handleChange = jest.fn(); render({ - handleDataChange: handleChange, - preselectedOptionIndex: 0, - formData: { - simpleBinding: 'denmark', + component: { + preselectedOptionIndex: 0, + }, + genericProps: { + handleDataChange: handleChange, + formData: { + simpleBinding: 'denmark', + }, }, }); @@ -120,9 +128,11 @@ describe('CheckboxContainerComponent', () => { it('should show several checkboxes as selected based on values in simpleBinding', () => { const handleChange = jest.fn(); render({ - handleDataChange: handleChange, - formData: { - simpleBinding: 'norway,denmark', + genericProps: { + handleDataChange: handleChange, + formData: { + simpleBinding: 'norway,denmark', + }, }, }); @@ -135,7 +145,7 @@ describe('CheckboxContainerComponent', () => { it('should not set any as selected when no binding and no preselectedOptionIndex is set', () => { const handleChange = jest.fn(); - render({ handleDataChange: handleChange }); + render({ genericProps: { handleDataChange: handleChange } }); expect(getCheckbox({ name: 'Norway' })).toBeInTheDocument(); expect(getCheckbox({ name: 'Sweden' })).toBeInTheDocument(); @@ -147,9 +157,11 @@ describe('CheckboxContainerComponent', () => { it('should call handleDataChange with updated values when selection changes', async () => { const handleChange = jest.fn(); render({ - handleDataChange: handleChange, - formData: { - simpleBinding: 'norway', + genericProps: { + handleDataChange: handleChange, + formData: { + simpleBinding: 'norway', + }, }, }); @@ -169,9 +181,11 @@ describe('CheckboxContainerComponent', () => { it('should call handleDataChange with updated values when deselecting item', async () => { const handleChange = jest.fn(); render({ - handleDataChange: handleChange, - formData: { - simpleBinding: 'norway,denmark', + genericProps: { + handleDataChange: handleChange, + formData: { + simpleBinding: 'norway,denmark', + }, }, }); @@ -191,9 +205,11 @@ describe('CheckboxContainerComponent', () => { it('should call handleDataChange instantly on blur when the value has changed', async () => { const handleChange = jest.fn(); render({ - handleDataChange: handleChange, - formData: { - simpleBinding: 'norway', + genericProps: { + handleDataChange: handleChange, + formData: { + simpleBinding: 'norway', + }, }, }); @@ -213,7 +229,9 @@ describe('CheckboxContainerComponent', () => { it('should not call handleDataChange on blur when the value is unchanged', async () => { const handleChange = jest.fn(); render({ - handleDataChange: handleChange, + genericProps: { + handleDataChange: handleChange, + }, }); expect(getCheckbox({ name: 'Denmark' })).toBeInTheDocument(); @@ -229,9 +247,11 @@ describe('CheckboxContainerComponent', () => { it('should call handleDataChange onBlur with no commas in string when starting with empty string formData', async () => { const handleChange = jest.fn(); render({ - handleDataChange: handleChange, - formData: { - simpleBinding: '', + genericProps: { + handleDataChange: handleChange, + formData: { + simpleBinding: '', + }, }, }); @@ -250,7 +270,9 @@ describe('CheckboxContainerComponent', () => { it('should show spinner while waiting for options', () => { render({ - optionsId: 'loadingOptions', + component: { + optionsId: 'loadingOptions', + }, }); expect(screen.queryByTestId('altinn-spinner')).toBeInTheDocument(); @@ -258,8 +280,10 @@ describe('CheckboxContainerComponent', () => { it('should show items in a row when layout is "row" and options count is 3', () => { const { container } = render({ - optionsId: 'countries', - layout: LayoutStyle.Row, + component: { + optionsId: 'countries', + layout: LayoutStyle.Row, + }, }); expect(container.querySelectorAll('.MuiFormGroup-root').length).toBe(1); @@ -268,21 +292,19 @@ describe('CheckboxContainerComponent', () => { }); it('should show items in a row when layout is not defined, and options count is 2', () => { - const { container } = render( - { + const { container } = render({ + component: { optionsId: 'countries', }, - { - optionState: { - options: { - countries: { - id: 'countries', - options: twoOptions, - }, + optionState: { + options: { + countries: { + id: 'countries', + options: twoOptions, }, - } as unknown as IOptionsState, - }, - ); + }, + } as unknown as IOptionsState, + }); expect(container.querySelectorAll('.MuiFormGroup-root').length).toBe(1); @@ -290,22 +312,20 @@ describe('CheckboxContainerComponent', () => { }); it('should show items in a column when layout is "column" and options count is 2 ', () => { - const { container } = render( - { + const { container } = render({ + component: { optionsId: 'countries', layout: LayoutStyle.Column, }, - { - optionState: { - options: { - countries: { - id: 'countries', - options: twoOptions, - }, + optionState: { + options: { + countries: { + id: 'countries', + options: twoOptions, }, - } as unknown as IOptionsState, - }, - ); + }, + } as unknown as IOptionsState, + }); expect(container.querySelectorAll('.MuiFormGroup-root').length).toBe(1); @@ -314,7 +334,9 @@ describe('CheckboxContainerComponent', () => { it('should show items in a columns when layout is not defined, and options count is 3', () => { const { container } = render({ - optionsId: 'countries', + component: { + optionsId: 'countries', + }, }); expect(container.querySelectorAll('.MuiFormGroup-root').length).toBe(1); @@ -326,11 +348,13 @@ describe('CheckboxContainerComponent', () => { const handleDataChange = jest.fn(); render({ - handleDataChange, - source: { - group: 'someGroup', - label: 'option.from.rep.group.label', - value: 'someGroup[{0}].valueField', + genericProps: { handleDataChange }, + component: { + source: { + group: 'someGroup', + label: 'option.from.rep.group.label', + value: 'someGroup[{0}].valueField', + }, }, }); diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index 631703f3d9..a40ec1618b 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -80,21 +80,15 @@ const defaultOptions: IOption[] = []; const defaultSelectedOptions: string[] = []; export const CheckboxContainerComponent = ({ - id, - options, - optionsId, + node, formData, - preselectedOptionIndex, handleDataChange, - layout, legend, - readOnly, getTextResourceAsString, getTextResource, - mapping, - source, }: ICheckboxContainerProps) => { const classes = useStyles(); + const { id, options, optionsId, preselectedOptionIndex, layout, readOnly, mapping, source } = node.item; const apiOptions = useGetOptions({ optionsId, mapping, source }); const calculatedOptions = apiOptions || options || defaultOptions; const hasSelectedInitial = React.useRef(false); diff --git a/src/layout/Custom/CustomWebComponent.test.tsx b/src/layout/Custom/CustomWebComponent.test.tsx index 5732364363..c32d98fd83 100644 --- a/src/layout/Custom/CustomWebComponent.test.tsx +++ b/src/layout/Custom/CustomWebComponent.test.tsx @@ -1,21 +1,23 @@ import React from 'react'; +import { screen } from '@testing-library/react'; + import { CustomWebComponent } from 'src/layout/Custom/CustomWebComponent'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; -import type { ICustomComponentProps } from 'src/layout/Custom/CustomWebComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; import type { ITextResource } from 'src/types/shared'; const jsonAttributeValue = { customKey: 'customValue' }; describe('CustomWebComponent', () => { it('should render the component with the provided tag name', () => { - const screen = render({ tagName: 'test-component' }); + render({ component: { tagName: 'test-component' } }); const element = screen.getByTestId('test-component'); expect(element).toBeInTheDocument(); }); it('should stringify json values when passed to the dom', () => { - const screen = render({ tagName: 'test-component' }); + render({ component: { tagName: 'test-component' } }); const element = screen.getByTestId('test-component'); expect(element.id).toEqual('test-component'); expect(element.getAttribute('data-CustomAttributeWithJson')).toEqual(JSON.stringify(jsonAttributeValue)); @@ -23,43 +25,19 @@ describe('CustomWebComponent', () => { }); it('should render the component with passed props as attributes', () => { - const screen = render({ tagName: 'test-component' }); + render({ component: { tagName: 'test-component' } }); const element = screen.getByTestId('test-component'); expect(element.id).toEqual('test-component'); expect(element.getAttribute('text')).toEqual('Title'); }); it('should render nothing if the tag name is missing', () => { - const screen = render({ tagName: undefined }); + render({ component: { tagName: undefined } }); const element = screen.queryByTestId('test-component'); expect(element).not.toBeInTheDocument(); }); - const render = (providedProps?: Partial) => { - const allProps: ICustomComponentProps = { - ...mockComponentProps, - id: 'test-component', - tagName: '', - formData: { simpleBinding: 'This is a test' }, - dataModelBindings: { simpleBinding: 'model' }, - text: 'Title', - handleDataChange: (value: string) => value, - getTextResource: (key: string) => { - return key; - }, - getTextResourceAsString: (key: string) => { - return key; - }, - isValid: true, - language: {}, - shouldFocus: false, - textResourceBindings: { - title: 'title', - }, - 'data-CustomAttributeWithJson': jsonAttributeValue, - 'data-CustomAttributeWithReact': Hello world, - }; - + const render = ({ component }: Partial> = {}) => { const resources = [ { id: 'title', @@ -67,20 +45,41 @@ describe('CustomWebComponent', () => { }, ] as ITextResource[]; - return renderWithProviders( - , - { - preloadedState: { - textResources: { - language: 'nb', - resources, - error: null, - }, + renderGenericComponentTest({ + type: 'Custom', + renderer: (props) => , + component: { + id: 'test-component', + tagName: '', + dataModelBindings: { simpleBinding: 'model' }, + textResourceBindings: { + title: 'title', + }, + ...({ 'data-CustomAttributeWithJson': jsonAttributeValue } as any), + ...component, + }, + genericProps: { + formData: { simpleBinding: 'This is a test' }, + text: 'Title', + handleDataChange: (value: string) => value, + getTextResource: (key: string) => { + return key; }, + getTextResourceAsString: (key: string) => { + return key; + }, + isValid: true, + language: {}, + shouldFocus: false, + ...({ 'data-CustomAttributeWithReact': Hello world } as any), + }, + manipulateState: (state) => { + state.textResources = { + language: 'nb', + resources, + error: null, + }; }, - ); + }); }; }); diff --git a/src/layout/Custom/CustomWebComponent.tsx b/src/layout/Custom/CustomWebComponent.tsx index fb1b894502..de66b0fd93 100644 --- a/src/layout/Custom/CustomWebComponent.tsx +++ b/src/layout/Custom/CustomWebComponent.tsx @@ -11,16 +11,18 @@ export type ICustomComponentProps = PropsFromGenericComponent<'Custom'> & { }; export function CustomWebComponent({ - tagName, + node, formData, componentValidations, - textResourceBindings, - dataModelBindings, language, - hidden, handleDataChange, - ...passThroughProps + ...passThroughPropsFromGenericComponent }: ICustomComponentProps) { + const { tagName, textResourceBindings, dataModelBindings, ...passThroughPropsFromNode } = node.item; + const passThroughProps = { + ...passThroughPropsFromGenericComponent, + ...passThroughPropsFromNode, + }; const Tag = tagName; const wcRef = React.useRef(null); const textResources = useAppSelector((state) => state.textResources.resources); @@ -57,7 +59,7 @@ export function CustomWebComponent({ } }, [formData, componentValidations]); - if (hidden || !Tag || !textResources) { + if (node.isHidden() || !Tag || !textResources) { return null; } diff --git a/src/layout/Datepicker/DatepickerComponent.test.tsx b/src/layout/Datepicker/DatepickerComponent.test.tsx index fdf92ddb3b..9fb8a4185d 100644 --- a/src/layout/Datepicker/DatepickerComponent.test.tsx +++ b/src/layout/Datepicker/DatepickerComponent.test.tsx @@ -2,12 +2,10 @@ import React from 'react'; import { act, fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import type { PreloadedState } from 'redux'; import { DatepickerComponent } from 'src/layout/Datepicker/DatepickerComponent'; -import { mockComponentProps, mockMediaQuery, renderWithProviders } from 'src/testUtils'; -import type { IDatepickerProps } from 'src/layout/Datepicker/DatepickerComponent'; -import type { RootState } from 'src/store'; +import { mockMediaQuery, renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; // Mock dateformat jest.mock('src/utils/dateHelpers', () => { @@ -18,16 +16,16 @@ jest.mock('src/utils/dateHelpers', () => { }; }); -const render = (props: Partial = {}, customState: PreloadedState = {}) => { - const allProps: IDatepickerProps = { - ...mockComponentProps, - minDate: '1900-01-01T12:00:00.000Z', - maxDate: '2100-01-01T12:00:00.000Z', - ...props, - }; - - renderWithProviders(, { - preloadedState: customState, +const render = ({ component, genericProps }: Partial> = {}) => { + renderGenericComponentTest({ + type: 'Datepicker', + renderer: (props) => , + component: { + minDate: '1900-01-01T12:00:00.000Z', + maxDate: '2100-01-01T12:00:00.000Z', + ...component, + }, + genericProps, }); }; @@ -93,7 +91,7 @@ describe('DatepickerComponent', () => { it('should call handleDataChange when clicking date in calendar', async () => { const handleDataChange = jest.fn(); - render({ handleDataChange }); + render({ genericProps: { handleDataChange } }); await act(() => userEvent.click(getOpenCalendarButton())); await act(() => userEvent.click(getCalendarDayButton('15'))); @@ -107,7 +105,7 @@ describe('DatepickerComponent', () => { it('should call handleDataChange without skipping validation if date is cleared', async () => { const handleDataChange = jest.fn(); - render({ handleDataChange, formData: { simpleBinding: '2022-12-31' } }); + render({ genericProps: { handleDataChange, formData: { simpleBinding: '2022-12-31' } } }); const inputField = screen.getByRole('textbox'); @@ -122,7 +120,7 @@ describe('DatepickerComponent', () => { it('should call handleDataChange with formatted value (timestamp=true) without skipping validation if date is valid', async () => { const handleDataChange = jest.fn(); - render({ handleDataChange, timeStamp: true }); + render({ genericProps: { handleDataChange }, component: { timeStamp: true } }); const inputField = screen.getByRole('textbox'); @@ -137,7 +135,7 @@ describe('DatepickerComponent', () => { it('should call handleDataChange with formatted value (timestamp=false) without skipping validation if date is valid', async () => { const handleDataChange = jest.fn(); - render({ handleDataChange, timeStamp: false }); + render({ genericProps: { handleDataChange }, component: { timeStamp: false } }); const inputField = screen.getByRole('textbox'); @@ -152,7 +150,7 @@ describe('DatepickerComponent', () => { it('should call handleDataChange with formatted value (timestamp=undefined) without skipping validation if date is valid', async () => { const handleDataChange = jest.fn(); - render({ handleDataChange, timeStamp: undefined }); + render({ genericProps: { handleDataChange }, component: { timeStamp: undefined } }); const inputField = screen.getByRole('textbox'); @@ -167,7 +165,7 @@ describe('DatepickerComponent', () => { it('should call handleDataChange without skipping validation if date is invalid but finished filling out', async () => { const handleDataChange = jest.fn(); - render({ handleDataChange }); + render({ genericProps: { handleDataChange } }); const inputField = screen.getByRole('textbox'); @@ -182,7 +180,7 @@ describe('DatepickerComponent', () => { it('should call handleDataChange with skipValidation=true if not finished filling out the date', async () => { const handleDataChange = jest.fn(); - render({ handleDataChange }); + render({ genericProps: { handleDataChange } }); const inputField = screen.getByRole('textbox'); await act(async () => { @@ -196,15 +194,17 @@ describe('DatepickerComponent', () => { it('should have aria-describedby if textResourceBindings.description is present', () => { render({ - textResourceBindings: { description: 'description' }, - id: 'test-id', + component: { + textResourceBindings: { description: 'description' }, + id: 'test-id', + }, }); const inputField = screen.getByRole('textbox'); expect(inputField).toHaveAttribute('aria-describedby', 'description-test-id'); }); it('should not have aria-describedby if textResources.description does not exist', () => { - render({ textResourceBindings: {}, id: 'test-id' }); + render({ component: { textResourceBindings: {}, id: 'test-id' } }); const inputField = screen.getByRole('textbox'); expect(inputField).not.toHaveAttribute('aria-describedby'); }); diff --git a/src/layout/Datepicker/DatepickerComponent.tsx b/src/layout/Datepicker/DatepickerComponent.tsx index 52bef831b3..6a9d71e494 100644 --- a/src/layout/Datepicker/DatepickerComponent.tsx +++ b/src/layout/Datepicker/DatepickerComponent.tsx @@ -103,21 +103,9 @@ class AltinnMomentUtils extends MomentUtils { // We dont use the built-in validation for the 3rd party component, so it is always empty string const emptyString = ''; -export function DatepickerComponent({ - minDate, - maxDate, - format, - language, - formData, - timeStamp = true, - handleDataChange, - readOnly, - required, - id, - isValid, - textResourceBindings, -}: IDatepickerProps) { +export function DatepickerComponent({ node, language, formData, handleDataChange, isValid }: IDatepickerProps) { const classes = useStyles(); + const { minDate, maxDate, format, timeStamp = true, readOnly, required, id, textResourceBindings } = node.item; const calculatedMinDate = getDateConstraint(minDate, 'min'); const calculatedMaxDate = getDateConstraint(maxDate, 'max'); diff --git a/src/layout/Dropdown/DropdownComponent.test.tsx b/src/layout/Dropdown/DropdownComponent.test.tsx index 566fd0e145..1bc052c389 100644 --- a/src/layout/Dropdown/DropdownComponent.test.tsx +++ b/src/layout/Dropdown/DropdownComponent.test.tsx @@ -2,25 +2,12 @@ import React from 'react'; import { act, fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import type { PreloadedState } from 'redux'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { DropdownComponent } from 'src/layout/Dropdown/DropdownComponent'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; -import type { IDropdownProps } from 'src/layout/Dropdown/DropdownComponent'; -import type { RootState } from 'src/store'; - -const render = (props: Partial = {}, customState: PreloadedState = {}) => { - const allProps: IDropdownProps = { - ...mockComponentProps, - optionsId: 'countries', - readOnly: false, - handleDataChange: jest.fn(), - getTextResourceAsString: (value) => value, - isValid: true, - ...props, - }; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; +const render = ({ component, genericProps }: Partial> = {}) => { const countries = { id: 'countries', options: [ @@ -38,11 +25,22 @@ const render = (props: Partial = {}, customState: PreloadedState }, ], }; - - renderWithProviders(, { - preloadedState: { - ...getInitialStateMock(), - optionState: { + renderGenericComponentTest({ + type: 'Dropdown', + renderer: (props) => , + component: { + optionsId: 'countries', + readOnly: false, + ...component, + }, + genericProps: { + handleDataChange: jest.fn(), + getTextResourceAsString: (value) => value, + isValid: true, + ...genericProps, + }, + manipulateState: (state) => { + state.optionState = { options: { countries, loadingOptions: { @@ -56,9 +54,8 @@ const render = (props: Partial = {}, customState: PreloadedState message: '', }, loading: true, - }, + }; }, - ...customState, }); }; @@ -75,7 +72,9 @@ describe('DropdownComponent', () => { it('should trigger handleDataChange when option is selected', async () => { const handleDataChange = jest.fn(); render({ - handleDataChange, + genericProps: { + handleDataChange, + }, }); await act(() => user.selectOptions(screen.getByRole('combobox'), [screen.getByText('Sweden')])); @@ -89,7 +88,9 @@ describe('DropdownComponent', () => { it('should show as disabled when readOnly is true', () => { render({ - readOnly: true, + component: { + readOnly: true, + }, }); const select = screen.getByRole('combobox'); @@ -99,7 +100,9 @@ describe('DropdownComponent', () => { it('should not show as disabled when readOnly is false', () => { render({ - readOnly: false, + component: { + readOnly: false, + }, }); const select = screen.getByRole('combobox'); @@ -110,8 +113,12 @@ describe('DropdownComponent', () => { it('should trigger handleDataChange when preselectedOptionIndex is set', () => { const handleDataChange = jest.fn(); render({ - preselectedOptionIndex: 2, - handleDataChange, + component: { + preselectedOptionIndex: 2, + }, + genericProps: { + handleDataChange, + }, }); expect(handleDataChange).toHaveBeenCalledWith('denmark'); @@ -121,8 +128,12 @@ describe('DropdownComponent', () => { it('should trigger handleDataChange instantly on blur', async () => { const handleDataChange = jest.fn(); render({ - preselectedOptionIndex: 2, - handleDataChange, + component: { + preselectedOptionIndex: 2, + }, + genericProps: { + handleDataChange, + }, }); expect(handleDataChange).toHaveBeenCalledWith('denmark'); @@ -140,7 +151,9 @@ describe('DropdownComponent', () => { it('should show spinner while waiting for options', () => { render({ - optionsId: 'loadingOptions', + component: { + optionsId: 'loadingOptions', + }, }); expect(screen.queryByTestId('altinn-spinner')).toBeInTheDocument(); @@ -148,7 +161,9 @@ describe('DropdownComponent', () => { it('should not show spinner when options are present', () => { render({ - optionsId: 'countries', + component: { + optionsId: 'countries', + }, }); expect(screen.queryByTestId('altinn-spinner')).not.toBeInTheDocument(); @@ -157,11 +172,15 @@ describe('DropdownComponent', () => { it('should present replaced label if setup with values from repeating group in redux and trigger handleDataChanged with replaced values', async () => { const handleDataChange = jest.fn(); render({ - handleDataChange, - source: { - group: 'someGroup', - label: 'option.from.rep.group.label', - value: 'someGroup[{0}].valueField', + component: { + source: { + group: 'someGroup', + label: 'option.from.rep.group.label', + value: 'someGroup[{0}].valueField', + }, + }, + genericProps: { + handleDataChange, }, }); diff --git a/src/layout/Dropdown/DropdownComponent.tsx b/src/layout/Dropdown/DropdownComponent.tsx index 755f0fb0e0..0eaaa4aa8b 100644 --- a/src/layout/Dropdown/DropdownComponent.tsx +++ b/src/layout/Dropdown/DropdownComponent.tsx @@ -12,17 +12,13 @@ import type { PropsFromGenericComponent } from 'src/layout'; export type IDropdownProps = PropsFromGenericComponent<'Dropdown'>; export function DropdownComponent({ - optionsId, + node, formData, - preselectedOptionIndex, handleDataChange, - id, - readOnly, isValid, getTextResourceAsString, - mapping, - source, }: IDropdownProps) { + const { optionsId, preselectedOptionIndex, id, readOnly, mapping, source } = node.item; const options = useGetOptions({ optionsId, mapping, source }); const lookupKey = optionsId && getOptionLookupKey({ id: optionsId, mapping }); const fetchingOptions = useAppSelector((state) => lookupKey && state.optionState.options[lookupKey]?.loading); diff --git a/src/layout/FileUpload/FileUploadComponent.test.tsx b/src/layout/FileUpload/FileUploadComponent.test.tsx index b1c1b00a98..1a19e0f449 100644 --- a/src/layout/FileUpload/FileUploadComponent.test.tsx +++ b/src/layout/FileUpload/FileUploadComponent.test.tsx @@ -3,19 +3,18 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { getAttachments } from 'src/__mocks__/attachmentsMock'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { FileUploadComponent } from 'src/layout/FileUpload/FileUploadComponent'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; -import type { IFileUploadProps } from 'src/layout/FileUpload/FileUploadComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; import type { IAttachment } from 'src/shared/resources/attachments'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; const testId = 'mockId'; describe('FileUploadComponent', () => { it('should show add attachment button and file counter when number of attachments is less than max', () => { render({ - props: { maxNumberOfAttachments: 3 }, - initialState: { attachments: getAttachments({ count: 2 }) }, + component: { maxNumberOfAttachments: 3 }, + attachments: getAttachments({ count: 2 }), }); expect( @@ -28,8 +27,8 @@ describe('FileUploadComponent', () => { it('should not show add attachment button, and should show file counter when number of attachments is same as max', () => { render({ - props: { maxNumberOfAttachments: 3 }, - initialState: { attachments: getAttachments({ count: 3 }) }, + component: { maxNumberOfAttachments: 3 }, + attachments: getAttachments({ count: 3 }), }); expect( @@ -45,9 +44,7 @@ describe('FileUploadComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].uploaded = false; - render({ - initialState: { attachments }, - }); + render({ attachments }); expect(screen.getByText(/general\.loading/i)).toBeInTheDocument(); }); @@ -56,9 +53,7 @@ describe('FileUploadComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].uploaded = true; - render({ - initialState: { attachments }, - }); + render({ attachments }); expect(screen.queryByText(/general\.loading/i)).not.toBeInTheDocument(); }); @@ -67,9 +62,7 @@ describe('FileUploadComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].deleting = true; - render({ - initialState: { attachments }, - }); + render({ attachments }); expect(screen.getByText(/general\.loading/i)).toBeInTheDocument(); }); @@ -78,9 +71,7 @@ describe('FileUploadComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].deleting = false; - render({ - initialState: { attachments }, - }); + render({ attachments }); expect(screen.queryByText(/general\.loading/i)).not.toBeInTheDocument(); }); @@ -89,8 +80,8 @@ describe('FileUploadComponent', () => { describe('displayMode', () => { it('should not display drop area when displayMode is simple', () => { render({ - props: { displayMode: 'simple' }, - initialState: { attachments: getAttachments({ count: 3 }) }, + component: { displayMode: 'simple' }, + attachments: getAttachments({ count: 3 }), }); expect(screen.queryByTestId(`altinn-drop-zone-${testId}`)).not.toBeInTheDocument(); @@ -98,8 +89,8 @@ describe('FileUploadComponent', () => { it('should display drop area when displayMode is not simple', () => { render({ - props: { displayMode: 'list' }, - initialState: { attachments: getAttachments({ count: 3 }) }, + component: { displayMode: 'list' }, + attachments: getAttachments({ count: 3 }), }); expect(screen.getByTestId(`altinn-drop-zone-${testId}`)).toBeInTheDocument(); @@ -107,8 +98,8 @@ describe('FileUploadComponent', () => { it('should not display drop area when displayMode is not simple and max attachments is reached', () => { render({ - props: { displayMode: 'list', maxNumberOfAttachments: 3 }, - initialState: { attachments: getAttachments({ count: 3 }) }, + component: { displayMode: 'list', maxNumberOfAttachments: 3 }, + attachments: getAttachments({ count: 3 }), }); expect(screen.queryByTestId(`altinn-drop-zone-${testId}`)).not.toBeInTheDocument(); @@ -116,37 +107,31 @@ describe('FileUploadComponent', () => { }); }); -interface IRenderProps { - props?: Partial; - initialState?: { - attachments?: IAttachment[]; - }; +interface Props extends Partial> { + attachments?: IAttachment[]; } -const render = ({ props = {}, initialState = {} }: IRenderProps = {}) => { - const { attachments = getAttachments() } = initialState; - const _initialState = { - ...getInitialStateMock(), - attachments: { - attachments: { +const render = ({ component, genericProps, attachments = getAttachments() }: Props = {}) => { + renderGenericComponentTest({ + type: 'FileUpload', + renderer: (props) => , + component: { + id: testId, + displayMode: 'simple', + maxFileSizeInMB: 2, + maxNumberOfAttachments: 4, + minNumberOfAttachments: 1, + readOnly: false, + ...component, + }, + genericProps: { + isValid: true, + ...genericProps, + }, + manipulateState: (state) => { + state.attachments.attachments = { [testId]: attachments, - }, + }; }, - }; - - const allProps: IFileUploadProps = { - ...mockComponentProps, - id: testId, - displayMode: 'simple', - maxFileSizeInMB: 2, - maxNumberOfAttachments: 4, - minNumberOfAttachments: 1, - isValid: true, - readOnly: false, - ...props, - }; - - return renderWithProviders(, { - preloadedState: _initialState, }); }; diff --git a/src/layout/FileUpload/FileUploadComponent.tsx b/src/layout/FileUpload/FileUploadComponent.tsx index 4ba6664b8e..82ecb6c6b1 100644 --- a/src/layout/FileUpload/FileUploadComponent.tsx +++ b/src/layout/FileUpload/FileUploadComponent.tsx @@ -26,21 +26,20 @@ export type IFileUploadProps = PropsFromGenericComponent<'FileUpload'>; export const bytesInOneMB = 1048576; export const emptyArray = []; -export function FileUploadComponent({ - id, - baseComponentId, - componentValidations, - readOnly, - maxNumberOfAttachments, - maxFileSizeInMB, - minNumberOfAttachments, - validFileEndings, - language, - displayMode, - hasCustomFileEndings, - textResourceBindings, - dataModelBindings, -}: IFileUploadProps) { +export function FileUploadComponent({ node, componentValidations, language }: IFileUploadProps) { + const { + id, + baseComponentId, + readOnly, + maxNumberOfAttachments, + maxFileSizeInMB, + minNumberOfAttachments, + validFileEndings, + displayMode, + hasCustomFileEndings, + textResourceBindings, + dataModelBindings, + } = node.item; const dispatch = useAppDispatch(); const [validations, setValidations] = React.useState([]); const [showFileUpload, setShowFileUpload] = React.useState(false); diff --git a/src/layout/FileUploadWithTag/EditWindowComponent.tsx b/src/layout/FileUploadWithTag/EditWindowComponent.tsx index 43465deb32..20b025568d 100644 --- a/src/layout/FileUploadWithTag/EditWindowComponent.tsx +++ b/src/layout/FileUploadWithTag/EditWindowComponent.tsx @@ -62,20 +62,21 @@ export interface EditWindowProps extends PropsFromGenericComponent<'FileUploadWi export function EditWindowComponent(props: EditWindowProps): JSX.Element { const dispatch = useAppDispatch(); const classes = useStyles(); + const { id, baseComponentId, dataModelBindings, readOnly, textResourceBindings } = props.node.item; const handleDeleteFile = () => { dispatch( AttachmentActions.deleteAttachment({ attachment: props.attachment, - componentId: props.id, - attachmentType: props.baseComponentId || props.id, - dataModelBindings: props.dataModelBindings, + componentId: id, + attachmentType: baseComponentId || id, + dataModelBindings: dataModelBindings, }), ); props.setEditIndex(-1); }; - const saveIsDisabled = props.attachment.updating === true || props.attachment.uploaded === false || props.readOnly; + const saveIsDisabled = props.attachment.updating === true || props.attachment.uploaded === false || readOnly; return (
- {props.textResourceBindings?.tagTitle &&
{props.getTextResource(props.textResourceBindings?.tagTitle)}
} + {textResourceBindings?.tagTitle &&
{props.getTextResource(textResourceBindings?.tagTitle)}
} i.id === props.attachment.id).length > 0, - 'disabled !important': props.attachment.updating ? true : props.readOnly, + 'disabled !important': props.attachment.updating ? true : readOnly, })} onChange={(e) => props.onDropdownDataChange(props.attachment.id, e.target.value)} onBlur={(e) => props.onDropdownDataChange(props.attachment.id, e.target.value)} diff --git a/src/layout/FileUploadWithTag/FileListComponent.tsx b/src/layout/FileUploadWithTag/FileListComponent.tsx index d787a7fcb4..b121f0997c 100644 --- a/src/layout/FileUploadWithTag/FileListComponent.tsx +++ b/src/layout/FileUploadWithTag/FileListComponent.tsx @@ -19,7 +19,6 @@ import { EditWindowComponent } from 'src/layout/FileUploadWithTag/EditWindowComp import { AltinnAppTheme } from 'src/theme/altinnAppTheme'; import { atleastOneTagExists } from 'src/utils/formComponentUtils'; import type { PropsFromGenericComponent } from 'src/layout'; -import type { IDataModelBindings } from 'src/layout/layout'; import type { IAttachment } from 'src/shared/resources/attachments'; import type { IOption } from 'src/types'; @@ -139,7 +138,6 @@ export interface FileListProps extends PropsFromGenericComponent<'FileUploadWith id: string; message: string; }[]; - dataModelBindings?: IDataModelBindings; } export const bytesInOneMB = 1048576; @@ -150,6 +148,8 @@ export function FileList(props: FileListProps): JSX.Element | null { return null; } + const { textResourceBindings } = props.node.item; + return ( - {props.textResourceBindings?.tagTitle && props.getTextResource(props.textResourceBindings.tagTitle)} + {textResourceBindings?.tagTitle && props.getTextResource(textResourceBindings.tagTitle)} {!props.mobileView ? ( @@ -301,8 +301,7 @@ export function FileList(props: FileListProps): JSX.Element | null { )} - id={props.id} - dataModelBindings={props.dataModelBindings} + node={props.node} attachment={props.attachments[index]} attachmentValidations={[ ...new Map( @@ -311,14 +310,12 @@ export function FileList(props: FileListProps): JSX.Element | null { ]} language={props.language} mobileView={props.mobileView} - readOnly={props.readOnly} options={props.options} getTextResource={props.getTextResource} getTextResourceAsString={props.getTextResourceAsString} onSave={props.onSave} onDropdownDataChange={props.onDropdownDataChange} setEditIndex={props.setEditIndex} - textResourceBindings={props.textResourceBindings} /> diff --git a/src/layout/FileUploadWithTag/FileUploadWithTagComponent.test.tsx b/src/layout/FileUploadWithTag/FileUploadWithTagComponent.test.tsx index 1941560200..3e30a5eee1 100644 --- a/src/layout/FileUploadWithTag/FileUploadWithTagComponent.test.tsx +++ b/src/layout/FileUploadWithTag/FileUploadWithTagComponent.test.tsx @@ -4,13 +4,11 @@ import { screen } from '@testing-library/react'; import { getAttachments } from 'src/__mocks__/attachmentsMock'; import { getFormLayoutStateMock } from 'src/__mocks__/formLayoutStateMock'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { getUiConfigStateMock } from 'src/__mocks__/uiConfigStateMock'; import { FileUploadWithTagComponent } from 'src/layout/FileUploadWithTag/FileUploadWithTagComponent'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; -import { AsciiUnitSeparator } from 'src/utils/attachment'; -import type { IFileUploadWithTagProps } from 'src/layout/FileUploadWithTag/FileUploadWithTagComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; import type { IAttachment } from 'src/shared/resources/attachments'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; const testId = 'test-id'; @@ -20,7 +18,7 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].uploaded = false; - render({ initialState: { attachments } }); + render({ attachments }); expect(screen.getByText(/general\.loading/i)).toBeInTheDocument(); }); @@ -29,7 +27,7 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].uploaded = true; - render({ initialState: { attachments } }); + render({ attachments }); expect(screen.queryByText(/general\.loading/i)).not.toBeInTheDocument(); }); @@ -40,7 +38,7 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].updating = true; - render({ initialState: { attachments, editIndex: 0 } }); + render({ attachments, editIndex: 0 }); expect(screen.getByText(/general\.loading/i)).toBeInTheDocument(); }); @@ -49,7 +47,7 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].updating = false; - render({ initialState: { attachments, editIndex: 0 } }); + render({ attachments, editIndex: 0 }); expect(screen.queryByText(/general\.loading/i)).not.toBeInTheDocument(); }); @@ -60,17 +58,15 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].updating = true; - render({ initialState: { attachments, editIndex: 0 } }); + render({ attachments, editIndex: 0 }); expect(screen.getByRole('combobox')).toBeDisabled(); }); it('should not disable dropdown in edit mode when not updating', () => { render({ - initialState: { - attachments: getAttachments({ count: 1 }), - editIndex: 0, - }, + attachments: getAttachments({ count: 1 }), + editIndex: 0, }); expect(screen.getByRole('combobox')).not.toBeDisabled(); @@ -78,10 +74,8 @@ describe('FileUploadWithTagComponent', () => { it('should not disable save button', () => { render({ - initialState: { - attachments: getAttachments({ count: 1 }), - editIndex: 0, - }, + attachments: getAttachments({ count: 1 }), + editIndex: 0, }); expect( @@ -95,8 +89,9 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); render({ - props: { readOnly: true }, - initialState: { attachments, editIndex: 0 }, + component: { readOnly: true }, + attachments, + editIndex: 0, }); expect( @@ -110,7 +105,7 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].uploaded = false; - render({ initialState: { attachments, editIndex: 0 } }); + render({ attachments, editIndex: 0 }); expect( screen.getByRole('button', { @@ -123,7 +118,7 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].updating = true; - render({ initialState: { attachments, editIndex: 0 } }); + render({ attachments, editIndex: 0 }); expect( screen.queryByRole('button', { @@ -136,7 +131,7 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].tags = []; - render({ initialState: { attachments } }); + render({ attachments }); expect( screen.getByRole('button', { @@ -149,7 +144,7 @@ describe('FileUploadWithTagComponent', () => { const attachments = getAttachments({ count: 1 }); attachments[0].tags = ['tag1']; - render({ initialState: { attachments } }); + render({ attachments }); expect( screen.queryByRole('button', { @@ -162,8 +157,8 @@ describe('FileUploadWithTagComponent', () => { describe('files', () => { it('should display drop area when max attachments is not reached', () => { render({ - props: { maxNumberOfAttachments: 3 }, - initialState: { attachments: getAttachments({ count: 2 }) }, + component: { maxNumberOfAttachments: 3 }, + attachments: getAttachments({ count: 2 }), }); expect( @@ -176,8 +171,8 @@ describe('FileUploadWithTagComponent', () => { it('should not display drop area when max attachments is reached', () => { render({ - props: { maxNumberOfAttachments: 3 }, - initialState: { attachments: getAttachments({ count: 3 }) }, + component: { maxNumberOfAttachments: 3 }, + attachments: getAttachments({ count: 3 }), }); expect( @@ -190,89 +185,74 @@ describe('FileUploadWithTagComponent', () => { }); }); -interface IRenderProps { - props?: Partial; - initialState?: { - attachments?: IAttachment[]; - editIndex?: number; - }; +interface Props extends Partial> { + attachments?: IAttachment[]; + editIndex?: number; } -const render = ({ props = {}, initialState = {} }: IRenderProps = {}) => { - const { attachments = getAttachments(), editIndex = -1 } = initialState; - const _initialState = { - ...getInitialStateMock(), - attachments: { - attachments: { - [testId]: attachments, - }, - validationResults: { - [testId]: { - simpleBinding: { - errors: ['mock error message', `attachment-id-2${AsciiUnitSeparator}mock error message`], - }, - }, +const render = ({ component, genericProps, attachments = getAttachments(), editIndex = -1 }: Props = {}) => { + renderGenericComponentTest({ + type: 'FileUploadWithTag', + renderer: (props) => , + component: { + id: testId, + displayMode: 'simple', + maxFileSizeInMB: 2, + maxNumberOfAttachments: 7, + minNumberOfAttachments: 1, + readOnly: false, + optionsId: 'test-options-id', + textResourceBindings: { + tagTitle: 'attachment-tag-title', + 'attachment-tag-label-0': 'attachment-tag-value-0', + 'attachment-tag-label-1': 'attachment-tag-value-1', + 'attachment-tag-label-2': 'attachment-tag-value-2', }, + ...component, }, - optionState: { - options: { - test: { - id: testId, - options: [ - { value: 'attachment-tag-0', label: 'attachment-tag-label-0' }, - { value: 'attachment-tag-1', label: 'attachment-tag-label-1' }, - { value: 'attachment-tag-2', label: 'attachment-tag-label-2' }, - ], - loading: false, - }, - }, - error: null, - optionsCount: 1, - optionsLoadedCount: 1, - loading: false, + genericProps: { + isValid: true, + getTextResource: jest.fn(), + getTextResourceAsString: jest.fn(), + ...genericProps, }, - formLayout: { - ...getFormLayoutStateMock(), - uiConfig: { - ...getUiConfigStateMock(), - fileUploadersWithTag: { - [testId]: { - editIndex, - chosenOptions: { - 'attachment-id-0': 'attachment-tag-0', - 'attachment-id-1': 'attachment-tag-1', - 'attachment-id-2': 'attachment-tag-2', + manipulateState: (state) => { + state.attachments = { + attachments: { + [testId]: attachments, + }, + }; + state.optionState = { + options: { + test: { + id: testId, + options: [ + { value: 'attachment-tag-0', label: 'attachment-tag-label-0' }, + { value: 'attachment-tag-1', label: 'attachment-tag-label-1' }, + { value: 'attachment-tag-2', label: 'attachment-tag-label-2' }, + ], + loading: false, + }, + }, + error: null, + loading: false, + }; + state.formLayout = { + ...getFormLayoutStateMock(), + uiConfig: { + ...getUiConfigStateMock(), + fileUploadersWithTag: { + [testId]: { + editIndex, + chosenOptions: { + 'attachment-id-0': 'attachment-tag-0', + 'attachment-id-1': 'attachment-tag-1', + 'attachment-id-2': 'attachment-tag-2', + }, }, }, }, - }, + }; }, - }; - - const textResourceBindings = { - tagTitle: 'attachment-tag-title', - 'attachment-tag-label-0': 'attachment-tag-value-0', - 'attachment-tag-label-1': 'attachment-tag-value-1', - 'attachment-tag-label-2': 'attachment-tag-value-2', - }; - - const allProps: IFileUploadWithTagProps = { - ...mockComponentProps, - id: testId, - displayMode: 'simple', - isValid: true, - maxFileSizeInMB: 2, - maxNumberOfAttachments: 7, - minNumberOfAttachments: 1, - readOnly: false, - optionsId: 'test-options-id', - textResourceBindings: textResourceBindings, - getTextResource: jest.fn(), - getTextResourceAsString: jest.fn(), - ...props, - }; - - renderWithProviders(, { - preloadedState: _initialState, }); }; diff --git a/src/layout/FileUploadWithTag/FileUploadWithTagComponent.tsx b/src/layout/FileUploadWithTag/FileUploadWithTagComponent.tsx index 37f87301b4..20faa34193 100644 --- a/src/layout/FileUploadWithTag/FileUploadWithTagComponent.tsx +++ b/src/layout/FileUploadWithTag/FileUploadWithTagComponent.tsx @@ -29,23 +29,26 @@ import type { IRuntimeState } from 'src/types'; export type IFileUploadWithTagProps = PropsFromGenericComponent<'FileUploadWithTag'>; export function FileUploadWithTagComponent({ - id, - baseComponentId, + node, componentValidations, language, - maxFileSizeInMB, - readOnly, - maxNumberOfAttachments, - minNumberOfAttachments, - hasCustomFileEndings, - validFileEndings, - optionsId, - mapping, getTextResource, getTextResourceAsString, - textResourceBindings, - dataModelBindings, }: IFileUploadWithTagProps): JSX.Element { + const { + id, + baseComponentId, + maxFileSizeInMB, + readOnly, + maxNumberOfAttachments, + minNumberOfAttachments, + hasCustomFileEndings, + validFileEndings, + optionsId, + mapping, + textResourceBindings, + dataModelBindings, + } = node.item; const dataDispatch = useAppDispatch(); const [validations, setValidations] = React.useState>([]); const mobileView = useMediaQuery('(max-width:992px)'); // breakpoint on altinn-modal @@ -246,13 +249,12 @@ export function FileUploadWithTagComponent({ )} - id={id} + node={node} attachments={attachments} attachmentValidations={attachmentValidationMessages} language={language} editIndex={editIndex} mobileView={mobileView} - readOnly={readOnly} options={options} getTextResource={getTextResource} getTextResourceAsString={getTextResourceAsString} @@ -260,8 +262,6 @@ export function FileUploadWithTagComponent({ onSave={handleSave} onDropdownDataChange={handleDropdownDataChange} setEditIndex={setEditIndex} - textResourceBindings={textResourceBindings} - dataModelBindings={dataModelBindings} /> {!shouldShowFileUpload() && diff --git a/src/layout/GenericComponent.tsx b/src/layout/GenericComponent.tsx index c2f65487ca..21bf6e4cab 100644 --- a/src/layout/GenericComponent.tsx +++ b/src/layout/GenericComponent.tsx @@ -256,10 +256,7 @@ export function GenericComponent; const showValidationMessages = hasValidationMessages && layoutComponent.renderDefaultValidations(); diff --git a/src/layout/Header/HeaderComponent.test.tsx b/src/layout/Header/HeaderComponent.test.tsx index 0e529ca5bc..129b66d55b 100644 --- a/src/layout/Header/HeaderComponent.test.tsx +++ b/src/layout/Header/HeaderComponent.test.tsx @@ -1,64 +1,68 @@ import React from 'react'; -import { fireEvent, render as rtlRender, screen } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/react'; import ResizeObserverModule from 'resize-observer-polyfill'; import { HeaderComponent } from 'src/layout/Header/HeaderComponent'; -import type { IHeaderProps } from 'src/layout/Header/HeaderComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; (global as any).ResizeObserver = ResizeObserverModule; -const render = (props = {}) => { - const allProps = { - id: 'id', - text: 'text', - getTextResource: (key: string) => key, - language: {}, - textResourceBindings: {}, - ...props, - } as IHeaderProps; - - rtlRender(); +const render = ({ component, genericProps }: Partial> = {}) => { + renderGenericComponentTest({ + type: 'Header', + renderer: (props) => , + component: { + textResourceBindings: {}, + ...component, + }, + genericProps: { + text: 'text', + getTextResource: (key: string) => key, + language: {}, + ...genericProps, + }, + }); }; - describe('HeaderComponent', () => { it('should render

when size is "L"', () => { - render({ size: 'L' }); + render({ component: { size: 'L' } }); const header = screen.getByRole('heading', { level: 2 }); expect(header).toBeInTheDocument(); }); it('should render

when size is "h2"', () => { - render({ size: 'h2' }); + render({ component: { size: 'h2' } }); const header = screen.getByRole('heading', { level: 2 }); expect(header).toBeInTheDocument(); }); it('should render

when size is "M"', () => { - render({ size: 'M' }); + render({ component: { size: 'M' } }); const header = screen.getByRole('heading', { level: 3 }); expect(header).toBeInTheDocument(); }); it('should render

when size is "h3"', () => { - render({ size: 'h3' }); + render({ component: { size: 'h3' } }); const header = screen.getByRole('heading', { level: 3 }); expect(header).toBeInTheDocument(); }); it('should render

when size is "S"', () => { - render({ size: 'S' }); + render({ component: { size: 'S' } }); const header = screen.getByRole('heading', { level: 4 }); expect(header).toBeInTheDocument(); }); it('should render

when size is "h4"', () => { - render({ size: 'h4' }); + render({ component: { size: 'h4' } }); const header = screen.getByRole('heading', { level: 4 }); expect(header).toBeInTheDocument(); @@ -83,8 +87,10 @@ describe('HeaderComponent', () => { it('should render help button when help text is defined', () => { render({ - textResourceBindings: { - help: 'this is the help text', + component: { + textResourceBindings: { + help: 'this is the help text', + }, }, }); @@ -98,8 +104,10 @@ describe('HeaderComponent', () => { it('should show and hide help text when clicking help button', async () => { const helpText = 'this is the help text'; render({ - textResourceBindings: { - help: helpText, + component: { + textResourceBindings: { + help: helpText, + }, }, }); diff --git a/src/layout/Header/HeaderComponent.tsx b/src/layout/Header/HeaderComponent.tsx index 2b8835fee2..cfd602e51c 100644 --- a/src/layout/Header/HeaderComponent.tsx +++ b/src/layout/Header/HeaderComponent.tsx @@ -60,7 +60,8 @@ export const HeaderSize = ({ id, size, text }: IHeaderSizeProps) => { } }; -export const HeaderComponent = ({ id, size, text, textResourceBindings, language, getTextResource }: IHeaderProps) => { +export const HeaderComponent = ({ node, text, language, getTextResource }: IHeaderProps) => { + const { id, size, textResourceBindings } = node.item; return ( state.profile.profile?.profileSettingPreference.language || 'nb'); - const width = props.image?.width || '100%'; - const align = props.image?.align || 'center'; - const altText = - props.textResourceBindings?.altTextImg && props.getTextResourceAsString(props.textResourceBindings.altTextImg); + const languageKey = useAppSelector((state) => state.profile.profile?.profileSettingPreference.language || 'nb'); + const width = image?.width || '100%'; + const align = image?.align || 'center'; + const altText = textResourceBindings?.altTextImg && getTextResourceAsString(textResourceBindings.altTextImg); - let imgSrc = props.image?.src[language] || props.image?.src.nb || ''; + let imgSrc = image?.src[languageKey] || image?.src.nb || ''; if (imgSrc.startsWith('wwwroot')) { imgSrc = imgSrc.replace( 'wwwroot', @@ -44,7 +44,7 @@ export function ImageComponent(props: IImageProps) { {renderSvg ? ( @@ -58,7 +58,7 @@ export function ImageComponent(props: IImageProps) { ) : ( {altText} )} - {props.textResourceBindings?.help && ( + {textResourceBindings?.help && ( diff --git a/src/layout/Input/InputComponent.test.tsx b/src/layout/Input/InputComponent.test.tsx index 3f882dacce..9ba86994c8 100644 --- a/src/layout/Input/InputComponent.test.tsx +++ b/src/layout/Input/InputComponent.test.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { act, render as rtlRender, screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { InputComponent } from 'src/layout/Input/InputComponent'; -import type { IInputProps } from 'src/layout/Input/InputComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; describe('InputComponent', () => { jest.useFakeTimers(); @@ -26,8 +27,10 @@ describe('InputComponent', () => { it('should have correct value with specified form data', () => { const simpleBindingValue = 'it123'; render({ - formData: { - simpleBinding: simpleBindingValue, + genericProps: { + formData: { + simpleBinding: simpleBindingValue, + }, }, }); const inputComponent = screen.getByRole('textbox') as HTMLInputElement; @@ -48,7 +51,7 @@ describe('InputComponent', () => { it('should call supplied dataChanged function after data change', async () => { const handleDataChange = jest.fn(); const typedValue = 'test input'; - render({ handleDataChange }); + render({ genericProps: { handleDataChange } }); const inputComponent = screen.getByRole('textbox'); await act(() => user.type(inputComponent, typedValue)); @@ -62,7 +65,7 @@ describe('InputComponent', () => { it('should call supplied dataChanged function immediately after onBlur', async () => { const handleDataChange = jest.fn(); const typedValue = 'test input'; - render({ handleDataChange }); + render({ genericProps: { handleDataChange } }); const inputComponent = screen.getByRole('textbox'); await act(async () => { @@ -82,15 +85,19 @@ describe('InputComponent', () => { const finalValuePlainText = `${inputValuePlainText}${typedValue}`; const finalValueFormatted = '$123,456,789'; render({ - handleDataChange, - formatting: { - number: { - thousandSeparator: true, - prefix: '$', + genericProps: { + handleDataChange, + formData: { + simpleBinding: inputValuePlainText, }, }, - formData: { - simpleBinding: inputValuePlainText, + component: { + formatting: { + number: { + thousandSeparator: true, + prefix: '$', + }, + }, }, }); const inputComponent = screen.getByRole('textbox'); @@ -108,8 +115,10 @@ describe('InputComponent', () => { it('should show aria-describedby if textResourceBindings.description is present', () => { render({ - textResourceBindings: { - description: 'description', + component: { + textResourceBindings: { + description: 'description', + }, }, }); @@ -124,17 +133,22 @@ describe('InputComponent', () => { expect(inputComponent).not.toHaveAttribute('aria-describedby'); }); - function render(props: Partial = {}) { - const allProps = { - id: 'mock-id', - formData: null, - handleDataChange: jest.fn(), - isValid: true, - readOnly: false, - required: false, - ...props, - } as IInputProps; - - rtlRender(); - } + const render = ({ component, genericProps }: Partial> = {}) => { + renderGenericComponentTest({ + type: 'Input', + renderer: (props) => , + component: { + id: 'mock-id', + readOnly: false, + required: false, + + ...component, + }, + genericProps: { + handleDataChange: jest.fn(), + isValid: true, + ...genericProps, + }, + }); + }; }); diff --git a/src/layout/Input/InputComponent.tsx b/src/layout/Input/InputComponent.tsx index 7dee1b4f83..632c318341 100644 --- a/src/layout/Input/InputComponent.tsx +++ b/src/layout/Input/InputComponent.tsx @@ -9,19 +9,9 @@ import type { IInputFormatting } from 'src/layout/layout'; export type IInputProps = PropsFromGenericComponent<'Input'>; -export function InputComponent({ - id, - readOnly, - required, - isValid, - formData, - formatting, - handleDataChange, - variant, - textResourceBindings, - saveWhileTyping, - autocomplete, -}: IInputProps) { +export function InputComponent({ node, isValid, formData, handleDataChange }: IInputProps) { + const { id, readOnly, required, formatting, variant, textResourceBindings, saveWhileTyping, autocomplete } = + node.item; const { value, setValue, saveValue, onPaste } = useDelayedSavedState( handleDataChange, formData?.simpleBinding ?? '', diff --git a/src/layout/InstanceInformation/InstanceInformationComponent.tsx b/src/layout/InstanceInformation/InstanceInformationComponent.tsx index 6c2cc00e01..5454ae43a4 100644 --- a/src/layout/InstanceInformation/InstanceInformationComponent.tsx +++ b/src/layout/InstanceInformation/InstanceInformationComponent.tsx @@ -43,7 +43,8 @@ export const returnInstanceMetaDataObject = ( return obj; }; -export function InstanceInformationComponent({ elements }: PropsFromGenericComponent<'InstanceInformation'>) { +export function InstanceInformationComponent({ node }: PropsFromGenericComponent<'InstanceInformation'>) { + const elements = node.item.elements; const { dateSent, sender, receiver, referenceNumber } = elements || {}; const instance: IInstance | null = useAppSelector((state: IRuntimeState) => state.instanceData.instance); diff --git a/src/layout/InstantiationButton/InstantiationButton.test.tsx b/src/layout/InstantiationButton/InstantiationButton.test.tsx index 4a2931daa7..7f50348355 100644 --- a/src/layout/InstantiationButton/InstantiationButton.test.tsx +++ b/src/layout/InstantiationButton/InstantiationButton.test.tsx @@ -5,52 +5,46 @@ import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import mockAxios from 'jest-mock-axios'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; -import { InstantiationButton } from 'src/layout/InstantiationButton/InstantiationButton'; -import { setupStore } from 'src/store'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; +import { InstantiationButtonComponent } from 'src/layout/InstantiationButton/InstantiationButtonComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; const render = () => { - const stateMock = getInitialStateMock(); - const store = setupStore(stateMock); - - const spy = jest.spyOn(store, 'dispatch'); - - renderWithProviders( - - - Instantiate} - /> - You are now looking at the instance} - /> - - , - { - store, - }, - ); - - return spy; + renderGenericComponentTest({ + type: 'InstantiationButton', + renderer: (props) => ( + + + + } + /> + You are now looking at the instance} + /> + + + ), + }); }; describe('InstantiationButton', () => { it('should show button and it should be possible to click and start loading', async () => { mockAxios.reset(); - const dispatch = render(); + render(); expect(screen.getByText('Instantiate')).toBeInTheDocument(); - expect(dispatch).toHaveBeenCalledTimes(0); expect(mockAxios).toHaveBeenCalledTimes(0); expect(screen.queryByText('general.loading')).toBeNull(); await act(() => userEvent.click(screen.getByRole('button'))); - expect(dispatch).toHaveBeenCalledTimes(1); expect(mockAxios).toHaveBeenCalledTimes(1); expect(screen.getByText('general.loading')).toBeInTheDocument(); diff --git a/src/layout/InstantiationButton/InstantiationButton.tsx b/src/layout/InstantiationButton/InstantiationButton.tsx index 34905e5799..4d6bc80eae 100644 --- a/src/layout/InstantiationButton/InstantiationButton.tsx +++ b/src/layout/InstantiationButton/InstantiationButton.tsx @@ -9,9 +9,9 @@ import { useInstantiateWithPrefillMutation } from 'src/services/InstancesApi'; import { AttachmentActions } from 'src/shared/resources/attachments/attachmentSlice'; import { InstanceDataActions } from 'src/shared/resources/instanceData/instanceDataSlice'; import { mapFormData } from 'src/utils/databindings'; -import type { IInstantiationButtonComponentProps } from 'src/layout/InstantiationButton/InstantiationButtonComponent'; +import type { IInstantiationButtonComponentProvidedProps } from 'src/layout/InstantiationButton/InstantiationButtonComponent'; -type Props = Omit, 'text'>; +type Props = Omit, 'text'>; export const InstantiationButton = ({ children, ...props }: Props) => { const dispatch = useAppDispatch(); diff --git a/src/layout/InstantiationButton/InstantiationButtonComponent.tsx b/src/layout/InstantiationButton/InstantiationButtonComponent.tsx index d47bcacfda..024399ab7e 100644 --- a/src/layout/InstantiationButton/InstantiationButtonComponent.tsx +++ b/src/layout/InstantiationButton/InstantiationButtonComponent.tsx @@ -2,8 +2,10 @@ import React from 'react'; import { InstantiationButton } from 'src/layout/InstantiationButton/InstantiationButton'; import type { PropsFromGenericComponent } from 'src/layout'; +import type { IButtonProvidedProps } from 'src/layout/Button/ButtonComponent'; -export type IInstantiationButtonComponentProps = PropsFromGenericComponent<'InstantiationButton'>; +export type IInstantiationButtonComponentReceivedProps = PropsFromGenericComponent<'InstantiationButton'>; +export type IInstantiationButtonComponentProvidedProps = IButtonProvidedProps; const btnGroupStyle = { marginTop: '2.25rem', @@ -14,7 +16,13 @@ const rowStyle = { marginLeft: '0', }; -export function InstantiationButtonComponent({ text, ...props }: IInstantiationButtonComponentProps) { +export function InstantiationButtonComponent({ + text, + node, + ...componentProps +}: IInstantiationButtonComponentReceivedProps) { + const props: IInstantiationButtonComponentProvidedProps = { ...componentProps, ...node.item, node, text }; + return (
{ +abstract class AnyComponent { /** * Given properties from GenericComponent, render this layout component */ @@ -96,7 +96,14 @@ export abstract class ActionComponent extends FormComponent { + readonly getComponentType = (): ComponentType => { + return ComponentType.Container; + }; +} + export type LayoutComponent = | PresentationComponent | FormComponent - | ActionComponent; + | ActionComponent + | ContainerComponent; diff --git a/src/layout/Likert/LikertComponent.tsx b/src/layout/Likert/LikertComponent.tsx index c809cb757d..73f0c51f5c 100644 --- a/src/layout/Likert/LikertComponent.tsx +++ b/src/layout/Likert/LikertComponent.tsx @@ -6,13 +6,12 @@ import { ControlledRadioGroup } from 'src/layout/RadioButtons/ControlledRadioGro import { useRadioButtons } from 'src/layout/RadioButtons/radioButtonsUtils'; import { StyledRadio } from 'src/layout/RadioButtons/StyledRadio'; import { LayoutStyle } from 'src/types'; -import { useResolvedNode } from 'src/utils/layout/ExprContext'; import { renderValidationMessagesForComponent } from 'src/utils/render'; +import type { PropsFromGenericComponent } from 'src/layout'; import type { IControlledRadioGroupProps } from 'src/layout/RadioButtons/ControlledRadioGroup'; -import type { IRadioButtonsContainerProps } from 'src/layout/RadioButtons/RadioButtonsContainerComponent'; -export const LikertComponent = (props: IRadioButtonsContainerProps) => { - const { layout } = props; +export const LikertComponent = (props: PropsFromGenericComponent<'Likert'>) => { + const { layout } = props.node.item; const useRadioProps = useRadioButtons(props); if (layout === LayoutStyle.Table) { @@ -33,7 +32,7 @@ export const LikertComponent = (props: IRadioButtonsContainerProps) => { }; const RadioGroupTableRow = ({ - id, + node, selected, handleChange, calculatedOptions, @@ -41,8 +40,8 @@ const RadioGroupTableRow = ({ componentValidations, legend, }: IControlledRadioGroupProps) => { - const node = useResolvedNode(id); - const groupContainerId = node?.closest((n) => n.type === 'Group')?.item.id; + const id = node.item.id; + const groupContainerId = node.closest((n) => n.type === 'Group')?.item.id; const RenderLegend = legend; const rowLabelId = `row-label-${id}`; return ( diff --git a/src/layout/Likert/index.tsx b/src/layout/Likert/index.tsx index c4fca55828..9b98457faf 100644 --- a/src/layout/Likert/index.tsx +++ b/src/layout/Likert/index.tsx @@ -16,7 +16,7 @@ export class Likert extends FormComponent<'Likert'> { } directRender(props: PropsFromGenericComponent<'Likert'>): boolean { - return props.layout === LayoutStyle.Table; + return props.node.item.layout === LayoutStyle.Table; } renderWithLabel(): boolean { diff --git a/src/layout/List/ListComponent.test.tsx b/src/layout/List/ListComponent.test.tsx index 68582eff6e..457ff0b5c1 100644 --- a/src/layout/List/ListComponent.test.tsx +++ b/src/layout/List/ListComponent.test.tsx @@ -1,15 +1,10 @@ import React from 'react'; -import { SortDirection } from '@altinn/altinn-design-system'; import { screen } from '@testing-library/react'; -import type { PreloadedState } from 'redux'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { ListComponent } from 'src/layout/List/ListComponent'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; -import type { IListProps } from 'src/layout/List/ListComponent'; -import type { IDataListsState } from 'src/shared/resources/dataLists'; -import type { RootState } from 'src/store'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; const countries = [ { Name: 'Norway', Population: 5, HighestMountain: 2469 }, @@ -20,41 +15,27 @@ const countries = [ { Name: 'France', Population: 67, HighestMountain: 4807 }, ]; -export const testState: IDataListsState = { - dataLists: { - ['countries']: { - listItems: countries, +const render = ({ component, genericProps }: Partial> = {}) => { + renderGenericComponentTest({ + type: 'List', + renderer: (props) => , + component: { + id: 'list-component-id', + tableHeaders: { Name: 'Name', Population: 'Population', HighestMountain: 'HighestMountain' }, + sortableColumns: ['population', 'highestMountain'], + pagination: { alternatives: [2, 5], default: 2 }, dataListId: 'countries', - loading: true, - sortColumn: 'HighestMountain', - sortDirection: SortDirection.Ascending, + ...component, }, - }, - dataListsWithIndexIndicator: [], - error: null, - dataListCount: 1, - dataListLoadedCount: 1, - loading: false, -}; - -const render = (props: Partial = {}, customState: PreloadedState = {}) => { - const allProps: IListProps = { - ...mockComponentProps, - dataListId: 'countries', - tableHeaders: { Name: 'Name', Population: 'Population', HighestMountain: 'HighestMountain' }, - sortableColumns: ['population', 'highestMountain'], - pagination: { alternatives: [2, 5], default: 2 }, - getTextResourceAsString: (value) => value, - legend: () => legend, - ...props, - }; - - renderWithProviders(, { - preloadedState: { - ...getInitialStateMock(), - dataListState: { + genericProps: { + getTextResourceAsString: (value) => value, + legend: () => legend, + ...genericProps, + }, + manipulateState: (state) => { + state.dataListState = { dataLists: { - [allProps.id]: { listItems: countries, id: 'countries' }, + ['list-component-id']: { listItems: countries, id: 'countries' }, }, error: { name: '', @@ -63,8 +44,7 @@ const render = (props: Partial = {}, customState: PreloadedState { jest.useFakeTimers(); it('should render rows that is sent in but not rows that is not sent in', async () => { - render({}); + render(); expect(screen.getByText('Norway')).toBeInTheDocument(); expect(screen.getByText('Sweden')).toBeInTheDocument(); expect(screen.queryByText('Italy')).not.toBeInTheDocument(); diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index fb2f732b4f..d1724a5830 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -18,17 +18,14 @@ export type IListProps = PropsFromGenericComponent<'List'>; const defaultDataList: any[] = []; export const ListComponent = ({ - tableHeaders, - id, - pagination, + node, formData, handleDataChange, getTextResourceAsString, - sortableColumns, - tableHeadersMobile, language, legend, }: IListProps) => { + const { tableHeaders, id, pagination, sortableColumns, tableHeadersMobile } = node.item; const classes = useRadioStyles(); const RenderLegend = legend; const dynamicDataList = useGetDataList({ id }); diff --git a/src/layout/Map/MapComponent.test.tsx b/src/layout/Map/MapComponent.test.tsx index d62d385cfd..16a83a5461 100644 --- a/src/layout/Map/MapComponent.test.tsx +++ b/src/layout/Map/MapComponent.test.tsx @@ -1,35 +1,36 @@ import React from 'react'; -import { render as rtlRender, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { MapComponent } from 'src/layout/Map/MapComponent'; -import { mockComponentProps } from 'src/testUtils'; -import type { IMapComponentProps } from 'src/layout/Map/MapComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; -const render = (props: Partial = {}) => { - const mockLanguage = { - map_component: { - selectedLocation: 'Selected location: {0},{1}', - noSelectedLocation: 'No selected location', +const render = ({ component, genericProps }: Partial> = {}) => { + return renderGenericComponentTest({ + type: 'Map', + renderer: (props) => , + component: { + dataModelBindings: {}, + readOnly: false, + required: false, + textResourceBindings: {}, + ...component, }, - }; - - const allProps: IMapComponentProps = { - ...mockComponentProps, - id: 'id', - formData: { - simpleBinding: undefined, + genericProps: { + formData: { + simpleBinding: undefined, + }, + isValid: true, + language: { + map_component: { + selectedLocation: 'Selected location: {0},{1}', + noSelectedLocation: 'No selected location', + }, + }, + ...genericProps, }, - isValid: true, - dataModelBindings: {}, - language: mockLanguage, - readOnly: false, - required: false, - textResourceBindings: {}, - ...props, - }; - - return rtlRender(); + }); }; describe('MapComponent', () => { @@ -42,8 +43,10 @@ describe('MapComponent', () => { it('should show correct footer text when location is set', () => { render({ - formData: { - simpleBinding: '59.2641592,10.4036248', + genericProps: { + formData: { + simpleBinding: '59.2641592,10.4036248', + }, }, }); @@ -53,7 +56,9 @@ describe('MapComponent', () => { it('should mark map component with validation error when validation fails', () => { const { container } = render({ - isValid: false, + genericProps: { + isValid: false, + }, }); const mapComponent = container.getElementsByClassName('map-component')[0]; @@ -62,7 +67,9 @@ describe('MapComponent', () => { it('should not mark map component with validation error when validation succeeds', () => { const { container } = render({ - isValid: true, + genericProps: { + isValid: true, + }, }); const mapComponent = container.getElementsByClassName('map-component')[0]; diff --git a/src/layout/Map/MapComponent.tsx b/src/layout/Map/MapComponent.tsx index 40e8a462b6..eec20bcd89 100644 --- a/src/layout/Map/MapComponent.tsx +++ b/src/layout/Map/MapComponent.tsx @@ -16,16 +16,8 @@ export const useStyles = makeStyles(() => ({ }, })); -export function MapComponent({ - formData, - handleDataChange, - language, - isValid, - readOnly, - layers, - centerLocation, - zoom, -}: IMapComponentProps) { +export function MapComponent({ formData, handleDataChange, language, isValid, node }: IMapComponentProps) { + const { readOnly, layers, centerLocation, zoom } = node.item; const classes = useStyles(); const location = formData.simpleBinding ? parseLocation(formData.simpleBinding) : undefined; diff --git a/src/layout/MultipleSelect/MultipleSelectComponent.test.tsx b/src/layout/MultipleSelect/MultipleSelectComponent.test.tsx index f194b71052..a612c1de01 100644 --- a/src/layout/MultipleSelect/MultipleSelectComponent.test.tsx +++ b/src/layout/MultipleSelect/MultipleSelectComponent.test.tsx @@ -1,53 +1,49 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; -import type { PreloadedState } from 'redux'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { MultipleSelectComponent } from 'src/layout/MultipleSelect/MultipleSelectComponent'; -import { mockComponentProps, renderWithProviders } from 'src/testUtils'; -import type { IMultipleSelectProps } from 'src/layout/MultipleSelect/MultipleSelectComponent'; -import type { RootState } from 'src/store'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; const dummyLabel = 'dummyLabel'; -const render = (props: Partial = {}, customState: PreloadedState = {}) => { - const allProps: IMultipleSelectProps = { - ...mockComponentProps, - formData: { simpleBinding: '' }, - isValid: true, - dataModelBindings: { simpleBinding: 'some.field' }, - options: [ - { value: 'value1', label: 'label1' }, - { value: 'value2', label: 'label2' }, - { value: 'value3', label: 'label3' }, - ], - readOnly: false, - required: false, - textResourceBindings: {}, - handleDataChange: jest.fn(), - getTextResourceAsString: (key) => key, - ...props, - }; - - return renderWithProviders( - <> - - - , - { - preloadedState: { - ...getInitialStateMock(), - ...customState, - }, +const render = ({ component, genericProps }: Partial> = {}) => { + renderGenericComponentTest({ + type: 'MultipleSelect', + renderer: (props) => ( + <> + + + + ), + component: { + dataModelBindings: { simpleBinding: 'some.field' }, + options: [ + { value: 'value1', label: 'label1' }, + { value: 'value2', label: 'label2' }, + { value: 'value3', label: 'label3' }, + ], + readOnly: false, + required: false, + textResourceBindings: {}, + ...component, + }, + genericProps: { + formData: { simpleBinding: '' }, + isValid: true, + handleDataChange: jest.fn(), + ...genericProps, }, - ); + }); }; describe('MultipleSelect', () => { it('should display correct options as selected when supplied with a comma separated form data', () => { render({ - formData: { simpleBinding: 'value1,value3' }, + genericProps: { + formData: { simpleBinding: 'value1,value3' }, + }, }); expect(screen.getByText('label1')).toBeInTheDocument(); expect(screen.queryByText('label2')).not.toBeInTheDocument(); @@ -57,8 +53,10 @@ describe('MultipleSelect', () => { it('should remove item from comma separated form data on delete', () => { const handleDataChange = jest.fn(); render({ - handleDataChange, - formData: { simpleBinding: 'value1,value2,value3' }, + genericProps: { + handleDataChange, + formData: { simpleBinding: 'value1,value2,value3' }, + }, }); fireEvent.click( screen.getByRole('button', { diff --git a/src/layout/MultipleSelect/MultipleSelectComponent.tsx b/src/layout/MultipleSelect/MultipleSelectComponent.tsx index 6130cdc221..f2df62945a 100644 --- a/src/layout/MultipleSelect/MultipleSelectComponent.tsx +++ b/src/layout/MultipleSelect/MultipleSelectComponent.tsx @@ -16,17 +16,13 @@ const invalidBorderColor = '#D5203B !important'; export type IMultipleSelectProps = PropsFromGenericComponent<'MultipleSelect'>; export function MultipleSelectComponent({ - options, - optionsId, - mapping, - source, + node, handleDataChange, getTextResourceAsString, formData, - id, - readOnly, isValid, }: IMultipleSelectProps) { + const { options, optionsId, mapping, source, id, readOnly } = node.item; const apiOptions = useGetOptions({ optionsId, mapping, source }); const calculatedOptions = (apiOptions || options)?.map((option) => ({ diff --git a/src/layout/NavigationBar/NavigationBarComponent.tsx b/src/layout/NavigationBar/NavigationBarComponent.tsx index dcfdc94e2f..eb3070bad9 100644 --- a/src/layout/NavigationBar/NavigationBarComponent.tsx +++ b/src/layout/NavigationBar/NavigationBarComponent.tsx @@ -105,7 +105,8 @@ const NavigationButton = React.forwardRef( NavigationButton.displayName = 'NavigationButton'; -export const NavigationBarComponent = ({ triggers, compact }: INavigationBar) => { +export const NavigationBarComponent = ({ node }: INavigationBar) => { + const { triggers, compact } = node.item; const classes = useStyles(); const dispatch = useAppDispatch(); const pageIds = useAppSelector(selectLayoutOrder); diff --git a/src/layout/NavigationButtons/NavigationButtonsComponent.test.tsx b/src/layout/NavigationButtons/NavigationButtonsComponent.test.tsx index 33b7e0d043..6ebb9c2be5 100644 --- a/src/layout/NavigationButtons/NavigationButtonsComponent.test.tsx +++ b/src/layout/NavigationButtons/NavigationButtonsComponent.test.tsx @@ -1,152 +1,137 @@ import React from 'react'; -import { Provider } from 'react-redux'; -import { render, screen } from '@testing-library/react'; -import configureStore from 'redux-mock-store'; +import { screen } from '@testing-library/react'; import { getFormLayoutStateMock } from 'src/__mocks__/formLayoutStateMock'; -import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { NavigationButtonsComponent } from 'src/layout/NavigationButtons/NavigationButtonsComponent'; -import type { INavigationButtons } from 'src/layout/NavigationButtons/NavigationButtonsComponent'; +import { renderGenericComponentTest } from 'src/testUtils'; +import type { ExprResolved } from 'src/features/expressions/types'; +import type { ILayoutCompNavButtons } from 'src/layout/NavigationButtons/types'; +import type { RenderGenericComponentTestProps } from 'src/testUtils'; describe('NavigationButton', () => { - let mockStore; - let mockLayout; - - beforeAll(() => { - mockLayout = getFormLayoutStateMock({ - layouts: { - layout1: [ - { - type: 'Input', - id: 'mockId1', - dataModelBindings: { - simpleBinding: 'mockDataBinding1', - }, - readOnly: false, - required: false, - disabled: false, - textResourceBindings: {}, - }, - { - id: 'nav-button-1', - type: 'NavigationButtons', - textResourceBindings: {}, - dataModelBindings: {}, - readOnly: false, - required: false, - }, - ], - layout2: [ - { - type: 'Input', - id: 'mockId2', - dataModelBindings: { - simpleBinding: 'mockDataBinding2', - }, - readOnly: false, - required: false, - disabled: false, - textResourceBindings: {}, + const navButton1: ExprResolved = { + id: 'nav-button1', + type: 'NavigationButtons', + textResourceBindings: {}, + dataModelBindings: {}, + readOnly: false, + required: false, + }; + const navButton2: ExprResolved = { + id: 'nav-button2', + type: 'NavigationButtons', + textResourceBindings: {}, + dataModelBindings: {}, + readOnly: false, + required: false, + }; + const mockLayout = getFormLayoutStateMock({ + layouts: { + layout1: [ + { + type: 'Input', + id: 'mockId1', + dataModelBindings: { + simpleBinding: 'mockDataBinding1', }, - { - id: 'nav-button-2', - type: 'NavigationButtons', - textResourceBindings: {}, - dataModelBindings: {}, - readOnly: false, - required: false, + readOnly: false, + required: false, + disabled: false, + textResourceBindings: {}, + }, + navButton1, + ], + layout2: [ + { + type: 'Input', + id: 'mockId2', + dataModelBindings: { + simpleBinding: 'mockDataBinding2', }, - ], + readOnly: false, + required: false, + disabled: false, + textResourceBindings: {}, + }, + navButton2, + ], + }, + uiConfig: { + currentView: 'layout1', + autoSave: true, + focus: null, + hiddenFields: [], + repeatingGroups: {}, + tracks: { + order: ['layout1', 'layout2'], + hidden: [], + hiddenExpr: {}, }, - uiConfig: { - currentView: 'layout1', - autoSave: true, - focus: null, - hiddenFields: [], - repeatingGroups: null, - tracks: { - order: ['layout1', 'layout2'], - hidden: [], - hiddenExpr: {}, + excludePageFromPdf: [], + excludeComponentFromPdf: [], + navigationConfig: { + layout1: { + next: 'layout2', }, - excludePageFromPdf: [], - excludeComponentFromPdf: [], - navigationConfig: { - layout1: { - next: 'layout2', - }, - layout2: { - previous: 'layout1', - }, + layout2: { + previous: 'layout1', }, }, - }); + }, }); - beforeEach(() => { - const createStore = configureStore(); - const mockInitialState = getInitialStateMock({ - formLayout: mockLayout, + const render = ({ + component, + genericProps, + manipulateState, + }: Partial> = {}) => { + renderGenericComponentTest({ + type: 'NavigationButtons', + renderer: (props) => , + component, + genericProps, + manipulateState: manipulateState + ? manipulateState + : (state) => { + state.formLayout = mockLayout; + }, }); - mockStore = createStore(mockInitialState); - }); + }; test('renders default NavigationButtons component', () => { - render( - - - , - ); + navButton1.showBackButton = false; + render({ + component: { + id: navButton1.id, + }, + }); expect(screen.getByText('next')).toBeTruthy(); expect(screen.queryByText('back')).toBeFalsy(); }); test('renders NavigationButtons component without back button if there is no previous page', () => { - render( - - - , - ); + navButton1.showBackButton = true; + render({ + component: { + id: navButton1.id, + }, + }); expect(screen.getByText('next')).toBeTruthy(); expect(screen.queryByText('back')).toBeNull(); }); test('renders NavigationButtons component with back button if there is a previous page', () => { - const uiConfig = { - ...mockLayout.uiConfig, - currentView: 'layout2', - }; - const layoutState = getFormLayoutStateMock({ - ...mockLayout, - uiConfig, - }); - const initialState = getInitialStateMock({ - formLayout: layoutState, + mockLayout.uiConfig.currentView = 'layout2'; + navButton2.showBackButton = true; + render({ + component: { + id: navButton2.id, + }, }); - const createStoreNew = configureStore(); - const store = createStoreNew(initialState); - render( - - - , - ); - expect(screen.queryByText('back')).toBeTruthy(); }); }); diff --git a/src/layout/NavigationButtons/NavigationButtonsComponent.tsx b/src/layout/NavigationButtons/NavigationButtonsComponent.tsx index 2651a9f7d2..dec7ccf87e 100644 --- a/src/layout/NavigationButtons/NavigationButtonsComponent.tsx +++ b/src/layout/NavigationButtons/NavigationButtonsComponent.tsx @@ -15,7 +15,8 @@ import type { ILayoutNavigation, INavigationConfig } from 'src/types'; export type INavigationButtons = PropsFromGenericComponent<'NavigationButtons'>; -export function NavigationButtonsComponent(props: INavigationButtons) { +export function NavigationButtonsComponent({ node }: INavigationButtons) { + const { id, showBackButton, textResourceBindings, triggers } = node.item; const dispatch = useAppDispatch(); const refPrev = React.useRef(null); @@ -37,9 +38,9 @@ export function NavigationButtonsComponent(props: INavigationButtons) { state.formLayout.uiConfig.currentView, ), ); - const triggers = props.triggers || pageTriggers; - const nextTextKey = returnToView ? 'form_filler.back_to_summary' : props.textResourceBindings?.next || 'next'; - const backTextKey = props.textResourceBindings?.back || 'back'; + const activeTriggers = triggers || pageTriggers; + const nextTextKey = returnToView ? 'form_filler.back_to_summary' : textResourceBindings?.next || 'next'; + const backTextKey = textResourceBindings?.back || 'back'; React.useEffect(() => { const currentViewIndex = orderedLayoutKeys?.indexOf(currentView); @@ -63,13 +64,13 @@ export function NavigationButtonsComponent(props: INavigationButtons) { }, []); const OnClickNext = () => { - const runValidations = reducePageValidations(triggers); + const runValidations = reducePageValidations(activeTriggers); const keepScrollPosAction: IKeepComponentScrollPos = { - componentId: props.id, + componentId: id, offsetTop: getScrollPosition(), }; - if (triggers?.includes(Triggers.CalculatePageOrder)) { + if (activeTriggers?.includes(Triggers.CalculatePageOrder)) { dispatch( FormLayoutActions.calculatePageOrderAndMoveToNextPage({ runValidations, @@ -92,7 +93,7 @@ export function NavigationButtonsComponent(props: INavigationButtons) { }; React.useLayoutEffect(() => { - if (!keepScrollPos || typeof keepScrollPos.offsetTop !== 'number' || keepScrollPos.componentId !== props.id) { + if (!keepScrollPos || typeof keepScrollPos.offsetTop !== 'number' || keepScrollPos.componentId !== id) { return; } @@ -103,7 +104,7 @@ export function NavigationButtonsComponent(props: INavigationButtons) { window.scrollBy({ top: currentPos - keepScrollPos.offsetTop }); dispatch(FormLayoutActions.clearKeepScrollPos()); - }, [keepScrollPos, dispatch, props.id, getScrollPosition]); + }, [keepScrollPos, dispatch, id, getScrollPosition]); if (!language) { return null; @@ -115,7 +116,7 @@ export function NavigationButtonsComponent(props: INavigationButtons) { container spacing={1} > - {!disableBack && props.showBackButton && ( + {!disableBack && showBackButton && (