Skip to content

Commit 2dedd4f

Browse files
committed
Add framework for integration tests
1 parent 1a0d727 commit 2dedd4f

3 files changed

Lines changed: 332 additions & 14 deletions

File tree

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { render, waitFor } from '@testing-library/react';
2+
import { vi, beforeAll } from 'vitest';
3+
4+
import { AboriginalForm, RequestDefinition } from './aboriginalFormUtils.tsx';
5+
import { Condition, FhirResource, Patient } from 'fhir/r4';
6+
import { getBirthDateForAge, getInputText, selectTab } from './testUtils.ts';
7+
8+
const patient: Patient = {
9+
resourceType: 'Patient',
10+
id: 'patient-123',
11+
name: [
12+
{
13+
use: 'official',
14+
family: 'John',
15+
given: ['Snow']
16+
}
17+
],
18+
birthDate: getBirthDateForAge(19),
19+
gender: 'male'
20+
};
21+
22+
const onsetDateTime = '2025-10-10T00:00:00.000Z';
23+
const abatementDateTime = '2026-01-01T00:00:00.000Z';
24+
25+
const condition: Condition = {
26+
resourceType: 'Condition',
27+
id: 'active-snomed-condition',
28+
subject: { reference: `Patient/${patient.id}` },
29+
onsetDateTime,
30+
code: {
31+
coding: [
32+
{
33+
system: 'http://snomed.info/sct',
34+
code: '123456',
35+
display: 'Example condition'
36+
}
37+
]
38+
},
39+
clinicalStatus: {
40+
coding: [
41+
{
42+
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
43+
code: 'active'
44+
}
45+
]
46+
},
47+
verificationStatus: {
48+
coding: [
49+
{
50+
code: 'confirmed'
51+
}
52+
]
53+
},
54+
category: [
55+
{
56+
coding: [
57+
{
58+
system: 'http://terminology.hl7.org/CodeSystem/condition-category',
59+
code: 'problem-list-item'
60+
}
61+
]
62+
}
63+
]
64+
};
65+
66+
const nonSnomedCondition: Condition = {
67+
resourceType: 'Condition',
68+
id: 'active-non-snomed-condition',
69+
subject: { reference: `Patient/${patient.id}` },
70+
onsetDateTime,
71+
code: {
72+
coding: [
73+
{
74+
system: 'http://loinc.org',
75+
code: '78910',
76+
display: 'Non-SNOMED condition'
77+
}
78+
],
79+
text: 'Non-SNOMED condition'
80+
},
81+
clinicalStatus: {
82+
coding: [
83+
{
84+
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
85+
code: 'active'
86+
}
87+
]
88+
},
89+
verificationStatus: {
90+
coding: [
91+
{
92+
code: 'confirmed'
93+
}
94+
]
95+
},
96+
category: [
97+
{
98+
coding: [
99+
{
100+
system: 'http://terminology.hl7.org/CodeSystem/condition-category',
101+
code: 'problem-list-item'
102+
}
103+
]
104+
}
105+
]
106+
};
107+
108+
const resolvedCondition: Condition = {
109+
resourceType: 'Condition',
110+
id: 'resolved-condition',
111+
subject: { reference: `Patient/${patient.id}` },
112+
onsetDateTime,
113+
abatementDateTime,
114+
code: {
115+
coding: [
116+
{
117+
system: 'http://snomed.info/sct',
118+
code: '654321',
119+
display: 'Resolved condition'
120+
}
121+
]
122+
},
123+
clinicalStatus: {
124+
coding: [
125+
{
126+
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
127+
code: 'resolved'
128+
}
129+
]
130+
},
131+
verificationStatus: {
132+
coding: [
133+
{
134+
code: 'confirmed'
135+
}
136+
]
137+
},
138+
category: [
139+
{
140+
coding: [
141+
{
142+
system: 'http://terminology.hl7.org/CodeSystem/condition-category',
143+
code: 'problem-list-item'
144+
}
145+
]
146+
}
147+
]
148+
};
149+
150+
const requestDefinitions: RequestDefinition[] = [
151+
{
152+
urlPrefix: 'Condition',
153+
params: {},
154+
responseBody: makeSearchSetBundle([condition, nonSnomedCondition, resolvedCondition])
155+
}
156+
157+
// For ObsHeartRate
158+
// {
159+
// urlPrefix: 'Observation',
160+
// params: {code: '8867-4'},
161+
// responseBody: makeSearchSetBundle([obsHeartRate])
162+
// },
163+
];
164+
165+
function makeSearchSetBundle(resources: FhirResource[]) {
166+
return {
167+
resourceType: 'Bundle',
168+
type: 'searchset',
169+
entry: resources.map((resource) => ({
170+
resource: resource
171+
}))
172+
};
173+
}
174+
175+
vi.mock('fhirclient', () => ({
176+
client: () => ({})
177+
}));
178+
179+
beforeAll(() => {
180+
globalThis.ResizeObserver = class ResizeObserver {
181+
observe() {
182+
// do nothing
183+
}
184+
unobserve() {
185+
// do nothing
186+
}
187+
disconnect() {
188+
// do nothing
189+
}
190+
};
191+
});
192+
193+
describe('Dummy integration test', () => {
194+
test('Patient details', async () => {
195+
const { container } = render(
196+
<AboriginalForm patient={patient} requestDefinitions={requestDefinitions} />
197+
);
198+
199+
// Timeout is increased to 5 seconds to allow the form to be populated
200+
await waitFor(() => expect(container.innerHTML).toContain('Patient Details'), {
201+
timeout: 5000
202+
});
203+
await selectTab(container, 'Patient Details');
204+
205+
const patientAge = await getInputText(container, 'Age');
206+
expect(patientAge).toBe('19');
207+
208+
const patientName = await getInputText(container, 'Name');
209+
expect(patientName).toBe('John, Snow');
210+
211+
});
212+
213+
test('Medical conditions', async () => {
214+
const { container } = render(
215+
<AboriginalForm patient={patient} requestDefinitions={requestDefinitions} />
216+
);
217+
218+
await waitFor(() => expect(container.innerHTML).toContain('Patient Details'), {
219+
timeout: 5000
220+
});
221+
await selectTab(container, 'Medical history and current problems');
222+
// const condition = await getInputText(container, 'Condition');
223+
// console.log(condition);
224+
});
225+
});
Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,88 @@
11
import {
22
BaseRenderer,
3+
buildForm,
34
RendererThemeProvider,
4-
useBuildForm,
55
useRendererQueryClient
66
} from '@aehrc/smart-forms-renderer';
77

88
import aboriginalForm from '../data/resources/Questionnaire/Questionnaire-AboriginalTorresStraitIslanderHealthCheckAssembled-0.4.0.json';
9-
import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4';
9+
import type { Questionnaire } from 'fhir/r4';
1010
import { QueryClientProvider } from '@tanstack/react-query';
11+
import type { Patient } from 'fhir/r4';
12+
import { populateQuestionnaire } from '@aehrc/sdc-populate';
13+
import { useEffect, useState } from 'react';
14+
const terminologyServerUrl = 'https://r4.ontoserver.csiro.au/fhir';
1115

12-
export function AboriginalForm() {
13-
return <BuildFormWrapper questionnaire={aboriginalForm as Questionnaire} />;
16+
export type RequestDefinition = {
17+
urlPrefix: string;
18+
params?: Record<string, string>;
19+
responseBody: any;
20+
};
21+
22+
interface AboriginalFormProps {
23+
patient?: Patient;
24+
requestDefinitions?: RequestDefinition[];
25+
}
26+
27+
export function AboriginalForm(props: AboriginalFormProps) {
28+
return (
29+
<BuildFormWrapperWithPopulate questionnaire={aboriginalForm as Questionnaire} {...props} />
30+
);
1431
}
1532

16-
interface BuildFormWrapperForStorybookProps {
33+
interface BuildFormWrapperWithPopulateProps extends AboriginalFormProps {
1734
questionnaire: Questionnaire;
18-
questionnaireResponse?: QuestionnaireResponse;
1935
}
2036

21-
function BuildFormWrapper(props: BuildFormWrapperForStorybookProps) {
22-
const { questionnaire, questionnaireResponse } = props;
37+
function BuildFormWrapperWithPopulate(props: BuildFormWrapperWithPopulateProps) {
38+
const { questionnaire, patient, requestDefinitions } = props;
2339
const queryClient = useRendererQueryClient();
24-
const isBuilding = useBuildForm({
25-
questionnaire,
26-
questionnaireResponse,
27-
terminologyServerUrl: 'https://r4.ontoserver.csiro.au/fhir'
28-
});
2940

30-
if (isBuilding) {
41+
const [isPopulating, setIsPopulating] = useState(false);
42+
43+
useEffect(() => {
44+
const load = async () => {
45+
setIsPopulating(true);
46+
47+
if (patient && requestDefinitions) {
48+
const result = await populateQuestionnaire({
49+
questionnaire: questionnaire,
50+
patient: patient,
51+
fetchResourceCallback: buildFetchResourceCallback(requestDefinitions),
52+
fetchResourceRequestConfig: { sourceServerUrl: 'http://mock.example' }
53+
});
54+
55+
const { populateSuccess, populateResult } = result;
56+
if (!populateSuccess || !populateResult) {
57+
setIsPopulating(false);
58+
return;
59+
}
60+
61+
const { populatedResponse, populatedContext } = populateResult;
62+
63+
await buildForm({
64+
questionnaire: questionnaire,
65+
questionnaireResponse: populatedResponse,
66+
terminologyServerUrl,
67+
additionalContext: {
68+
patient: patient,
69+
...populatedContext
70+
}
71+
});
72+
} else {
73+
await buildForm({
74+
questionnaire: questionnaire,
75+
terminologyServerUrl
76+
});
77+
}
78+
79+
setIsPopulating(false);
80+
};
81+
82+
load();
83+
}, [questionnaire, patient, requestDefinitions]);
84+
85+
if (isPopulating) {
3186
return <div>Loading...</div>;
3287
}
3388

@@ -39,3 +94,35 @@ function BuildFormWrapper(props: BuildFormWrapperForStorybookProps) {
3994
</RendererThemeProvider>
4095
);
4196
}
97+
98+
function buildFetchResourceCallback(requestDefinitions: RequestDefinition[]) {
99+
return async (url: string) => {
100+
const requestUrl = url;
101+
const [path, queryString] = requestUrl.split('?');
102+
103+
const searchParams = new URLSearchParams(queryString ?? '');
104+
const paramsObject: Record<string, string> = {};
105+
searchParams.forEach((value, key) => {
106+
paramsObject[key] = value;
107+
});
108+
109+
const match = requestDefinitions.find((def) => {
110+
if (!path.startsWith(def.urlPrefix)) {
111+
return false;
112+
}
113+
114+
if (!def.params) {
115+
return true;
116+
}
117+
118+
return Object.entries(def.params).every(([key, value]) => paramsObject[key] === value);
119+
});
120+
121+
if (match) {
122+
return Promise.resolve(match.responseBody);
123+
}
124+
125+
return Promise.resolve({});
126+
};
127+
}
128+

apps/smart-forms-app/src/test/testUtils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,9 @@ export async function getVisibleTab(canvasElement: HTMLElement): Promise<HTMLEle
380380
return tabPanelOrAccordion;
381381
});
382382
}
383+
384+
export function getBirthDateForAge(ageInYears: number): string {
385+
const today = new Date();
386+
const birthDate = new Date(today.getFullYear() - ageInYears, today.getMonth(), today.getDate());
387+
return birthDate.toISOString().slice(0, 10);
388+
}

0 commit comments

Comments
 (0)