@kenyaemr/esm-admin-app 5.4.5-pre.795 → 5.4.5-pre.798
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/4124.js +1 -1
- package/dist/4124.js.map +1 -1
- package/dist/5117.js +1 -1
- package/dist/5117.js.map +1 -1
- package/dist/5767.js +1 -1
- package/dist/5767.js.map +1 -1
- package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +9 -9
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/components/facility-setup/facility-info.component.test.tsx +116 -0
- package/src/components/global-property/dashboard/global-property-dashboard.test.tsx +112 -0
- package/src/components/locations/auto-suggest/autosuggest.component.test.tsx +90 -0
- package/src/components/locations/auto-suggest/location-autosuggest.component.test.tsx +76 -0
- package/src/components/locations/common/results-tile.component.test.tsx +65 -0
- package/src/components/modal/hwr-empty.modal.test.tsx +67 -0
- package/src/components/provider-banner/provider-banner.component.tsx +35 -23
- package/src/components/provider-banner/provider-banner.test.tsx +109 -0
- package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-list/user-role-scope-list.component.test.tsx +154 -0
- package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope-fields.component.test.tsx +161 -0
- package/src/components/users/manage-users/user-details/user-details.component.test.tsx +135 -0
- package/src/components/users/manage-users/user-list/user-list.component.test.tsx +172 -0
- package/src/components/users/manage-users/user-list/user-list.component.tsx +9 -14
- package/src/left-pannel-link.test.tsx +69 -0
- package/src/root.component.test.tsx +394 -0
- package/src/testing/openmrs-fetch-mock.tsx +253 -0
- package/translations/en.json +2 -0
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemrCharts":"^1.6.7"},"extensions":[{"component":"adminLeftPanelLink","name":"admin-left-panel-link","slot":"admin-left-panel-slot"},{"component":"userManagementLeftPannelLink","name":"user-management-left-panel-link","slot":"admin-left-panel-slot"},{"component":"etlAdministrationLeftPannelLink","name":"etl-administration-left-panel-link","slot":"admin-left-panel-slot"},{"component":"locationsLeftPanelLink","name":"locations-left-panel-link","slot":"admin-left-panel-slot"},{"component":"facilitySetupLeftPanelLink","name":"facility-setup-left-panel-link","slot":"admin-left-panel-slot"},{"name":"global-property-link","component":"globalPropertyLink","slot":"admin-left-panel-slot"},{"component":"providerBanner","name":"provider-banner","slot":"provider-banner-info-slot","order":1},{"component":"globalPropertyDashboard","name":"global-property-dashboard"}],"modals":[{"component":"operationConfirmationModal","name":"operation-confirmation-modal"},{"component":"hwrConfirmationModal","name":"hwr-confirmation-modal"},{"component":"hwrEmptyModal","name":"hwr-empty-modal"},{"component":"hwrSyncModal","name":"hwr-syncing-modal"},{"component":"deleteGlobalPropertyModal","name":"delete-global-property-modal"}],"workspaces2":[{"name":"manage-user-workspace","component":"manageUserWorkspace","window":"esm-admin-extra-wide-workspace-window"},{"name":"user-role-scope-workspace","component":"userRoleScopeWorkspace","window":"esm-admin-workspace-window"},{"name":"add-location-workspace","component":"addLocation","window":"esm-admin-workspace-window"},{"name":"search-location-workspace","component":"searchLocationWorkspace","window":"esm-admin-workspace-window"},{"name":"global-property-workspace","component":"globalPropertyWorkspace","window":"esm-admin-workspace-window"},{"name":"upload-logo-workspace","component":"uploadLogoWorkspace","window":"esm-admin-workspace-window"}],"workspaceWindows2":[{"name":"esm-admin-workspace-window","canMaximize":true,"group":"esm-admin-workspace-group","width":"wider"},{"name":"esm-admin-extra-wide-workspace-window","canMaximize":true,"group":"esm-admin-workspace-group","width":"extra-wide"}],"workspaceGroups2":[{"name":"esm-admin-workspace-group","scopePattern":"/admin","closeable":true}],"pages":[{"component":"root","route":"admin"}],"version":"5.4.5-pre.
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemrCharts":"^1.6.7"},"extensions":[{"component":"adminLeftPanelLink","name":"admin-left-panel-link","slot":"admin-left-panel-slot"},{"component":"userManagementLeftPannelLink","name":"user-management-left-panel-link","slot":"admin-left-panel-slot"},{"component":"etlAdministrationLeftPannelLink","name":"etl-administration-left-panel-link","slot":"admin-left-panel-slot"},{"component":"locationsLeftPanelLink","name":"locations-left-panel-link","slot":"admin-left-panel-slot"},{"component":"facilitySetupLeftPanelLink","name":"facility-setup-left-panel-link","slot":"admin-left-panel-slot"},{"name":"global-property-link","component":"globalPropertyLink","slot":"admin-left-panel-slot"},{"component":"providerBanner","name":"provider-banner","slot":"provider-banner-info-slot","order":1},{"component":"globalPropertyDashboard","name":"global-property-dashboard"}],"modals":[{"component":"operationConfirmationModal","name":"operation-confirmation-modal"},{"component":"hwrConfirmationModal","name":"hwr-confirmation-modal"},{"component":"hwrEmptyModal","name":"hwr-empty-modal"},{"component":"hwrSyncModal","name":"hwr-syncing-modal"},{"component":"deleteGlobalPropertyModal","name":"delete-global-property-modal"}],"workspaces2":[{"name":"manage-user-workspace","component":"manageUserWorkspace","window":"esm-admin-extra-wide-workspace-window"},{"name":"user-role-scope-workspace","component":"userRoleScopeWorkspace","window":"esm-admin-workspace-window"},{"name":"add-location-workspace","component":"addLocation","window":"esm-admin-workspace-window"},{"name":"search-location-workspace","component":"searchLocationWorkspace","window":"esm-admin-workspace-window"},{"name":"global-property-workspace","component":"globalPropertyWorkspace","window":"esm-admin-workspace-window"},{"name":"upload-logo-workspace","component":"uploadLogoWorkspace","window":"esm-admin-workspace-window"}],"workspaceWindows2":[{"name":"esm-admin-workspace-window","canMaximize":true,"group":"esm-admin-workspace-group","width":"wider"},{"name":"esm-admin-extra-wide-workspace-window","canMaximize":true,"group":"esm-admin-workspace-group","width":"extra-wide"}],"workspaceGroups2":[{"name":"esm-admin-workspace-group","scopePattern":"/admin","closeable":true}],"pages":[{"component":"root","route":"admin"}],"version":"5.4.5-pre.798"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { type FacilityRegistryRecord } from './type';
|
|
7
|
+
import FacilityRegistryView from './facility-info.component';
|
|
8
|
+
import { syncFacilityRegistry, useFacilityRegistry } from './useFacilityRegistry';
|
|
9
|
+
|
|
10
|
+
vi.mock('./useFacilityRegistry', () => ({
|
|
11
|
+
syncFacilityRegistry: vi.fn(),
|
|
12
|
+
useFacilityRegistry: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const facility: FacilityRegistryRecord = {
|
|
16
|
+
fr_code: 'FR-12345',
|
|
17
|
+
official_name: 'Mbagathi County Hospital',
|
|
18
|
+
registration_number: 'REG-001',
|
|
19
|
+
is_hub: false,
|
|
20
|
+
license_status: 'ACTIVE',
|
|
21
|
+
regulatory_operational_status: 'OPERATIONAL',
|
|
22
|
+
sha_operational_status: 'ACTIVE',
|
|
23
|
+
sha_contract_status: '',
|
|
24
|
+
address: {
|
|
25
|
+
country: 'Kenya',
|
|
26
|
+
county: 'Nairobi',
|
|
27
|
+
latitude: -1.301,
|
|
28
|
+
longitude: 36.805,
|
|
29
|
+
},
|
|
30
|
+
facility_administrator_name: 'Alice Wanjiku',
|
|
31
|
+
facility_administrator_phone: '+254711111111',
|
|
32
|
+
facility_administrator_email: 'alice@example.com',
|
|
33
|
+
sha_contracted_services: ['Outpatient', { name: 'Maternity' }, { code: 'DIALYSIS' } as { name?: string }],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const mutate = vi.fn();
|
|
37
|
+
const mockUseFacilityRegistry = vi.mocked(useFacilityRegistry);
|
|
38
|
+
const mockSyncFacilityRegistry = vi.mocked(syncFacilityRegistry);
|
|
39
|
+
const mockShowSnackbar = vi.mocked(showSnackbar);
|
|
40
|
+
|
|
41
|
+
function mockFacility(overrides: Partial<FacilityRegistryRecord> = {}) {
|
|
42
|
+
mockUseFacilityRegistry.mockReturnValue({
|
|
43
|
+
facility: { ...facility, ...overrides },
|
|
44
|
+
isLoading: false,
|
|
45
|
+
error: undefined,
|
|
46
|
+
notYetSynced: false,
|
|
47
|
+
mutate,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getInfoValue(label: string) {
|
|
52
|
+
return within(screen.getByText(label).parentElement).getByText('—');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('FacilityRegistryView missing branches', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
mockFacility();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders the uncontracted status, coordinates, and administrator details', () => {
|
|
62
|
+
render(<FacilityRegistryView />);
|
|
63
|
+
|
|
64
|
+
expect(screen.getByText('Not contracted')).toBeVisible();
|
|
65
|
+
expect(screen.getByText('-1.301, 36.805')).toBeVisible();
|
|
66
|
+
expect(screen.getByText('Alice Wanjiku')).toBeVisible();
|
|
67
|
+
expect(screen.getByText('+254711111111')).toBeVisible();
|
|
68
|
+
expect(screen.getByText('alice@example.com')).toBeVisible();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('renders placeholders for missing facility phone and email', () => {
|
|
72
|
+
mockFacility({ facility_phone_number: undefined, facility_email: undefined });
|
|
73
|
+
render(<FacilityRegistryView />);
|
|
74
|
+
|
|
75
|
+
expect(getInfoValue('Phone')).toHaveTextContent('—');
|
|
76
|
+
expect(getInfoValue('Email')).toHaveTextContent('—');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('shows the empty bed-occupancy state when no data is available', () => {
|
|
80
|
+
mockFacility({ bed_occupancy: undefined });
|
|
81
|
+
render(<FacilityRegistryView />);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByText('No bed occupancy data')).toBeVisible();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders string, named-object, and unnamed-object contracted services', () => {
|
|
87
|
+
render(<FacilityRegistryView />);
|
|
88
|
+
|
|
89
|
+
expect(screen.getByText('Outpatient')).toBeVisible();
|
|
90
|
+
expect(screen.getByText('Maternity')).toBeVisible();
|
|
91
|
+
expect(screen.getByText('{"code":"DIALYSIS"}')).toBeVisible();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.each([
|
|
95
|
+
[{ responseBody: { error: 'Registry rejected the request' } }, 'Registry rejected the request'],
|
|
96
|
+
[new Error('Network unavailable'), 'Network unavailable'],
|
|
97
|
+
[{}, 'Unable to reach the facility registry'],
|
|
98
|
+
])('shows the appropriate sync error message for %#', async (error, expectedSubtitle) => {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
mockSyncFacilityRegistry.mockRejectedValue(error);
|
|
101
|
+
render(<FacilityRegistryView />);
|
|
102
|
+
|
|
103
|
+
await user.click(screen.getByRole('button', { name: 'Sync with facility registry' }));
|
|
104
|
+
|
|
105
|
+
await waitFor(() =>
|
|
106
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
107
|
+
expect.objectContaining({
|
|
108
|
+
title: 'Sync failed',
|
|
109
|
+
subtitle: expectedSubtitle,
|
|
110
|
+
kind: 'error',
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
expect(mutate).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom/vitest';
|
|
4
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import { SWRConfig } from 'swr';
|
|
6
|
+
import { openmrsFetch } from '@openmrs/esm-api';
|
|
7
|
+
import { launchWorkspace2 } from '@openmrs/esm-framework';
|
|
8
|
+
import GlobalPropertyDashboard from './global-property-dashboard.component';
|
|
9
|
+
|
|
10
|
+
vi.mock('@openmrs/esm-api', async (importOriginal) => ({
|
|
11
|
+
...(await importOriginal<typeof import('@openmrs/esm-api')>()),
|
|
12
|
+
openmrsFetch: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockOpenmrsFetch = vi.mocked(openmrsFetch);
|
|
16
|
+
const mockLaunchWorkspace2 = vi.mocked(launchWorkspace2);
|
|
17
|
+
|
|
18
|
+
const mockProperties = [
|
|
19
|
+
{ uuid: 'uuid-1', property: 'setting.one', value: 'value1', description: 'First setting' },
|
|
20
|
+
{ uuid: 'uuid-2', property: 'setting.two', value: 'value2', description: 'Second setting' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function mockFetchSuccess(results = mockProperties) {
|
|
24
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
25
|
+
data: { results, links: [], totalCount: results.length },
|
|
26
|
+
} as unknown as Awaited<ReturnType<typeof openmrsFetch>>);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fresh SWR cache per render so responses do not leak between tests.
|
|
30
|
+
function renderDashboard() {
|
|
31
|
+
return render(
|
|
32
|
+
<SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>
|
|
33
|
+
<GlobalPropertyDashboard />
|
|
34
|
+
</SWRConfig>,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('GlobalPropertyDashboard', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders the page header with the dashboard title and illustration', async () => {
|
|
44
|
+
mockFetchSuccess();
|
|
45
|
+
renderDashboard();
|
|
46
|
+
expect(screen.getByText('FacilityPictogram')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getAllByText('Global Property').length).toBeGreaterThan(0);
|
|
48
|
+
await screen.findByText('setting.one');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('requests global properties from the paginated systemsetting endpoint', async () => {
|
|
52
|
+
mockFetchSuccess();
|
|
53
|
+
renderDashboard();
|
|
54
|
+
await screen.findByText('setting.one');
|
|
55
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledWith(
|
|
56
|
+
expect.stringContaining('/openmrs/ws/rest/v1/systemsetting?v=default&limit=10&startIndex=0&totalCount=true'),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders the fetched global properties in the table', async () => {
|
|
61
|
+
mockFetchSuccess();
|
|
62
|
+
renderDashboard();
|
|
63
|
+
expect(await screen.findByText('setting.one')).toBeInTheDocument();
|
|
64
|
+
expect(screen.getByText('value1')).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByText('setting.two')).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText('value2')).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByText('Property')).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByText('Value')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders the toolbar actions for adding a property and uploading an image', async () => {
|
|
73
|
+
mockFetchSuccess();
|
|
74
|
+
renderDashboard();
|
|
75
|
+
expect(screen.getByRole('button', { name: /add global property/i })).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByRole('button', { name: /upload image/i })).toBeInTheDocument();
|
|
77
|
+
await screen.findByText('setting.one');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('opens the add workspace when "Add global property" is clicked from the dashboard', async () => {
|
|
81
|
+
mockFetchSuccess();
|
|
82
|
+
renderDashboard();
|
|
83
|
+
await screen.findByText('setting.one');
|
|
84
|
+
fireEvent.click(screen.getByRole('button', { name: /add global property/i }));
|
|
85
|
+
expect(mockLaunchWorkspace2).toHaveBeenCalledWith(
|
|
86
|
+
'global-property-workspace',
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
systemSetting: undefined,
|
|
89
|
+
mutateGlobalProperty: expect.any(Function),
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('refetches with the search term as a query param when the user searches', async () => {
|
|
95
|
+
mockFetchSuccess();
|
|
96
|
+
renderDashboard();
|
|
97
|
+
await screen.findByText('setting.one');
|
|
98
|
+
const searchInput = screen.getByPlaceholderText('Search for global properties');
|
|
99
|
+
fireEvent.change(searchInput, { target: { value: 'setting.two' } });
|
|
100
|
+
await waitFor(() => {
|
|
101
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledWith(expect.stringContaining('q=setting.two'));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('keeps the page header visible and shows an error card when the request fails', async () => {
|
|
106
|
+
mockOpenmrsFetch.mockRejectedValue(new Error('Network error'));
|
|
107
|
+
renderDashboard();
|
|
108
|
+
expect(await screen.findByRole('heading', { name: 'Global property' })).toBeInTheDocument();
|
|
109
|
+
expect(screen.getByText('FacilityPictogram')).toBeInTheDocument();
|
|
110
|
+
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { Autosuggest } from './autosuggest.component';
|
|
6
|
+
|
|
7
|
+
const suggestions = [
|
|
8
|
+
{ uuid: 'location-1', display: 'Mbagathi Hospital' },
|
|
9
|
+
{ uuid: 'location-2', display: 'Kenyatta National Hospital' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const defaultProps = {
|
|
13
|
+
id: 'location',
|
|
14
|
+
labelText: 'Location',
|
|
15
|
+
placeholder: 'Search locations',
|
|
16
|
+
getDisplayValue: (item) => item.display,
|
|
17
|
+
getFieldValue: (item) => item.uuid,
|
|
18
|
+
getSearchResults: vi.fn().mockResolvedValue(suggestions),
|
|
19
|
+
onSuggestionSelected: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('Autosuggest', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
defaultProps.getSearchResults.mockResolvedValue(suggestions);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('searches with a trimmed query and selects a suggestion', async () => {
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
render(<Autosuggest {...defaultProps} />);
|
|
31
|
+
|
|
32
|
+
const input = screen.getByRole('searchbox', { name: 'Location' });
|
|
33
|
+
await user.type(input, ' Mbagathi ');
|
|
34
|
+
|
|
35
|
+
expect(await screen.findByText('Mbagathi Hospital')).toBeVisible();
|
|
36
|
+
expect(defaultProps.getSearchResults).toHaveBeenLastCalledWith('Mbagathi');
|
|
37
|
+
|
|
38
|
+
await user.click(screen.getByText('Mbagathi Hospital'));
|
|
39
|
+
|
|
40
|
+
expect(input).toHaveValue('Mbagathi Hospital');
|
|
41
|
+
expect(defaultProps.onSuggestionSelected).toHaveBeenLastCalledWith('location', 'location-1');
|
|
42
|
+
expect(screen.queryByText('Kenyatta National Hospital')).not.toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('clears suggestions without searching a blank query', async () => {
|
|
46
|
+
const user = userEvent.setup();
|
|
47
|
+
render(<Autosuggest {...defaultProps} />);
|
|
48
|
+
|
|
49
|
+
const input = screen.getByRole('searchbox', { name: 'Location' });
|
|
50
|
+
await user.type(input, 'M');
|
|
51
|
+
await screen.findByText('Mbagathi Hospital');
|
|
52
|
+
await user.clear(input);
|
|
53
|
+
|
|
54
|
+
expect(defaultProps.onSuggestionSelected).toHaveBeenLastCalledWith('location', undefined);
|
|
55
|
+
expect(defaultProps.getSearchResults).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(screen.queryByText('Mbagathi Hospital')).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('closes results on an outside click and displays validation feedback', async () => {
|
|
60
|
+
render(<Autosuggest {...defaultProps} invalid invalidText="Select a valid location" />);
|
|
61
|
+
|
|
62
|
+
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'Mbagathi' } });
|
|
63
|
+
await screen.findByText('Mbagathi Hospital');
|
|
64
|
+
fireEvent.mouseDown(document.body);
|
|
65
|
+
|
|
66
|
+
expect(screen.queryByText('Mbagathi Hospital')).not.toBeInTheDocument();
|
|
67
|
+
expect(screen.getByText('Select a valid location')).toBeVisible();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders a custom empty state when no suggestions are returned', async () => {
|
|
71
|
+
defaultProps.getSearchResults.mockResolvedValue([]);
|
|
72
|
+
render(<Autosuggest {...defaultProps} renderEmptyState={(value) => <div>No locations found for {value}</div>} />);
|
|
73
|
+
|
|
74
|
+
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'Unknown' } });
|
|
75
|
+
|
|
76
|
+
expect(await screen.findByText('No locations found for Unknown')).toBeVisible();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('stops loading and clears suggestions when a search fails', async () => {
|
|
80
|
+
defaultProps.getSearchResults.mockRejectedValue(new Error('Request failed'));
|
|
81
|
+
render(<Autosuggest {...defaultProps} renderEmptyState={(value) => <div>No locations found for {value}</div>} />);
|
|
82
|
+
|
|
83
|
+
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'Unknown' } });
|
|
84
|
+
expect(screen.getByText('Loading...')).toBeVisible();
|
|
85
|
+
|
|
86
|
+
await waitFor(() => expect(screen.queryByText('Loading...')).not.toBeInTheDocument());
|
|
87
|
+
expect(screen.getByText('No locations found for Unknown')).toBeVisible();
|
|
88
|
+
expect(screen.queryByRole('list')).not.toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { searchLocation } from '../hooks/UseFacilityLocations';
|
|
6
|
+
import { type LocationResponse } from '../types';
|
|
7
|
+
import { LocationAutosuggest } from './location-autosuggest.component';
|
|
8
|
+
|
|
9
|
+
vi.mock('../hooks/UseFacilityLocations', () => ({
|
|
10
|
+
searchLocation: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const locations: Array<LocationResponse> = [
|
|
14
|
+
{
|
|
15
|
+
uuid: 'location-1',
|
|
16
|
+
display: 'Mbagathi Hospital',
|
|
17
|
+
name: 'Mbagathi',
|
|
18
|
+
description: 'County hospital',
|
|
19
|
+
stateProvince: 'Nairobi',
|
|
20
|
+
country: 'Kenya',
|
|
21
|
+
countyDistrict: '',
|
|
22
|
+
address5: '',
|
|
23
|
+
address6: '',
|
|
24
|
+
tags: [],
|
|
25
|
+
attributes: [],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const mockSearchLocation = vi.mocked(searchLocation);
|
|
30
|
+
|
|
31
|
+
describe('LocationAutosuggest', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
mockSearchLocation.mockResolvedValue(locations);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('searches for locations and returns the selected location', async () => {
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
const onLocationSelected = vi.fn();
|
|
40
|
+
render(<LocationAutosuggest onLocationSelected={onLocationSelected} />);
|
|
41
|
+
|
|
42
|
+
await user.type(screen.getByRole('searchbox', { name: 'Select Location' }), 'Mbagathi');
|
|
43
|
+
|
|
44
|
+
expect(await screen.findByText('Mbagathi Hospital')).toBeVisible();
|
|
45
|
+
expect(mockSearchLocation).toHaveBeenLastCalledWith('Mbagathi');
|
|
46
|
+
|
|
47
|
+
await user.click(screen.getByText('Mbagathi Hospital'));
|
|
48
|
+
expect(onLocationSelected).toHaveBeenCalledWith('location-1', locations[0]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('supports custom input and validation labels', () => {
|
|
52
|
+
render(
|
|
53
|
+
<LocationAutosuggest
|
|
54
|
+
onLocationSelected={vi.fn()}
|
|
55
|
+
labelText="Facility"
|
|
56
|
+
placeholder="Find a facility"
|
|
57
|
+
invalid
|
|
58
|
+
invalidText="Facility is required"
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(screen.getByRole('searchbox', { name: 'Facility' })).toHaveAttribute('placeholder', 'Find a facility');
|
|
63
|
+
expect(screen.getByText('Facility is required')).toBeVisible();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('shows an empty state when a search has no matches', async () => {
|
|
67
|
+
const user = userEvent.setup();
|
|
68
|
+
mockSearchLocation.mockResolvedValue([]);
|
|
69
|
+
render(<LocationAutosuggest onLocationSelected={vi.fn()} />);
|
|
70
|
+
|
|
71
|
+
await user.type(screen.getByRole('searchbox', { name: 'Select Location' }), 'Unknown');
|
|
72
|
+
|
|
73
|
+
expect(await screen.findByText('Found no matching results')).toBeVisible();
|
|
74
|
+
expect(screen.getByText('Try searching for a different term')).toBeVisible();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { type LocationResponse } from '../types';
|
|
6
|
+
import ResultsTile from './results-tile.component';
|
|
7
|
+
|
|
8
|
+
const location: LocationResponse = {
|
|
9
|
+
uuid: 'location-1',
|
|
10
|
+
display: 'Mbagathi Hospital',
|
|
11
|
+
name: 'Mbagathi',
|
|
12
|
+
description: 'County hospital',
|
|
13
|
+
stateProvince: 'Nairobi',
|
|
14
|
+
country: 'Kenya',
|
|
15
|
+
countyDistrict: '',
|
|
16
|
+
address5: '',
|
|
17
|
+
address6: '',
|
|
18
|
+
tags: [
|
|
19
|
+
{ uuid: 'tag-1', display: 'Facility', name: 'Facility', description: '' },
|
|
20
|
+
{ uuid: 'tag-2', display: '', name: 'Clinical location', description: '' },
|
|
21
|
+
],
|
|
22
|
+
attributes: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('ResultsTile', () => {
|
|
26
|
+
it('renders the location details and tags', () => {
|
|
27
|
+
render(<ResultsTile location={location} />);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByText('Mbagathi Hospital')).toBeVisible();
|
|
30
|
+
expect(screen.getByText('County hospital')).toBeVisible();
|
|
31
|
+
expect(screen.getByText('Nairobi, Kenya')).toBeVisible();
|
|
32
|
+
expect(screen.getByText('Facility')).toBeVisible();
|
|
33
|
+
expect(screen.getByText('Clinical location')).toBeVisible();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('falls back to the location and tag names and omits missing optional details', () => {
|
|
37
|
+
render(
|
|
38
|
+
<ResultsTile
|
|
39
|
+
location={{
|
|
40
|
+
...location,
|
|
41
|
+
display: '',
|
|
42
|
+
description: '',
|
|
43
|
+
stateProvince: '',
|
|
44
|
+
country: '',
|
|
45
|
+
tags: [{ uuid: 'tag-1', display: '', name: 'Ward', description: '' }],
|
|
46
|
+
}}
|
|
47
|
+
/>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText('Mbagathi')).toBeVisible();
|
|
51
|
+
expect(screen.getByText('Ward')).toBeVisible();
|
|
52
|
+
expect(screen.queryByText('County hospital')).not.toBeInTheDocument();
|
|
53
|
+
expect(screen.queryByText('Nairobi, Kenya')).not.toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('calls onClose when the close icon is clicked', async () => {
|
|
57
|
+
const user = userEvent.setup();
|
|
58
|
+
const onClose = vi.fn();
|
|
59
|
+
const { container } = render(<ResultsTile location={location} onClose={onClose} />);
|
|
60
|
+
|
|
61
|
+
await user.click(container.querySelector('svg'));
|
|
62
|
+
|
|
63
|
+
expect(onClose).toHaveBeenCalledOnce();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { HWR_API_NO_CREDENTIALS, RESOURCE_NOT_FOUND, UNKNOWN } from '../../constants';
|
|
6
|
+
import HWREmptyModal from './hwr-empty.modal.component';
|
|
7
|
+
|
|
8
|
+
const errorMessages = [
|
|
9
|
+
[
|
|
10
|
+
RESOURCE_NOT_FOUND,
|
|
11
|
+
'The Health Work Registry is not reachable, kindly confirm your internet connectivity and try again. Do you want to continue to create an account',
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
HWR_API_NO_CREDENTIALS,
|
|
15
|
+
'Health Care Worker Registry API credentials not configured, Kindly contact system admin. Do you want to continue to create an account',
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
UNKNOWN,
|
|
19
|
+
'An error occurred while searching Health Worker Registry, kindly contact system admin. Do you want to continue to create an account',
|
|
20
|
+
],
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
describe('HWREmptyModal', () => {
|
|
24
|
+
it('renders the modal heading and registration actions', () => {
|
|
25
|
+
render(<HWREmptyModal close={vi.fn()} />);
|
|
26
|
+
|
|
27
|
+
expect(screen.getByRole('heading', { name: 'Create an Account' })).toBeVisible();
|
|
28
|
+
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
|
29
|
+
expect(screen.getByRole('button', { name: 'Continue to registration' })).toBeVisible();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it.each(errorMessages)('renders the message for the %s error', (errorCode, expectedMessage) => {
|
|
33
|
+
render(<HWREmptyModal close={vi.fn()} errorCode={errorCode} />);
|
|
34
|
+
expect(screen.getByText(expectedMessage)).toBeVisible();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('uses the health-worker-not-found message for an absent or unrecognized error code', () => {
|
|
38
|
+
const { rerender } = render(<HWREmptyModal close={vi.fn()} />);
|
|
39
|
+
const defaultMessage =
|
|
40
|
+
'The health worker records could not be found in Health Worker registry, do you want to continue to create an account';
|
|
41
|
+
|
|
42
|
+
expect(screen.getByText(defaultMessage)).toBeVisible();
|
|
43
|
+
|
|
44
|
+
rerender(<HWREmptyModal close={vi.fn()} errorCode="UNRECOGNIZED" />);
|
|
45
|
+
expect(screen.getByText(defaultMessage)).toBeVisible();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('closes the modal when Cancel is clicked', async () => {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
const close = vi.fn();
|
|
51
|
+
render(<HWREmptyModal close={close} />);
|
|
52
|
+
|
|
53
|
+
await user.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
54
|
+
|
|
55
|
+
expect(close).toHaveBeenCalledOnce();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('closes the modal when registration is continued', async () => {
|
|
59
|
+
const user = userEvent.setup();
|
|
60
|
+
const close = vi.fn();
|
|
61
|
+
render(<HWREmptyModal close={close} />);
|
|
62
|
+
|
|
63
|
+
await user.click(screen.getByRole('button', { name: 'Continue to registration' }));
|
|
64
|
+
|
|
65
|
+
expect(close).toHaveBeenCalledOnce();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -9,7 +9,7 @@ import upperCase from 'lodash-es/upperCase';
|
|
|
9
9
|
const ProviderBannerTag: React.FC = () => {
|
|
10
10
|
const { currentProvider } = useSession();
|
|
11
11
|
const { t } = useTranslation();
|
|
12
|
-
const currentProviderUuid = currentProvider?.uuid;
|
|
12
|
+
const currentProviderUuid = currentProvider?.uuid ?? '';
|
|
13
13
|
const { isLoading, error, providerAttributes } = useProviderAttributes(currentProviderUuid);
|
|
14
14
|
|
|
15
15
|
const getAttributeValue = (attributes: Array<Attribute>, displayName: string): string => {
|
|
@@ -59,12 +59,30 @@ const ProviderBannerTag: React.FC = () => {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const name = upperCase(providerAttributes?.person?.display) || 'NONE';
|
|
62
|
-
const hwi = getAttributeValue(providerAttributes?.attributes, 'Provider unique identifier') || 'NONE';
|
|
63
|
-
const licenseExpiry = getAttributeValue(providerAttributes?.attributes, 'License Expiry Date');
|
|
62
|
+
const hwi = getAttributeValue(providerAttributes?.attributes ?? [], 'Provider unique identifier') || 'NONE';
|
|
63
|
+
const licenseExpiry = getAttributeValue(providerAttributes?.attributes ?? [], 'License Expiry Date');
|
|
64
64
|
const formattedExpiry = formatDate(licenseExpiry) || '0000-00-00';
|
|
65
65
|
const shouldShowLicenseExpiry = !!licenseExpiry;
|
|
66
66
|
const licenseStatus = getLicenseStatus(licenseExpiry);
|
|
67
67
|
|
|
68
|
+
if (error) {
|
|
69
|
+
return (
|
|
70
|
+
<div className={styles.providerBanner}>
|
|
71
|
+
<div className={styles.bannerContent}>
|
|
72
|
+
<div className={styles.divider} />
|
|
73
|
+
<div className={styles.infoItem}>
|
|
74
|
+
<span className={styles.label}>{t('providerLabel', 'Provider:')}</span>
|
|
75
|
+
<span className={styles.statusIndicator} title={error?.message ?? String(error)}>
|
|
76
|
+
<Tag size="md" type="red">
|
|
77
|
+
{t('errorLoadingProvider', 'Error loading provider information')}
|
|
78
|
+
</Tag>
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
return (
|
|
69
87
|
<div className={styles.providerBanner}>
|
|
70
88
|
<div className={styles.bannerContent}>
|
|
@@ -76,28 +94,22 @@ const ProviderBannerTag: React.FC = () => {
|
|
|
76
94
|
|
|
77
95
|
<div className={styles.divider} />
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
<span className={styles.label}>{t('licenseProvider', 'License:')}</span>
|
|
97
|
+
<div className={styles.infoItem}>
|
|
98
|
+
<span className={styles.label}>{t('nameProvider', 'Name:')}</span>
|
|
99
|
+
<span className={styles.value}>{name}</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className={styles.divider} />
|
|
102
|
+
<div className={styles.infoItem}>
|
|
103
|
+
<span className={styles.label}>{t('licenseProvider', 'License:')}</span>
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
<>
|
|
90
|
-
<span className={styles.value}>{formattedExpiry}</span>
|
|
91
|
-
</>
|
|
92
|
-
)}
|
|
105
|
+
{shouldShowLicenseExpiry && <span className={styles.value}>{formattedExpiry}</span>}
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</>
|
|
107
|
+
<span className={styles.statusIndicator}>
|
|
108
|
+
<Tag size="md" type={licenseStatus.tagType}>
|
|
109
|
+
{licenseStatus.message}
|
|
110
|
+
</Tag>
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
101
113
|
</div>
|
|
102
114
|
</div>
|
|
103
115
|
);
|