Skip to content

Commit 3b39ce2

Browse files
committed
add some frontend tests
1 parent 66f7aec commit 3b39ce2

10 files changed

Lines changed: 337 additions & 48 deletions

File tree

client/package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/src/api/schemas/uploadCustomCodesPreviewItem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* Validated CSV row ready for confirmation.
44
*/
55
export interface UploadCustomCodesPreviewItem {
6+
id: string;
67
code: string;
78
system_key: string;
8-
system_display_name: string;
99
name: string;
1010
row?: number | null;
1111
}

client/src/api/schemas/uploadCustomCodesPreviewResponse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { UploadCustomCodesPreviewItem } from './uploadCustomCodesPreviewIte
66
*/
77
export interface UploadCustomCodesPreviewResponse {
88
preview_items: UploadCustomCodesPreviewItem[];
9+
code_systems: IndexedCodeSystem;
910
codes_processed?: number | null;
1011
total_custom_codes_in_configuration?: number | null;
11-
code_systems: IndexedCodeSystem;
1212
}
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { render, screen, within } from '@testing-library/react';
2+
import { MemoryRouter } from 'react-router';
3+
import { TestQueryClientProvider } from '../../../../../test-utils';
4+
import { ImportCustomCodes } from './ImportCustomCodes';
5+
import {
6+
MOCK_CONFIG_ID,
7+
mockCodeSystems,
8+
mockIndexedSystem,
9+
} from '../../../../../utils/fixtures';
10+
import userEvent from '@testing-library/user-event';
11+
import { useUploadCustomCodesCsv } from '../../../../../api/configurations/configurations';
12+
import { Mock } from 'vitest';
13+
import {
14+
CSV_DOWNLOAD_TEMPLATE,
15+
EXAMPLE_CVX_CODE,
16+
EXAMPLE_LOINC_CODE,
17+
EXAMPLE_OTHER_CODE_SUFFIX,
18+
EXAMPLE_SNOMED_CODE,
19+
} from './utils';
20+
import {
21+
UploadCustomCodesPreviewItem,
22+
UploadCustomCodesPreviewResponse,
23+
} from '../../../../../api/schemas';
24+
25+
vi.mock('../../../../../api/configurations/configurations', async () => {
26+
const actual = await vi.importActual(
27+
'../../../../../api/configurations/configurations'
28+
);
29+
return {
30+
...actual,
31+
useUploadCustomCodesCsv: vi.fn(),
32+
};
33+
});
34+
35+
vi.mock('../../../../../api/code-systems/code-systems', async () => {
36+
const actual = await vi.importActual(
37+
'../../../../../api/code-systems/code-systems'
38+
);
39+
return {
40+
...actual,
41+
useGetCodeSystems: vi.fn(() => ({
42+
data: {
43+
data: mockCodeSystems,
44+
},
45+
})),
46+
};
47+
});
48+
49+
describe('Custom codes upload', () => {
50+
const user = userEvent.setup();
51+
52+
beforeEach(async () => {
53+
(useUploadCustomCodesCsv as unknown as Mock).mockReturnValue({
54+
mutate: vi.fn((variables, options) => {
55+
if (options?.onSuccess) options.onSuccess({ data: mockUploadResponse });
56+
if (options?.onSettled) options.onSettled({}, null, variables);
57+
}),
58+
});
59+
60+
render(
61+
<MemoryRouter
62+
initialEntries={[`/configurations/${MOCK_CONFIG_ID}/build`]}
63+
>
64+
<TestQueryClientProvider>
65+
<ImportCustomCodes configurationId={MOCK_CONFIG_ID} />
66+
</TestQueryClientProvider>
67+
</MemoryRouter>
68+
);
69+
expect(await screen.findByText('Import from CSV')).toBeInTheDocument();
70+
const uploadCsvButton = screen.getByLabelText(
71+
'Bulk custom code upload file input'
72+
);
73+
expect(uploadCsvButton).toBeInTheDocument();
74+
75+
const file = new File([CSV_DOWNLOAD_TEMPLATE], 'test.csv', {
76+
type: 'text/csv',
77+
});
78+
79+
await user.upload(uploadCsvButton, file);
80+
81+
uploadCsvButton.click();
82+
const rowsUploaded = screen.getAllByRole('row');
83+
expect(rowsUploaded.length).toBe(mockCodeSystems.length);
84+
});
85+
86+
test('delete all resets to the first step', async () => {
87+
await userEvent.click(screen.getByText('Undo & delete codes'));
88+
89+
expect(
90+
screen.getByRole('heading', { name: 'Undo & delete codes' })
91+
).toBeInTheDocument();
92+
const confirmationCopy = screen.getByText(
93+
'Are you sure you want to delete all these uploaded codes?',
94+
{ exact: false }
95+
);
96+
expect(confirmationCopy).toBeInTheDocument();
97+
98+
const confirmDeleteButton = screen.getByRole('button', {
99+
name: 'Undo & delete codes',
100+
});
101+
expect(confirmDeleteButton).toBeInTheDocument();
102+
await userEvent.click(confirmDeleteButton);
103+
expect(confirmationCopy).not.toBeInTheDocument();
104+
});
105+
106+
test('delete row successfully deletes a row', async () => {
107+
const deleteRows = screen.getAllByRole('row');
108+
checkAllCodesExistence();
109+
110+
await userEvent.click(
111+
await within(deleteRows[0]).findByRole('button', { name: 'Delete' })
112+
);
113+
expect(screen.getAllByRole('row').length).toBe(mockCodeSystems.length - 1);
114+
115+
checkSnomedCode(false); // first row is the SNOMED row
116+
checkOtherCode();
117+
checkIcd10Code();
118+
checkLoincCode();
119+
checkRxNormCode();
120+
checkCvxCode();
121+
});
122+
123+
test('filters appropriately when search string is supplied', async () => {
124+
const searchInput = screen.getByPlaceholderText('Search codes');
125+
checkAllCodesExistence();
126+
// by display
127+
await userEvent.type(searchInput, 'LOINC Example');
128+
checkLoincCode();
129+
checkSnomedCode(false);
130+
checkOtherCode(false);
131+
checkIcd10Code(false);
132+
checkCvxCode(false);
133+
134+
// by code
135+
await userEvent.clear(searchInput);
136+
await userEvent.type(searchInput, EXAMPLE_LOINC_CODE);
137+
checkLoincCode();
138+
checkOtherCode(false);
139+
checkIcd10Code(false);
140+
checkSnomedCode(false);
141+
checkCvxCode(false);
142+
143+
// make sure codes with matching prefixes get appropriately filtered
144+
await userEvent.clear(searchInput);
145+
await userEvent.type(searchInput, EXAMPLE_CVX_CODE);
146+
expect(screen.getAllByText(EXAMPLE_CVX_CODE).length).toBe(2);
147+
148+
expect(screen.getByText('Other Example')).toBeInTheDocument();
149+
expect(screen.getByText('CVX Example')).toBeInTheDocument();
150+
checkLoincCode(false);
151+
checkSnomedCode(false);
152+
checkIcd10Code(false);
153+
154+
await userEvent.type(searchInput, EXAMPLE_OTHER_CODE_SUFFIX);
155+
checkOtherCode();
156+
checkCvxCode(false);
157+
checkLoincCode(false);
158+
checkSnomedCode(false);
159+
checkIcd10Code(false);
160+
});
161+
162+
describe('edit modal', () => {
163+
beforeEach(async () => {
164+
const editRows = screen.getAllByRole('row');
165+
checkAllCodesExistence();
166+
167+
await userEvent.click(
168+
await within(editRows[0]).findByRole('button', { name: 'Edit' })
169+
);
170+
});
171+
test('opens when clicked', () => {
172+
expect(
173+
screen.getByText(`Edit ${EXAMPLE_SNOMED_CODE}`)
174+
).toBeInTheDocument();
175+
});
176+
177+
test('disables save state when any one of the three required fields is missing', async () => {
178+
const saveButton = screen.getByRole('button', { name: 'Save changes' });
179+
expect(saveButton).toBeEnabled();
180+
181+
const code = screen.getByLabelText('Code');
182+
await userEvent.clear(code);
183+
expect(saveButton).toBeDisabled();
184+
185+
await userEvent.type(code, '13535135');
186+
const codeName = screen.getByLabelText('Code name');
187+
188+
await userEvent.clear(codeName);
189+
expect(saveButton).toBeDisabled();
190+
191+
await userEvent.type(codeName, 'SNOMED Example');
192+
expect(saveButton).toBeEnabled();
193+
});
194+
});
195+
});
196+
197+
function checkAllCodesExistence() {
198+
checkSnomedCode();
199+
checkOtherCode();
200+
checkIcd10Code();
201+
checkLoincCode();
202+
checkLoincCode();
203+
checkCvxCode();
204+
}
205+
206+
function checkLoincCode(exists = true) {
207+
checkCode('LOINC', exists);
208+
}
209+
210+
function checkSnomedCode(exists = true) {
211+
checkCode('SNOMED', exists);
212+
}
213+
214+
function checkCvxCode(exists = true) {
215+
checkCode('CVX', exists);
216+
}
217+
218+
function checkIcd10Code(exists = true) {
219+
checkCode('ICD-10', exists);
220+
}
221+
function checkRxNormCode(exists = true) {
222+
checkCode('RxNorm', exists);
223+
}
224+
225+
function checkOtherCode(exists = true) {
226+
checkCode('Other', exists);
227+
}
228+
229+
function checkCode(codeSystemName: string, exists = true) {
230+
const mockCode = mockPreviewItems.find((i) => i.system_key === codeSystemName)
231+
?.code as string;
232+
233+
const matcher = exists ? 'getByText' : 'queryByText';
234+
235+
const totalExpectation = (text: string) => {
236+
const assertion = expect(screen[matcher](text));
237+
return exists
238+
? assertion.toBeInTheDocument()
239+
: assertion.not.toBeInTheDocument();
240+
};
241+
242+
totalExpectation(mockCode);
243+
totalExpectation(`${codeSystemName} Example`);
244+
}
245+
246+
const uploadLines = csvToDict(CSV_DOWNLOAD_TEMPLATE);
247+
const mockPreviewItems: UploadCustomCodesPreviewItem[] = uploadLines.map(
248+
(row, i) => {
249+
return {
250+
id: crypto.randomUUID(),
251+
code: row['code'],
252+
system_key: row['code_system'],
253+
name: row['display_name'],
254+
row: i,
255+
};
256+
}
257+
);
258+
259+
const mockUploadResponse: UploadCustomCodesPreviewResponse = {
260+
preview_items: mockPreviewItems,
261+
codes_processed: mockPreviewItems.length,
262+
total_custom_codes_in_configuration: mockPreviewItems.length,
263+
code_systems: mockIndexedSystem,
264+
};
265+
266+
function csvToDict(csv: string) {
267+
const lines = csv.trim().split('\n');
268+
const headers = lines[0].split(',').map((h) => h.trim());
269+
const indexedValues: Record<string, string>[] = [];
270+
lines.slice(1).forEach((line) => {
271+
const rowObject: Record<string, string> = {};
272+
const values = line.split(',');
273+
274+
headers.forEach((_, j) => {
275+
rowObject[headers[j]] = values[j];
276+
});
277+
indexedValues.push(rowObject);
278+
});
279+
280+
return indexedValues;
281+
}

0 commit comments

Comments
 (0)