Skip to content

Commit

Permalink
Merge pull request #423 from beda-software/fixed-repeatable-groups
Browse files Browse the repository at this point in the history
Add workaround to repeatable group to store it's state
  • Loading branch information
ir4y authored Jan 14, 2025
2 parents c6f325f + efa50e6 commit eaac4bd
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 5 deletions.
20 changes: 20 additions & 0 deletions resources/seeds/Questionnaire/repeatable-group.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
id: repeatable-group
name: Repeatable Group
title: Repeatable Group
status: active
meta:
profile:
- https://beda.software/beda-emr-questionnaire
item:
- text: Items
type: group
linkId: surgeries-group
item:
- item:
- text: Text
type: string
linkId: repeatable-group-text
type: group
linkId: repeatable-group
repeats: true
resourceType: Questionnaire
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function QuestionQuantity({ parentPath, questionItem }: QuestionItemProps
const { linkId, text, hidden } = questionItem;
const fieldName = [...parentPath, linkId, 0, 'value'];
const { value } = useFieldController(fieldName, questionItem);
const quantity: Quantity | undefined = value?.Quantity;
const quantity: Quantity | undefined = value?.Quantity;

if (hidden) {
return null;
Expand All @@ -60,7 +60,9 @@ export function QuestionQuantity({ parentPath, questionItem }: QuestionItemProps
return (
<S.Question className={classNames(s.question, s.row, 'form__question')}>
<span className={s.questionText}>{text}</span>
<span className={s.answer}>{quantity && _.isNumber(quantity?.value) ? `${quantity.value} ${quantity.unit}` : '-'}</span>
<span className={s.answer}>
{quantity && _.isNumber(quantity?.value) ? `${quantity.value} ${quantity.unit}` : '-'}
</span>
</S.Question>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { screen, render, act, fireEvent, waitFor } from '@testing-library/react';
import { Patient, Practitioner, QuestionnaireResponse } from 'fhir/r4b';
import { expect, test, vi } from 'vitest';

import { getFHIRResources } from 'aidbox-react/lib/services/fhir';
import { axiosInstance } from 'aidbox-react/lib/services/instance';

import { ensure, extractBundleResources, WithId, withRootAccess } from '@beda.software/fhir-react';

import { PatientDocument } from 'src/containers/PatientDetails/PatientDocument';
import { createPatient, createPractitionerRole, loginAdminUser } from 'src/setupTests';
import { ThemeProvider } from 'src/theme';
import { evaluate } from 'src/utils';

type ProcedureCase = {
case: { text: string }[];
};

const CASES: ProcedureCase[] = [
{
case: [
{
text: 'Test 1',
},
],
},
{
case: [
{
text: 'Test 2',
},
{
text: 'Test 3',
},
],
},
{
case: [
{
text: 'Test 4',
},
{
text: 'Test 5',
},
{
text: 'Test 6',
},
],
},
{
case: [
{
text: 'Test 7',
},
{
text: 'Test 8',
},
{
text: 'Test 9',
},
{
text: 'Test 10',
},
{
text: 'Test 11',
},
],
},
];

describe('Repeatable group creates correct questionnaire response', async () => {
async function setup() {
await loginAdminUser();
return await withRootAccess(axiosInstance, async () => {
const patient = await createPatient({
name: [{ given: ['John'], family: 'Smith' }],
});

const { practitioner, practitionerRole } = await createPractitionerRole({});

return { patient, practitioner, practitionerRole };
});
}

async function renderRepeatableGroupForm(patient: Patient, practitioner: WithId<Practitioner>) {
const onSuccess = vi.fn();

act(() => {
i18n.activate('en');
});

render(
<ThemeProvider>
<I18nProvider i18n={i18n}>
<PatientDocument
patient={patient}
author={practitioner}
questionnaireId="repeatable-group"
onSuccess={onSuccess}
/>
</I18nProvider>
</ThemeProvider>,
);

return onSuccess;
}

test.each(CASES)(
'Test group adding first and then filling all fields',
async (caseData) => {
const { patient, practitioner } = await setup();

const onSuccess = await renderRepeatableGroupForm(patient, practitioner);

if (caseData.case.length > 1) {
const addAnotherAnswerButton = (await screen.findByText('Add another answer')).parentElement!;
caseData.case.slice(1).forEach(() => {
act(() => {
fireEvent.click(addAnotherAnswerButton);
});
});
}

const textFields = await screen.findAllByTestId('repeatable-group-text');
textFields.forEach((textField, textFieldIndex) => {
expect(textField).toBeEnabled();

const textInput = textField.querySelector('input')!;
act(() => {
fireEvent.change(textInput, {
target: { value: caseData.case[textFieldIndex]!.text },
});
});
});

const submitButton = await screen.findByTestId('submit-button');
expect(submitButton).toBeEnabled();

act(() => {
fireEvent.click(submitButton);
});

await waitFor(() => expect(onSuccess).toHaveBeenCalled());

await withRootAccess(axiosInstance, async () => {
const qrsBundleRD = await getFHIRResources<QuestionnaireResponse>('QuestionnaireResponse', {
questionnaire: 'repeatable-group',
_sort: ['-createdAt', '_id'],
});

const qrs = extractBundleResources(ensure(qrsBundleRD)).QuestionnaireResponse;
expect(qrs.length).toBeGreaterThan(0);

const currentQR = qrs[0];

const repeatableGroupTexts = evaluate(
currentQR,
"QuestionnaireResponse.repeat(item).where(linkId='repeatable-group-text')",
);
expect(repeatableGroupTexts.length).toBe(caseData.case.length);

repeatableGroupTexts.forEach((text, textIndex) => {
expect(text!.answer[0].value.string).toBe(caseData.case[textIndex]!.text);
});
});
},
60000,
);

test.each(CASES)(
'Test filling all fields and adding one by one',
async (caseData) => {
const { patient, practitioner } = await setup();

const onSuccess = await renderRepeatableGroupForm(patient, practitioner);

for (const [caseIndex, caseItem] of caseData.case.entries()) {
const textFields = await screen.findAllByTestId('repeatable-group-text');
const textField = textFields[caseIndex];
expect(textField).toBeEnabled();

const textInput = textField!.querySelector('input')!;
act(() => {
fireEvent.change(textInput, {
target: { value: caseItem.text },
});
});

const isLastText = caseIndex === caseData.case.length - 1;
if (!isLastText) {
const addAnotherAnswerButton = (await screen.findByText('Add another answer')).parentElement!;
act(() => {
fireEvent.click(addAnotherAnswerButton);
});
}
}

const submitButton = await screen.findByTestId('submit-button');
expect(submitButton).toBeEnabled();

act(() => {
fireEvent.click(submitButton);
});

await waitFor(() => expect(onSuccess).toHaveBeenCalled());

await withRootAccess(axiosInstance, async () => {
const qrsBundleRD = await getFHIRResources<QuestionnaireResponse>('QuestionnaireResponse', {
questionnaire: 'repeatable-group',
_sort: ['-createdAt', '_id'],
});

const qrs = extractBundleResources(ensure(qrsBundleRD)).QuestionnaireResponse;
expect(qrs.length).toBeGreaterThan(0);

const currentQR = qrs[0];

const repeatableGroupTexts = evaluate(
currentQR,
"QuestionnaireResponse.repeat(item).where(linkId='repeatable-group-text')",
);
expect(repeatableGroupTexts.length).toBe(caseData.case.length);

repeatableGroupTexts.forEach((text, textIndex) => {
expect(text!.answer[0].value.string).toBe(caseData.case[textIndex]!.text);
});
});
},
60000,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Trans } from '@lingui/macro';
import { Button } from 'antd';
import _ from 'lodash';
import React, { ReactNode } from 'react';
import { useFormContext } from 'react-hook-form';
import { GroupItemProps, QuestionItems } from 'sdc-qrf';

import { useFieldController } from 'src/components/BaseQuestionnaireResponseForm/hooks';
Expand All @@ -13,15 +14,26 @@ import s from './RepeatableGroups.module.scss';
interface RepeatableGroupsProps {
groupItem: GroupItemProps;
renderGroup?: (props: RepeatableGroupProps) => ReactNode;
buildValue?: (existingItems: Array<any>) => any;
}

function defaultBuildValue(exisingItems: Array<any>) {
return [...exisingItems, {}];
}

export function RepeatableGroups(props: RepeatableGroupsProps) {
const { groupItem, renderGroup } = props;
const { parentPath, questionItem } = groupItem;
const { linkId, required } = questionItem;
const fieldName = [...parentPath, linkId];
const { value, onChange } = useFieldController(fieldName, questionItem);
const { onChange } = useFieldController(fieldName, questionItem);

const { getValues } = useFormContext();

const value = _.get(getValues(), fieldName);

const items = value.items && value.items.length ? value.items : required ? [{}] : [];
const buildValue = props.buildValue ?? defaultBuildValue;

return (
<div className={s.group}>
Expand Down Expand Up @@ -56,8 +68,7 @@ export function RepeatableGroups(props: RepeatableGroupsProps) {
type="link"
className={s.addButton}
onClick={() => {
const existingItems = items || [];
const updatedInput = { items: [...existingItems, {}] };
const updatedInput = { items: buildValue(items ?? []) };
onChange(updatedInput);
}}
size="small"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import classNames from 'classnames';
import _ from 'lodash';
import { GroupItemProps, QuestionItems } from 'sdc-qrf';

import { Text } from 'src/components/Typography';
Expand Down
3 changes: 3 additions & 0 deletions src/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '@testing-library/jest-dom/extend-expect';

import { cleanup } from '@testing-library/react';
import {
Consent,
Encounter,
Expand Down Expand Up @@ -220,6 +221,8 @@ afterEach(async () => {
data: { query: `select drop_before_all(${txId});` },
});
});
cleanup();
vi.clearAllMocks();
});

// afterAll(() => {
Expand Down

0 comments on commit eaac4bd

Please sign in to comment.