From ec7db795fdae497889913fdf7d2bbdbbf150c9a8 Mon Sep 17 00:00:00 2001 From: Dennis Kigen Date: Fri, 12 Jul 2024 12:53:28 +0300 Subject: [PATCH] (test) Fix flaky behaviour in Programs and Conditions form tests (#1909) * (test) Fix flaky behaviour in Programs and Conditions form tests * Try something else * Fixup --- __mocks__/programs.mock.ts | 8 ++ .../src/conditions/conditions-form.test.tsx | 95 ++++++++------- .../conditions-widget.component.tsx | 4 +- .../src/dashboard.meta.ts | 2 - .../src/programs/programs-form.test.tsx | 113 ++++++++---------- .../src/programs/programs-form.workspace.tsx | 43 +++---- .../src/programs/programs.resource.tsx | 8 +- .../src/types/index.ts | 8 +- .../translations/en.json | 2 +- 9 files changed, 135 insertions(+), 148 deletions(-) diff --git a/__mocks__/programs.mock.ts b/__mocks__/programs.mock.ts index f273501e52..1fb5702e5c 100644 --- a/__mocks__/programs.mock.ts +++ b/__mocks__/programs.mock.ts @@ -50,6 +50,10 @@ export const mockEnrolledProgramsResponse = [ uuid: '64f950e6-1b07-4ac0-8e7e-f3e148f3463f', name: 'HIV Care and Treatment', allWorkflows: [], + concept: { + uuid: '70724784-438a-490e-a581-68b7d1f8f47f', + display: 'Human immunodeficiency virus (HIV) disease', + }, }, display: 'HIV Care and Treatment', location: { @@ -68,6 +72,10 @@ export const mockEnrolledInAllProgramsResponse = [ uuid: '64f950e6-1b07-4ac0-8e7e-f3e148f3463f', name: 'HIV Care and Treatment', allWorkflows: [], + concept: { + uuid: '70724784-438a-490e-a581-68b7d1f8f47f', + display: 'Human immunodeficiency virus (HIV) disease', + }, }, display: 'HIV Care and Treatment', location: { diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx index 109b17f681..946a0094f3 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx @@ -21,38 +21,36 @@ const testProps = { setTitle: jest.fn(), }; -const mockCreateCondition = createCondition as jest.Mock; -const mockUseConditionsSearch = useConditionsSearch as jest.Mock; -const mockShowSnackbar = showSnackbar as jest.Mock; +const mockCreateCondition = jest.mocked(createCondition); +const mockUseConditionsSearch = jest.mocked(useConditionsSearch); +const mockShowSnackbar = jest.mocked(showSnackbar); const mockOpenmrsFetch = jest.mocked(openmrsFetch); -jest.mock('@openmrs/esm-framework', () => { - const originalModule = jest.requireActual('@openmrs/esm-framework'); +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + showSnackbar: jest.fn(), +})); - return { - ...originalModule, - showSnackbar: jest.fn(), - }; -}); +jest.mock('./conditions.resource', () => ({ + ...jest.requireActual('./conditions.resource'), + createCondition: jest.fn(), + editCondition: jest.fn(), + useConditionsSearch: jest.fn(), +})); + +describe('Conditions form', () => { + beforeEach(() => { + jest.clearAllMocks(); -jest.mock('./conditions.resource', () => { - const originalModule = jest.requireActual('./conditions.resource'); + mockOpenmrsFetch.mockResolvedValue({ data: [] } as FetchResponse); - return { - ...originalModule, - createCondition: jest.fn(), - editCondition: jest.fn(), - useConditionsSearch: jest.fn().mockImplementation(() => ({ - conditions: [], + mockUseConditionsSearch.mockReturnValue({ + searchResults: [], error: null, isSearching: false, - })), - }; -}); + }); -describe('Conditions form', () => { - beforeEach(() => { - mockShowSnackbar.mockClear(); + mockCreateCondition.mockResolvedValue({ status: 201, body: 'Condition created' } as unknown as FetchResponse); }); it('renders the conditions form with all the relevant fields and values', () => { @@ -123,13 +121,15 @@ describe('Conditions form', () => { it('renders a success notification upon successfully recording a condition', async () => { const user = userEvent.setup(); - mockOpenmrsFetch.mockResolvedValue({ data: [] } as FetchResponse); - mockCreateCondition.mockResolvedValue({ status: 201, body: 'Condition created' }); mockUseConditionsSearch.mockReturnValue({ searchResults: searchedCondition, error: null, isSearching: false, }); + mockOpenmrsFetch.mockResolvedValue({ + data: mockFhirConditionsResponse, + mutate: Promise.resolve(undefined), + } as unknown as FetchResponse); renderConditionsForm(); @@ -146,17 +146,23 @@ describe('Conditions form', () => { await user.type(onsetDateInput, '2020-05-05'); await user.click(submitButton); - // TODO: Figure out why the following assertions are flaky - // expect(mockShowSnackbar).toHaveBeenCalled(); - // expect(mockShowSnackbar).toHaveBeenCalledWith({ - // kind: 'success', - // subtitle: 'It is now visible on the Conditions page', - // title: 'Condition saved', - // }); + expect(mockShowSnackbar).toHaveBeenCalled(); + expect(mockShowSnackbar).toHaveBeenCalledWith({ + kind: 'success', + subtitle: 'It is now visible on the Conditions page', + title: 'Condition saved', + }); }); it('renders an error notification if there was a problem recording a condition', async () => { const user = userEvent.setup(); + + mockUseConditionsSearch.mockReturnValue({ + searchResults: searchedCondition, + error: null, + isSearching: false, + }); + renderConditionsForm(); const submitButton = screen.getByRole('button', { name: /save & close/i }); @@ -172,7 +178,7 @@ describe('Conditions form', () => { }, }; - mockCreateCondition.mockImplementation(() => Promise.reject(error)); + mockCreateCondition.mockRejectedValue(error); await user.type(conditionSearchInput, 'Headache'); await user.click(screen.getByRole('menuitem', { name: /Headache/i })); await user.type(onsetDateInput, '2020-05-05'); @@ -185,13 +191,15 @@ describe('Conditions form', () => { it('validates the form against the provided zod schema before submitting it', async () => { const user = userEvent.setup(); - mockOpenmrsFetch.mockResolvedValue({ data: [] } as FetchResponse); - mockCreateCondition.mockResolvedValue({ status: 201, body: 'Condition created' }); mockUseConditionsSearch.mockReturnValue({ searchResults: searchedCondition, error: null, isSearching: false, }); + mockOpenmrsFetch.mockResolvedValue({ + data: mockFhirConditionsResponse, + mutate: Promise.resolve(undefined), + } as unknown as FetchResponse); renderConditionsForm(); @@ -215,13 +223,12 @@ describe('Conditions form', () => { expect(screen.queryByText(/a condition is required/i)).not.toBeInTheDocument(); expect(screen.queryByText(/a clinical status is required/i)).not.toBeInTheDocument(); - // TODO: Figure out why the following assertions are flaky - // expect(mockShowSnackbar).toHaveBeenCalled(); - // expect(mockShowSnackbar).toHaveBeenCalledWith({ - // kind: 'success', - // subtitle: 'It is now visible on the Conditions page', - // title: 'Condition saved', - // }); + expect(mockShowSnackbar).toHaveBeenCalled(); + expect(mockShowSnackbar).toHaveBeenCalledWith({ + kind: 'success', + subtitle: 'It is now visible on the Conditions page', + title: 'Condition saved', + }); }); it('launching the form with an existing condition prepopulates the form with the condition details', async () => { @@ -243,7 +250,7 @@ describe('Conditions form', () => { mockOpenmrsFetch.mockResolvedValue({ data: mockFhirConditionsResponse } as FetchResponse); renderConditionsForm(); - expect(screen.queryByRole('searchbox', { name: /Enter condition/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('searchbox', { name: /enter condition/i })).not.toBeInTheDocument(); const inactiveStatusInput = screen.getByLabelText(/inactive/i); const submitButton = screen.getByRole('button', { name: /save & close/i }); diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx index 3775657b7a..37e785e7e2 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx @@ -111,7 +111,7 @@ const ConditionsWidget: React.FC = ({ try { await createCondition(payload); - mutate(); + await mutate(); showSnackbar({ kind: 'success', @@ -153,7 +153,7 @@ const ConditionsWidget: React.FC = ({ try { await updateCondition(conditionToEdit?.id, payload); - mutate(); + await mutate(); showSnackbar({ kind: 'success', diff --git a/packages/esm-patient-programs-app/src/dashboard.meta.ts b/packages/esm-patient-programs-app/src/dashboard.meta.ts index a3cc6901b5..063351513a 100644 --- a/packages/esm-patient-programs-app/src/dashboard.meta.ts +++ b/packages/esm-patient-programs-app/src/dashboard.meta.ts @@ -1,5 +1,3 @@ -export const moduleName = '@openmrs/esm-patient-programs-app'; - export const dashboardMeta = { slot: 'patient-chart-programs-dashboard-slot', columns: 1, diff --git a/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx b/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx index 0cf0acbed8..cbc4e72216 100644 --- a/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx +++ b/packages/esm-patient-programs-app/src/programs/programs-form.test.tsx @@ -1,17 +1,26 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from '@testing-library/react'; -import { openmrsFetch, showSnackbar } from '@openmrs/esm-framework'; -import { mockPatient } from 'tools'; +import { type FetchResponse, showSnackbar } from '@openmrs/esm-framework'; import { mockCareProgramsResponse, mockEnrolledProgramsResponse, mockLocationsResponse } from '__mocks__'; -import { createProgramEnrollment, updateProgramEnrollment } from './programs.resource'; +import { mockPatient } from 'tools'; +import { + createProgramEnrollment, + updateProgramEnrollment, + useAvailablePrograms, + useEnrollments, +} from './programs.resource'; import ProgramsForm from './programs-form.workspace'; -const mockCreateProgramEnrollment = createProgramEnrollment as jest.Mock; -const mockUpdateProgramEnrollment = updateProgramEnrollment as jest.Mock; -const mockOpenmrsFetch = openmrsFetch as jest.Mock; -const mockShowSnackbar = showSnackbar as jest.Mock; +const mockUseAvailablePrograms = jest.mocked(useAvailablePrograms); +const mockUseEnrollments = jest.mocked(useEnrollments); +const mockCreateProgramEnrollment = jest.mocked(createProgramEnrollment); +const mockUpdateProgramEnrollment = jest.mocked(updateProgramEnrollment); +const mockShowSnackbar = jest.mocked(showSnackbar); + +const mockCloseWorkspace = jest.fn(); const mockCloseWorkspaceWithSavedChanges = jest.fn(); +const mockPromptBeforeClosing = jest.fn(); jest.mock('@openmrs/esm-framework', () => ({ ...jest.requireActual('@openmrs/esm-framework'), @@ -20,20 +29,36 @@ jest.mock('@openmrs/esm-framework', () => ({ })); jest.mock('./programs.resource', () => ({ - ...jest.requireActual('./programs.resource'), createProgramEnrollment: jest.fn(), updateProgramEnrollment: jest.fn(), - useEnrollments: jest.fn().mockReturnValue({ - data: mockEnrolledProgramsResponse, - isLoading: false, - isError: false, - mutateEnrollments: jest.fn().mockResolvedValue(undefined), - }), + useAvailablePrograms: jest.fn(), + useEnrollments: jest.fn(), })); describe('ProgramsForm', () => { beforeEach(() => { jest.clearAllMocks(); + + mockUseAvailablePrograms.mockReturnValue({ + data: mockCareProgramsResponse, + eligiblePrograms: [], + error: null, + isLoading: false, + }); + + mockUseEnrollments.mockReturnValue({ + data: mockEnrolledProgramsResponse, + error: null, + isLoading: false, + isValidating: false, + activeEnrollments: [], + mutateEnrollments: jest.fn(), + }); + + mockCreateProgramEnrollment.mockResolvedValue({ + status: 201, + statusText: 'Created', + } as unknown as FetchResponse); }); it('renders a success toast notification upon successfully recording a program enrollment', async () => { @@ -42,10 +67,6 @@ describe('ProgramsForm', () => { const inpatientWardUuid = 'b1a8b05e-3542-4037-bbd3-998ee9c40574'; const oncologyScreeningProgramUuid = '11b129ca-a5e7-4025-84bf-b92a173e20de'; - mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockCareProgramsResponse } }); - mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledProgramsResponse } }); - mockCreateProgramEnrollment.mockResolvedValueOnce({ status: 201, statusText: 'Created' }); - renderProgramsForm(); const programNameInput = screen.getByRole('combobox', { name: /program name/i }); @@ -54,12 +75,11 @@ describe('ProgramsForm', () => { const enrollButton = screen.getByRole('button', { name: /save and close/i }); await user.click(enrollButton); - expect(screen.getByText(/programrequired/i)).toBeInTheDocument(); + expect(screen.getByText(/program is required/i)).toBeInTheDocument(); await user.type(enrollmentDateInput, '2020-05-05'); await user.selectOptions(programNameInput, [oncologyScreeningProgramUuid]); await user.selectOptions(enrollmentLocationInput, [inpatientWardUuid]); - expect(screen.getByRole('option', { name: /Inpatient Ward/i })).toBeInTheDocument(); await user.click(enrollButton); @@ -92,7 +112,10 @@ describe('ProgramsForm', () => { const enrollButton = screen.getByRole('button', { name: /save and close/i }); const completionDateInput = screen.getByRole('textbox', { name: /date completed/i }); - mockUpdateProgramEnrollment.mockResolvedValueOnce({ status: 200, statusText: 'OK' }); + mockUpdateProgramEnrollment.mockResolvedValue({ + status: 200, + statusText: 'OK', + } as unknown as FetchResponse); await user.type(completionDateInput, '05/05/2020'); await user.tab(); @@ -102,8 +125,8 @@ describe('ProgramsForm', () => { expect(mockUpdateProgramEnrollment).toHaveBeenCalledWith( mockEnrolledProgramsResponse[0].uuid, expect.objectContaining({ - dateEnrolled: '2020-01-16T00:00:00+00:00', - dateCompleted: '2020-05-05T00:00:00+00:00', + dateCompleted: expect.stringMatching(/^2020-05-05/), + dateEnrolled: expect.stringMatching(/^2020-01-16/), location: mockEnrolledProgramsResponse[0].location.uuid, patient: mockPatient.id, program: mockEnrolledProgramsResponse[0].program.uuid, @@ -119,54 +142,14 @@ describe('ProgramsForm', () => { }), ); }); - - it('renders an error notification if there was a problem recording a program enrollment', async () => { - const user = userEvent.setup(); - - const inpatientWardUuid = 'b1a8b05e-3542-4037-bbd3-998ee9c40574'; - const oncologyScreeningProgramUuid = '11b129ca-a5e7-4025-84bf-b92a173e20de'; - - const error = { - message: 'Internal Server Error', - response: { - status: 500, - statusText: 'Internal Server Error', - }, - }; - - mockOpenmrsFetch.mockReturnValue({ data: { results: mockCareProgramsResponse } }); - mockOpenmrsFetch.mockReturnValue({ data: { results: mockEnrolledProgramsResponse } }); - mockCreateProgramEnrollment.mockRejectedValueOnce(error); - - renderProgramsForm(); - - const programNameInput = screen.getByRole('combobox', { name: /program name/i }); - const enrollmentDateInput = screen.getByRole('textbox', { name: /date enrolled/i }); - const enrollmentLocationInput = screen.getByRole('combobox', { name: /enrollment location/i }); - const enrollButton = screen.getByRole('button', { name: /save and close/i }); - - await user.type(enrollmentDateInput, '2020-05-05'); - await user.selectOptions(programNameInput, [oncologyScreeningProgramUuid]); - await user.selectOptions(enrollmentLocationInput, [inpatientWardUuid]); - - expect(enrollButton).toBeEnabled(); - - await user.click(enrollButton); - - expect(mockShowSnackbar).toHaveBeenCalledWith({ - subtitle: 'An unknown error occurred', - kind: 'error', - title: 'Error saving program enrollment', - }); - }); }); function renderProgramsForm(programEnrollmentUuidToEdit?: string) { const testProps = { - closeWorkspace: jest.fn(), + closeWorkspace: mockCloseWorkspace, closeWorkspaceWithSavedChanges: mockCloseWorkspaceWithSavedChanges, patientUuid: mockPatient.id, - promptBeforeClosing: jest.fn(), + promptBeforeClosing: mockPromptBeforeClosing, setTitle: jest.fn(), }; diff --git a/packages/esm-patient-programs-app/src/programs/programs-form.workspace.tsx b/packages/esm-patient-programs-app/src/programs/programs-form.workspace.tsx index b5db1e2aa8..f96a8df59f 100644 --- a/packages/esm-patient-programs-app/src/programs/programs-form.workspace.tsx +++ b/packages/esm-patient-programs-app/src/programs/programs-form.workspace.tsx @@ -1,7 +1,9 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { type TFunction, useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import filter from 'lodash-es/filter'; +import includes from 'lodash-es/includes'; +import map from 'lodash-es/map'; import { Button, ButtonSet, @@ -19,14 +21,7 @@ import { import { z } from 'zod'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { - parseDate, - showSnackbar, - translateFrom, - useLayoutType, - useLocations, - useSession, -} from '@openmrs/esm-framework'; +import { parseDate, showSnackbar, useLayoutType, useLocations, useSession } from '@openmrs/esm-framework'; import { type DefaultPatientWorkspaceProps } from '@openmrs/esm-patient-common-lib'; import { createProgramEnrollment, @@ -34,23 +29,21 @@ import { useEnrollments, updateProgramEnrollment, } from './programs.resource'; -import { moduleName } from '../dashboard.meta'; import styles from './programs-form.scss'; interface ProgramsFormProps extends DefaultPatientWorkspaceProps { programEnrollmentId?: string; } -const programsFormSchema = z.object({ - selectedProgram: z - .string() - .refine((value) => !!value, translateFrom(moduleName, 'programRequired', 'Program is required')), - enrollmentDate: z.date(), - completionDate: z.date().nullable(), - enrollmentLocation: z.string(), -}); +const createProgramsFormSchema = (t: TFunction) => + z.object({ + selectedProgram: z.string().refine((value) => !!value, t('programRequired', 'Program is required')), + enrollmentDate: z.date(), + completionDate: z.date().nullable(), + enrollmentLocation: z.string(), + }); -export type ProgramsFormData = z.infer; +export type ProgramsFormData = z.infer>; const ProgramsForm: React.FC = ({ closeWorkspace, @@ -67,6 +60,8 @@ const ProgramsForm: React.FC = ({ const { data: enrollments, mutateEnrollments } = useEnrollments(patientUuid); const [isSubmittingForm, setIsSubmittingForm] = useState(false); + const programsFormSchema = useMemo(() => createProgramsFormSchema(t), [t]); + const currentEnrollment = programEnrollmentId && enrollments.filter((e) => e.uuid === programEnrollmentId)[0]; const currentProgram = currentEnrollment ? { @@ -77,11 +72,7 @@ const ProgramsForm: React.FC = ({ const eligiblePrograms = currentProgram ? [currentProgram] - : filter(availablePrograms, (program) => { - const existingEnrollment = enrollments.find((e) => e.program.uuid === program.uuid); - - return !existingEnrollment || existingEnrollment.dateCompleted !== null; - }); + : filter(availablePrograms, (program) => !includes(map(enrollments, 'program.uuid'), program.uuid)); const getLocationUuid = () => { if (!currentEnrollment?.location.uuid && session?.sessionLocation?.uuid) { @@ -165,7 +156,7 @@ const ProgramsForm: React.FC = ({