@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } 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 dayjs from 'dayjs';
|
|
7
|
+
import { openmrsFetch, useSession } from '@openmrs/esm-framework';
|
|
8
|
+
|
|
9
|
+
import ProviderBannerTag from './provider-banner.component';
|
|
10
|
+
import { type Attribute } from './provider-banner.resource';
|
|
11
|
+
|
|
12
|
+
const mockOpenmrsFetch = vi.mocked(openmrsFetch);
|
|
13
|
+
const mockUseSession = vi.mocked(useSession);
|
|
14
|
+
|
|
15
|
+
function mockProviderResponse(attributes: Array<Attribute>, display = 'Super User') {
|
|
16
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
17
|
+
data: { person: { display }, attributes },
|
|
18
|
+
} as unknown as Awaited<ReturnType<typeof openmrsFetch>>);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function licenseExpiry(value: string): Attribute {
|
|
22
|
+
return { attributeType: { display: 'License Expiry Date' }, value };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const uniqueIdentifier: Attribute = { attributeType: { display: 'Provider unique identifier' }, value: 'HWI-001' };
|
|
26
|
+
|
|
27
|
+
function renderBanner() {
|
|
28
|
+
return render(
|
|
29
|
+
<SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>
|
|
30
|
+
<ProviderBannerTag />
|
|
31
|
+
</SWRConfig>,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('ProviderBannerTag', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
mockUseSession.mockReturnValue({
|
|
39
|
+
currentProvider: { uuid: 'provider-1', identifier: 'HWI-001' },
|
|
40
|
+
} as unknown as ReturnType<typeof useSession>);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('shows an inline loading indicator while fetching', () => {
|
|
44
|
+
mockOpenmrsFetch.mockReturnValue(new Promise(() => {}) as ReturnType<typeof openmrsFetch>);
|
|
45
|
+
renderBanner();
|
|
46
|
+
expect(screen.getByText('loading...')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('fetches the provider attributes for the session provider', async () => {
|
|
50
|
+
mockProviderResponse([uniqueIdentifier]);
|
|
51
|
+
renderBanner();
|
|
52
|
+
await screen.findByText('SUPER USER');
|
|
53
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledWith(
|
|
54
|
+
'/ws/rest/v1/provider/provider-1?v=custom:(person:(display),attributes:(attributeType:(display),value))',
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders the identifier, upper-cased name, and an Unlicensed tag when there is no license expiry', async () => {
|
|
59
|
+
mockProviderResponse([uniqueIdentifier]);
|
|
60
|
+
renderBanner();
|
|
61
|
+
|
|
62
|
+
expect(await screen.findByText('SUPER USER')).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText('HWI-001')).toBeInTheDocument();
|
|
64
|
+
expect(screen.getByText('Unlicensed')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('falls back to NONE when the provider has no identifier or name', async () => {
|
|
68
|
+
mockProviderResponse([], '');
|
|
69
|
+
renderBanner();
|
|
70
|
+
expect(await screen.findAllByText('NONE')).toHaveLength(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('shows a valid license tag with the expiry date when the license lasts beyond 90 days', async () => {
|
|
74
|
+
const expiry = dayjs().add(120, 'day');
|
|
75
|
+
mockProviderResponse([uniqueIdentifier, licenseExpiry(expiry.toISOString())]);
|
|
76
|
+
renderBanner();
|
|
77
|
+
|
|
78
|
+
expect(await screen.findByText('Valid License')).toBeInTheDocument();
|
|
79
|
+
expect(screen.getByText(expiry.format('YYYY-MM-DD'))).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('shows an expires-in-3-months tag when the license expires within 90 days', async () => {
|
|
83
|
+
mockProviderResponse([licenseExpiry(dayjs().add(60, 'day').toISOString())]);
|
|
84
|
+
renderBanner();
|
|
85
|
+
expect(await screen.findByText('Expires in 3 months')).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('shows an expires-soon tag when the license expires within 30 days', async () => {
|
|
89
|
+
mockProviderResponse([licenseExpiry(dayjs().add(20, 'day').toISOString())]);
|
|
90
|
+
renderBanner();
|
|
91
|
+
expect(await screen.findByText('Expires Soon')).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('shows an expired tag when the license expiry is in the past', async () => {
|
|
95
|
+
mockProviderResponse([licenseExpiry(dayjs().subtract(10, 'day').toISOString())]);
|
|
96
|
+
renderBanner();
|
|
97
|
+
expect(await screen.findByText('License Expired')).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('renders the banner-style error tag with the error message on hover when the request fails', async () => {
|
|
101
|
+
mockOpenmrsFetch.mockRejectedValue(new Error('Server unavailable'));
|
|
102
|
+
renderBanner();
|
|
103
|
+
|
|
104
|
+
expect(await screen.findByText('Error loading provider information')).toBeInTheDocument();
|
|
105
|
+
expect(screen.getByText('Provider:')).toBeInTheDocument();
|
|
106
|
+
expect(screen.getByTitle('Server unavailable')).toBeInTheDocument();
|
|
107
|
+
expect(screen.queryByText('Identifier:')).not.toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { act, render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { restBaseUrl, showModal, showSnackbar } from '@openmrs/esm-framework';
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { deleteUserRoleScopes, handleMutation, useUserRoleScopes } from '../../../../../user-management.resources';
|
|
7
|
+
import { type User, type UserRoleScope } from '../../../../../types';
|
|
8
|
+
import StockUserRoleScopesList from './user-role-scope-list.component';
|
|
9
|
+
|
|
10
|
+
vi.mock('../../../../../user-management.resources', () => ({
|
|
11
|
+
deleteUserRoleScopes: vi.fn(),
|
|
12
|
+
handleMutation: vi.fn(),
|
|
13
|
+
useUserRoleScopes: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const user = {
|
|
17
|
+
uuid: 'user-1',
|
|
18
|
+
username: 'alice',
|
|
19
|
+
roles: [],
|
|
20
|
+
} as User;
|
|
21
|
+
|
|
22
|
+
const roleScope: UserRoleScope = {
|
|
23
|
+
uuid: 'scope-1',
|
|
24
|
+
userUuid: 'user-1',
|
|
25
|
+
role: 'Inventory Clerk',
|
|
26
|
+
permanent: true,
|
|
27
|
+
enabled: true,
|
|
28
|
+
locations: [
|
|
29
|
+
{ locationUuid: 'location-1', locationName: 'Main Store', enableDescendants: true },
|
|
30
|
+
{ locationUuid: 'location-2', locationName: 'Pharmacy', enableDescendants: false },
|
|
31
|
+
],
|
|
32
|
+
operationTypes: [
|
|
33
|
+
{ operationTypeUuid: 'operation-1', operationTypeName: 'Stock Issue' },
|
|
34
|
+
{ operationTypeUuid: 'operation-2', operationTypeName: 'Stock Receipt' },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const mockUseUserRoleScopes = vi.mocked(useUserRoleScopes);
|
|
39
|
+
const mockDeleteUserRoleScopes = vi.mocked(deleteUserRoleScopes);
|
|
40
|
+
const mockHandleMutation = vi.mocked(handleMutation);
|
|
41
|
+
const mockShowModal = vi.mocked(showModal);
|
|
42
|
+
const mockShowSnackbar = vi.mocked(showSnackbar);
|
|
43
|
+
|
|
44
|
+
describe('StockUserRoleScopesList', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
mockUseUserRoleScopes.mockReturnValue({
|
|
48
|
+
items: { results: [roleScope], totalCount: 1 },
|
|
49
|
+
loadingRoleScope: false,
|
|
50
|
+
userRoleScopeError: undefined,
|
|
51
|
+
isValidating: false,
|
|
52
|
+
});
|
|
53
|
+
mockShowModal.mockReturnValue(vi.fn());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('shows a skeleton while role scopes are loading', () => {
|
|
57
|
+
mockUseUserRoleScopes.mockReturnValue({
|
|
58
|
+
items: undefined,
|
|
59
|
+
loadingRoleScope: true,
|
|
60
|
+
userRoleScopeError: undefined,
|
|
61
|
+
isValidating: false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const { container } = render(<StockUserRoleScopesList user={user} />);
|
|
65
|
+
expect(container.querySelector('.cds--skeleton')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('renders only the selected user role scopes', () => {
|
|
69
|
+
mockUseUserRoleScopes.mockReturnValue({
|
|
70
|
+
items: {
|
|
71
|
+
results: [roleScope, { ...roleScope, uuid: 'scope-2', userUuid: 'another-user', role: 'Hidden Role' }],
|
|
72
|
+
totalCount: 2,
|
|
73
|
+
},
|
|
74
|
+
loadingRoleScope: false,
|
|
75
|
+
userRoleScopeError: undefined,
|
|
76
|
+
isValidating: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
render(<StockUserRoleScopesList user={user} />);
|
|
80
|
+
|
|
81
|
+
expect(screen.getByRole('table', { name: 'user role scope table' })).toBeVisible();
|
|
82
|
+
expect(screen.getByText('Inventory Clerk')).toBeVisible();
|
|
83
|
+
expect(screen.getByText('Main Store')).toBeVisible();
|
|
84
|
+
expect(screen.getByText('Pharmacy')).toBeVisible();
|
|
85
|
+
expect(screen.getByText('Stock Issue, Stock Receipt')).toBeVisible();
|
|
86
|
+
expect(screen.getAllByText('Yes')).toHaveLength(2);
|
|
87
|
+
expect(screen.queryByText('Hidden Role')).not.toBeInTheDocument();
|
|
88
|
+
expect(screen.getByText('Refreshing user scopes...')).toBeVisible();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('shows an empty state when the user has no role scopes', () => {
|
|
92
|
+
mockUseUserRoleScopes.mockReturnValue({
|
|
93
|
+
items: { results: [], totalCount: 0 },
|
|
94
|
+
loadingRoleScope: false,
|
|
95
|
+
userRoleScopeError: undefined,
|
|
96
|
+
isValidating: false,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
render(<StockUserRoleScopesList user={user} />);
|
|
100
|
+
expect(screen.getByRole('heading', { name: 'User Role Scope' })).toBeVisible();
|
|
101
|
+
expect(screen.queryByText('Inventory Clerk')).not.toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('passes the selected scope to the edit callback', async () => {
|
|
105
|
+
const userEventInstance = userEvent.setup();
|
|
106
|
+
const onEditUserRoleScope = vi.fn();
|
|
107
|
+
render(<StockUserRoleScopesList user={user} onEditUserRoleScope={onEditUserRoleScope} />);
|
|
108
|
+
|
|
109
|
+
await userEventInstance.click(screen.getByRole('button', { name: 'Options' }));
|
|
110
|
+
await userEventInstance.click(await screen.findByText('Edit'));
|
|
111
|
+
|
|
112
|
+
expect(onEditUserRoleScope).toHaveBeenCalledWith(roleScope);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('deletes a role scope and refreshes the list after confirmation', async () => {
|
|
116
|
+
const userEventInstance = userEvent.setup();
|
|
117
|
+
mockDeleteUserRoleScopes.mockResolvedValue({} as Awaited<ReturnType<typeof deleteUserRoleScopes>>);
|
|
118
|
+
render(<StockUserRoleScopesList user={user} />);
|
|
119
|
+
|
|
120
|
+
await userEventInstance.click(screen.getByRole('button', { name: 'Options' }));
|
|
121
|
+
await userEventInstance.click(await screen.findByText('Delete'));
|
|
122
|
+
|
|
123
|
+
expect(mockShowModal).toHaveBeenCalledWith(
|
|
124
|
+
'delete-stock-user-scope-modal',
|
|
125
|
+
expect.objectContaining({ uuid: 'scope-1', onConfirmation: expect.any(Function) }),
|
|
126
|
+
);
|
|
127
|
+
const [, modalProps] = mockShowModal.mock.calls[0] as [string, { onConfirmation: () => void }];
|
|
128
|
+
|
|
129
|
+
await act(async () => modalProps.onConfirmation());
|
|
130
|
+
|
|
131
|
+
expect(mockDeleteUserRoleScopes).toHaveBeenCalledWith(['scope-1']);
|
|
132
|
+
expect(mockHandleMutation).toHaveBeenCalledWith(`${restBaseUrl}/stockmanagement/userrolescope`);
|
|
133
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({ kind: 'success', subtitle: 'Stock User Scope Deleted Successfully' }),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('shows an error notification when deletion fails', async () => {
|
|
139
|
+
const userEventInstance = userEvent.setup();
|
|
140
|
+
mockDeleteUserRoleScopes.mockRejectedValue(new Error('Delete failed'));
|
|
141
|
+
render(<StockUserRoleScopesList user={user} />);
|
|
142
|
+
|
|
143
|
+
await userEventInstance.click(screen.getByRole('button', { name: 'Options' }));
|
|
144
|
+
await userEventInstance.click(await screen.findByText('Delete'));
|
|
145
|
+
const [, modalProps] = mockShowModal.mock.calls[0] as [string, { onConfirmation: () => void }];
|
|
146
|
+
|
|
147
|
+
await act(async () => modalProps.onConfirmation());
|
|
148
|
+
|
|
149
|
+
expect(mockHandleMutation).not.toHaveBeenCalled();
|
|
150
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
151
|
+
expect.objectContaining({ kind: 'error', subtitle: 'Delete failed' }),
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { type Role, type StockOperationType, type UserRoleScope } from '../../../../../types';
|
|
7
|
+
import UserRoleScopeFormFields from './user-role-scope-fields.component';
|
|
8
|
+
|
|
9
|
+
vi.mock('@carbon/react', async (importOriginal) => {
|
|
10
|
+
const original = await importOriginal<typeof import('@carbon/react')>();
|
|
11
|
+
return {
|
|
12
|
+
...original,
|
|
13
|
+
DatePicker: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
|
14
|
+
DatePickerInput: ({ id, labelText }: { id: string; labelText: React.ReactNode }) => (
|
|
15
|
+
<label htmlFor={id}>
|
|
16
|
+
{labelText}
|
|
17
|
+
<input id={id} />
|
|
18
|
+
</label>
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const roles: Array<Role> = [
|
|
24
|
+
{ uuid: 'role-1', display: 'Inventory Clerk' },
|
|
25
|
+
{ uuid: 'role-2', display: 'Inventory Manager' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const stockOperations = [
|
|
29
|
+
{ uuid: 'operation-1', name: 'Stock Issue' },
|
|
30
|
+
{ uuid: 'operation-2', name: 'Stock Receipt' },
|
|
31
|
+
] as Array<StockOperationType>;
|
|
32
|
+
|
|
33
|
+
const stockLocations = [
|
|
34
|
+
{ id: 'location-1', name: 'Main Store' },
|
|
35
|
+
{ id: 'location-2', name: 'Pharmacy' },
|
|
36
|
+
] as Array<fhir.Location>;
|
|
37
|
+
|
|
38
|
+
const emptyScope = null as UserRoleScope;
|
|
39
|
+
|
|
40
|
+
function TestForm({
|
|
41
|
+
defaultValues = {
|
|
42
|
+
forms: [
|
|
43
|
+
{
|
|
44
|
+
role: '',
|
|
45
|
+
enabled: false,
|
|
46
|
+
permanent: false,
|
|
47
|
+
dateRange: {},
|
|
48
|
+
operationTypes: [],
|
|
49
|
+
locations: [],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
loadingStock = false,
|
|
54
|
+
initialScope = emptyScope,
|
|
55
|
+
removeForm = vi.fn(),
|
|
56
|
+
}: {
|
|
57
|
+
defaultValues?: Record<string, any>;
|
|
58
|
+
loadingStock?: boolean;
|
|
59
|
+
initialScope?: UserRoleScope;
|
|
60
|
+
removeForm?: (index: number) => void;
|
|
61
|
+
}) {
|
|
62
|
+
const methods = useForm({ defaultValues });
|
|
63
|
+
return (
|
|
64
|
+
<FormProvider {...methods}>
|
|
65
|
+
<UserRoleScopeFormFields
|
|
66
|
+
field={{}}
|
|
67
|
+
index={0}
|
|
68
|
+
control={methods.control}
|
|
69
|
+
removeForm={removeForm}
|
|
70
|
+
filteredInventoryRoles={roles}
|
|
71
|
+
hasInventoryRole
|
|
72
|
+
stockOperations={stockOperations}
|
|
73
|
+
loadingStock={loadingStock}
|
|
74
|
+
stockLocations={stockLocations}
|
|
75
|
+
roleScopeformMethods={methods}
|
|
76
|
+
userRoleScopeInitialValues={initialScope}
|
|
77
|
+
/>
|
|
78
|
+
<output data-testid="values">{JSON.stringify(methods.watch())}</output>
|
|
79
|
+
</FormProvider>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('UserRoleScopeFormFields', () => {
|
|
84
|
+
it('renders the role, access, operation, and location controls', () => {
|
|
85
|
+
render(<TestForm />);
|
|
86
|
+
|
|
87
|
+
expect(screen.getByText('Stock Role')).toBeVisible();
|
|
88
|
+
expect(screen.getByText('Stock Role Access')).toBeVisible();
|
|
89
|
+
expect(screen.getByLabelText('Enable?')).not.toBeChecked();
|
|
90
|
+
expect(screen.getByLabelText('Stock Issue')).not.toBeChecked();
|
|
91
|
+
expect(screen.getByLabelText('Stock Receipt')).not.toBeChecked();
|
|
92
|
+
expect(screen.getByLabelText('Main Store')).not.toBeChecked();
|
|
93
|
+
expect(screen.getByLabelText('Pharmacy')).not.toBeChecked();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('shows duration controls when enabled and clears dates when permanent is selected', async () => {
|
|
97
|
+
const user = userEvent.setup();
|
|
98
|
+
render(
|
|
99
|
+
<TestForm
|
|
100
|
+
defaultValues={{
|
|
101
|
+
forms: [
|
|
102
|
+
{
|
|
103
|
+
enabled: false,
|
|
104
|
+
permanent: false,
|
|
105
|
+
dateRange: { activeFrom: new Date('2026-08-01'), activeTo: new Date('2026-08-02') },
|
|
106
|
+
operationTypes: [],
|
|
107
|
+
locations: [],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await user.click(screen.getByLabelText('Enable?'));
|
|
115
|
+
expect(screen.getByLabelText('Permanent?')).toBeVisible();
|
|
116
|
+
expect(screen.getByText('Active From')).toBeVisible();
|
|
117
|
+
expect(screen.getByText('Active To')).toBeVisible();
|
|
118
|
+
|
|
119
|
+
await user.click(screen.getByLabelText('Permanent?'));
|
|
120
|
+
expect(screen.queryByText('Active From')).not.toBeInTheDocument();
|
|
121
|
+
expect(screen.getByTestId('values')).toHaveTextContent('"dateRange":{}');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('adds and removes stock operations and locations', async () => {
|
|
125
|
+
const user = userEvent.setup();
|
|
126
|
+
render(<TestForm />);
|
|
127
|
+
|
|
128
|
+
await user.click(screen.getByLabelText('Stock Issue'));
|
|
129
|
+
await user.click(screen.getByLabelText('Main Store'));
|
|
130
|
+
expect(screen.getByTestId('values')).toHaveTextContent(
|
|
131
|
+
'"operationTypes":[{"operationTypeUuid":"operation-1","operationTypeName":"Stock Issue"}]',
|
|
132
|
+
);
|
|
133
|
+
expect(screen.getByTestId('values')).toHaveTextContent(
|
|
134
|
+
'"locations":[{"locationName":"Main Store","locationUuid":"location-1"}]',
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
await user.click(screen.getByLabelText('Stock Issue'));
|
|
138
|
+
await user.click(screen.getByLabelText('Main Store'));
|
|
139
|
+
expect(screen.getByTestId('values')).toHaveTextContent('"operationTypes":[]');
|
|
140
|
+
expect(screen.getByTestId('values')).toHaveTextContent('"locations":[]');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('shows loading indicators while stock data is loading', () => {
|
|
144
|
+
render(<TestForm loadingStock />);
|
|
145
|
+
expect(screen.getAllByText('Loading data...')).toHaveLength(2);
|
|
146
|
+
expect(screen.queryByLabelText('Stock Issue')).not.toBeInTheDocument();
|
|
147
|
+
expect(screen.queryByLabelText('Main Store')).not.toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('removes a new form but does not offer removal while editing', async () => {
|
|
151
|
+
const user = userEvent.setup();
|
|
152
|
+
const removeForm = vi.fn();
|
|
153
|
+
const { rerender } = render(<TestForm removeForm={removeForm} />);
|
|
154
|
+
|
|
155
|
+
await user.click(screen.getByText('Remove'));
|
|
156
|
+
expect(removeForm).toHaveBeenCalledWith(0);
|
|
157
|
+
|
|
158
|
+
rerender(<TestForm removeForm={removeForm} initialScope={{ uuid: 'scope-1' } as UserRoleScope} />);
|
|
159
|
+
expect(screen.queryByText('Remove')).not.toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { configSchema } from '../../../../config-schema';
|
|
7
|
+
import { type ProviderResponse, type UserResponse } from '../../../../types';
|
|
8
|
+
import UserDetails from './user-details.component';
|
|
9
|
+
|
|
10
|
+
const config = getDefaultsFromConfigSchema(configSchema);
|
|
11
|
+
const attribute = (uuid: string, value: unknown) => ({
|
|
12
|
+
uuid: `${uuid}-${String(value)}`,
|
|
13
|
+
attributeType: { uuid, display: uuid },
|
|
14
|
+
value,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const user = {
|
|
18
|
+
uuid: 'user-1',
|
|
19
|
+
roles: [],
|
|
20
|
+
} as UserResponse;
|
|
21
|
+
|
|
22
|
+
function providerWith(attributes: Array<ReturnType<typeof attribute>>, gender = 'F') {
|
|
23
|
+
return {
|
|
24
|
+
uuid: 'provider-1',
|
|
25
|
+
person: { uuid: 'person-1', display: 'Alice Wanjiku', gender },
|
|
26
|
+
attributes,
|
|
27
|
+
} as ProviderResponse;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('UserDetails', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.mocked(useConfig).mockReturnValue(config);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows an unlicensed status, female gender, and the no-roles state', () => {
|
|
36
|
+
render(<UserDetails provider={providerWith([])} user={user} />);
|
|
37
|
+
|
|
38
|
+
expect(screen.getByText('Unlicensed')).toBeVisible();
|
|
39
|
+
expect(screen.getByText('Female')).toBeVisible();
|
|
40
|
+
expect(screen.getByText('No roles assigned')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('shows expired and expiring-soon license statuses', () => {
|
|
44
|
+
const { rerender } = render(
|
|
45
|
+
<UserDetails
|
|
46
|
+
provider={providerWith([
|
|
47
|
+
attribute(config.licenseNumberUuid, 'LIC-1'),
|
|
48
|
+
attribute(config.licenseExpiryDateUuid, dayjs().subtract(2, 'day').toISOString()),
|
|
49
|
+
])}
|
|
50
|
+
user={user}
|
|
51
|
+
/>,
|
|
52
|
+
);
|
|
53
|
+
expect(screen.getByText('License expired')).toBeVisible();
|
|
54
|
+
|
|
55
|
+
rerender(
|
|
56
|
+
<UserDetails
|
|
57
|
+
provider={providerWith([
|
|
58
|
+
attribute(config.licenseNumberUuid, 'LIC-1'),
|
|
59
|
+
attribute(config.licenseExpiryDateUuid, dayjs().add(2, 'day').toISOString()),
|
|
60
|
+
])}
|
|
61
|
+
user={user}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
expect(screen.getByText('License expiring soon')).toBeVisible();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('shows male gender and leaves unknown gender blank', () => {
|
|
68
|
+
const { rerender } = render(<UserDetails provider={providerWith([], 'M')} user={user} />);
|
|
69
|
+
expect(screen.getByText('Male')).toBeVisible();
|
|
70
|
+
|
|
71
|
+
rerender(<UserDetails provider={providerWith([], 'U')} user={user} />);
|
|
72
|
+
expect(screen.queryByText('Male')).not.toBeInTheDocument();
|
|
73
|
+
expect(screen.queryByText('Female')).not.toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders practice and provider identifiers with non-string attribute values', () => {
|
|
77
|
+
render(
|
|
78
|
+
<UserDetails
|
|
79
|
+
provider={providerWith([
|
|
80
|
+
attribute(config.licenseNumberUuid, 'LIC-1'),
|
|
81
|
+
attribute(config.practiceTypeUuid, 'private practice'),
|
|
82
|
+
attribute(config.providerUniqueIdentifierAttributeTypeUuid, 12345),
|
|
83
|
+
attribute(config.externalProviderIdentifierUuid, 'EXT-99'),
|
|
84
|
+
])}
|
|
85
|
+
user={user}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(screen.getByText('private practice')).toBeVisible();
|
|
90
|
+
expect(screen.getByText('12345')).toBeVisible();
|
|
91
|
+
expect(screen.getByText('EXT-99')).toBeVisible();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders qualification, specialty, and cadre tags for an active provider', () => {
|
|
95
|
+
render(
|
|
96
|
+
<UserDetails
|
|
97
|
+
provider={providerWith([
|
|
98
|
+
attribute(config.licenseNumberUuid, 'LIC-1'),
|
|
99
|
+
attribute(config.licenseExpiryDateUuid, dayjs().add(30, 'day').toISOString()),
|
|
100
|
+
attribute(config.qualificationUuid, 'medical degree'),
|
|
101
|
+
attribute(config.specialtyUuid, 'cardiology'),
|
|
102
|
+
attribute(config.providerCadreUuid, 'consultant'),
|
|
103
|
+
])}
|
|
104
|
+
user={user}
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(screen.getByText('Active')).toBeVisible();
|
|
109
|
+
expect(screen.getByText('Medical degree')).toBeVisible();
|
|
110
|
+
expect(screen.getByText('Cardiology')).toBeVisible();
|
|
111
|
+
expect(screen.getByText('Consultant')).toBeVisible();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('renders assigned roles and falls back when a role has no description', () => {
|
|
115
|
+
render(
|
|
116
|
+
<UserDetails
|
|
117
|
+
provider={providerWith([])}
|
|
118
|
+
user={
|
|
119
|
+
{
|
|
120
|
+
...user,
|
|
121
|
+
roles: [
|
|
122
|
+
{ uuid: 'role-1', display: 'Inventory Manager', description: 'Manages inventory' },
|
|
123
|
+
{ uuid: 'role-2', display: 'Provider', description: '' },
|
|
124
|
+
],
|
|
125
|
+
} as UserResponse
|
|
126
|
+
}
|
|
127
|
+
/>,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(screen.getByText('Inventory Manager')).toBeInTheDocument();
|
|
131
|
+
expect(screen.getByText('Manages inventory')).toBeInTheDocument();
|
|
132
|
+
expect(screen.getByText('Provider')).toBeInTheDocument();
|
|
133
|
+
expect(screen.getByText('No description available')).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
});
|