Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix tests coverage #1522

Merged
merged 10 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/cd-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
matrix:
node-version: [22.x]
os: [ubuntu-latest, windows-latest]
allow-failure: [false, true]
fail-fast: false # Ensure the workflow doesn't stop when one matrix combination fails
runs-on: ${{ matrix.os }}

steps:
Expand Down Expand Up @@ -49,3 +51,5 @@ jobs:
directory: ./coverage
fail_ci_if_error: true
verbose: true

continue-on-error: ${{ matrix.os == 'windows-latest' }}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { ReactNode } from 'react';
import { CareTeamForm } from '../Form';
import { defaultInitialValues, getCareTeamFormFields } from '../utils';
import { cleanup, fireEvent, waitFor, render, screen } from '@testing-library/react';
Expand All @@ -21,7 +21,7 @@ import { store } from '@opensrp/store';
import { authenticateUser } from '@onaio/session-reducer';
import { QueryClientProvider, QueryClient } from 'react-query';
import { Router } from 'react-router';
import { createMemoryHistory } from 'history';
import { createMemoryHistory, MemoryHistory } from 'history';

jest.mock('@opensrp/notifications', () => ({
__esModule: true,
Expand Down Expand Up @@ -49,9 +49,13 @@ const queryClient = new QueryClient({
},
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const AppWrapper = ({ children }: { children: any }) => {
const history = createMemoryHistory();
const AppWrapper = ({
children,
history = createMemoryHistory(),
}: {
children: ReactNode;
history?: MemoryHistory;
}) => {
return (
<Router history={history}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
Expand Down Expand Up @@ -233,3 +237,70 @@ test('1157 - editing care team works corectly', async () => {

expect(nock.isDone()).toBeTruthy();
});

test('Errors out with message and cancel redirects correctly', async () => {
const history = createMemoryHistory();
const errorNoticeMock = jest
.spyOn(notifications, 'sendErrorNotification')
.mockImplementation(() => undefined);

const preloadScope = nock(props.fhirBaseURL)
.get(`/${organizationResourceType}/_search`)
.query({ _getpagesoffset: '0', _count: '20' })
.reply(200, organizations)
.get(`/${practitionerResourceType}/_search`)
.query({ _getpagesoffset: '0', _count: '20' })
.reply(200, practitioners);

nock(props.fhirBaseURL)
.put(`/${careTeamResourceType}/${createdCareTeam2.id}`, createdCareTeam2)
.replyWithError('Not taking requests at this time')
.persist();

render(
<AppWrapper history={history}>
<CareTeamForm {...props} />
</AppWrapper>
);

await waitFor(() => {
expect(preloadScope.pendingMocks()).toEqual([]);
});
await waitFor(() => {
expect(screen.getByText(/Create Care Team/)).toBeInTheDocument();
});

const nameInput = screen.getByLabelText('Name') as Element;
userEvents.type(nameInput, 'care team');

const activeStatusRadio = screen.getByLabelText('Active');
expect(activeStatusRadio).toBeChecked();

const inactiveStatusRadio = screen.getByLabelText('Inactive');
expect(inactiveStatusRadio).not.toBeChecked();
userEvents.click(inactiveStatusRadio);

const practitionersInput = screen.getByLabelText('Practitioner Participant');
fireEvent.mouseDown(practitionersInput);
fireEvent.click(screen.getByTitle('Ward N 2 Williams MD'));

const managingOrgsSelect = screen.getByLabelText('Managing organizations');
fireEvent.mouseDown(managingOrgsSelect);
fireEvent.click(screen.getByTitle('Test Team 70'));

const saveBtn = screen.getByRole('button', { name: 'Save' });
userEvents.click(saveBtn);

await waitFor(() => {
expect(errorNoticeMock.mock.calls).toEqual([['There was a problem creating the Care Team']]);
});

// cancel form
const cancelButton = screen.getByRole('button', {
name: /cancel/i,
});
fireEvent.click(cancelButton);
expect(history.location.pathname).toEqual('/admin/CareTeams');

expect(nock.isDone()).toBeTruthy();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getPatientName } from '../utils';

test('getPatientName works for undefined input', () => {
expect(getPatientName()).toEqual('');
});
Original file line number Diff line number Diff line change
Expand Up @@ -221,16 +221,6 @@ export interface SelectOptions {
label?: string;
}

/**
* filter practitioners select on search
*
* @param inputValue search term
* @param option select option to filter against
*/
export const selectFilterFunction = (inputValue: string, option?: SelectOptions) => {
return !!option?.label?.toLowerCase().includes(inputValue.toLowerCase());
};

/**
* creates util function that given a set of resource ids, it can fetch
* just those resources whose id are provided
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { IPatient } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IPatient';
import { sorterFn } from '../../../helpers/utils';
import { getPatientName, getPatientStatus, patientStatusColor } from '../../PatientsList/utils';
import { getPatientName, getPatientStatus } from '../../PatientsList/utils';
import { Button, Tag, Typography } from 'antd';
import { Column, ResourceDetailsProps, dateToLocaleString } from '@opensrp/react-utils';
import type { TFunction } from '@opensrp/i18n';
Expand Down Expand Up @@ -145,15 +145,15 @@ export function patientDetailsProps(
[t('Address')]: get(resource, 'address.0.line.0') || 'N/A',
[t('Country')]: get(resource, 'address.0.country'),
};
const patientStatus = getPatientStatus(active as boolean, deceasedBoolean as boolean);
const patientStatus = getPatientStatus(active as boolean, deceasedBoolean as boolean, t);
return {
title: patientName,
headerRightData,
headerLeftData,
bodyData,
status: {
title: patientStatus,
color: patientStatusColor[patientStatus],
title: patientStatus.title,
color: patientStatus.color,
},
};
}
100 changes: 100 additions & 0 deletions packages/fhir-client/src/components/PatientsList/tests/utils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
getPatientName,
getPatientStatus,
getObservationLabel,
buildObservationValueString,
} from '../utils';

test('get patient name for undefined', () => {
expect(getPatientName()).toEqual('');
});

test('getPatientStatus works correctly', () => {
const t = (x: string) => x;
expect(getPatientStatus(true, true, t)).toEqual({ title: t('Deceased'), color: 'red' });
expect(getPatientStatus(true, false, t)).toEqual({ title: t('Active'), color: 'green' });
expect(getPatientStatus(false, true, t)).toEqual({ title: t('Deceased'), color: 'red' });
expect(getPatientStatus(false, true, t)).toEqual({ title: t('Deceased'), color: 'red' });
expect(getPatientStatus(false, false, t)).toEqual({ title: t('Inactive'), color: 'gray' });
});

describe('getObservationLabel', () => {
test('returns display property if available', () => {
const resource = {
code: { coding: [{ display: 'Test Display' }] },
};
expect(getObservationLabel(resource)).toBe('Test Display');
});

test('returns text property if display is missing', () => {
const resource = {
code: { text: 'Test Text' },
};
expect(getObservationLabel(resource)).toBe('Test Text');
});

test('returns valueQuantity.code if display and text are missing', () => {
const resource = {
valueQuantity: { code: 'Test Code' },
};
expect(getObservationLabel(resource)).toBe('Test Code');
});

test('returns undefined if all properties are missing', () => {
const resource = {};
expect(getObservationLabel(resource)).toBeUndefined();
});
});

describe('buildObservationValueString', () => {
test('builds value string for array component with labels and values', () => {
const resource = {
component: [
{
code: { coding: [{ display: 'Blood Pressure Systolic' }] },
valueQuantity: { value: 120, unit: 'mmHg' },
},
{
code: { coding: [{ display: 'Blood Pressure Diastolic' }] },
valueQuantity: { value: 80, unit: 'mmHg' },
},
],
};
expect(buildObservationValueString(resource)).toBe(' Systolic: 120mmHg, Diastolic: 80mmHg');
});

test('handles non-array component gracefully', () => {
const resource = {
valueQuantity: { value: 98.6, unit: '°F' },
};
expect(buildObservationValueString(resource)).toBe('98.6 °F');
});

test('returns N/A if valueQuantity is missing in non-array component', () => {
const resource = {};
expect(buildObservationValueString(resource)).toBe(' ');
});

test('handles missing unit or value gracefully in array component', () => {
const resource = {
component: [
{
code: { coding: [{ display: 'Blood Pressure Systolic' }] },
valueQuantity: { value: 120 },
},
{
code: { coding: [{ display: 'Blood Pressure Diastolic' }] },
valueQuantity: { unit: 'mmHg' },
},
],
};
expect(buildObservationValueString(resource)).toBe(' Systolic: 120, Diastolic: mmHg');
});

test('handles empty array component gracefully', () => {
const resource = {
component: [],
};
expect(buildObservationValueString(resource)).toBe('');
});
});
41 changes: 11 additions & 30 deletions packages/fhir-client/src/components/PatientsList/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,19 @@ import { Dictionary } from '@onaio/utils';
import get from 'lodash/get';
import { IPatient } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IPatient';
import { parseFhirHumanName } from '@opensrp/react-utils';
import { TFunction } from '@opensrp/i18n';

export enum PatientStatus {
ACTIVE = 'Active',
InACTIVE = 'Inactive',
DECEASED = 'Deceased',
}

export const patientStatusColor = {
[PatientStatus.ACTIVE]: 'green',
[PatientStatus.InACTIVE]: 'gray',
[PatientStatus.DECEASED]: 'red',
};

/**
* util to extract patient name
*
* @param patient - patient object
* @returns {string[]} - returns an array of name strings
* @returns - returns an array of name strings
*/
export function getPatientName(patient?: IPatient) {
if (!patient) {
Expand All @@ -29,21 +24,6 @@ export function getPatientName(patient?: IPatient) {
return parseFhirHumanName(name);
}

/**
* Walks thru an object (ar array) and returns the value found at the provided
* path. This function is very simple so it intentionally does not support any
* argument polymorphism, meaning that the path can only be a dot-separated
* string. If the path is invalid returns undefined.
*
* @param {Object} obj The object (or Array) to walk through
* @param {string} path The path (eg. "a.b.4.c")
* @returns {*} Whatever is found in the path or undefined
*/
export function getPath(obj: Dictionary, path = '') {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return path.split('.').reduce((out, key) => (out ? out[key] : undefined), obj);
}

/**
* Function to get observation label
*
Expand All @@ -59,8 +39,8 @@ export function getObservationLabel(obj: Dictionary): string {
/**
* Function to get observation value quantity
*
* @param {Object} obj - resource object
* @returns {string} - returns value string
* @param obj - resource object
* @returns - returns value string
*/
export function buildObservationValueString(obj: Dictionary): string {
let quantValue = '';
Expand All @@ -82,15 +62,16 @@ export function buildObservationValueString(obj: Dictionary): string {
/**
* Function to get patient status based on active and deceased status
*
* @param {boolean} isActive - Patient active status
* @param {boolean} isDeceased - Patient deceased status
* @param isActive - Patient active status
* @param isDeceased - Patient deceased status
* @param t - translator function
*/
export const getPatientStatus = (isActive: boolean, isDeceased: boolean) => {
export const getPatientStatus = (isActive: boolean, isDeceased: boolean, t: TFunction) => {
if (isDeceased) {
return PatientStatus.DECEASED;
return { title: t('Deceased'), color: 'red' };
}
if (isActive) {
return PatientStatus.ACTIVE;
return { title: t('Active'), color: 'green' };
}
return PatientStatus.InACTIVE;
return { title: t('Inactive'), color: 'gray' };
};
Loading
Loading