@openmrs/esm-stock-management-app 1.0.1-pre.641 → 1.0.1-pre.653

Sign up to get free protection for your applications and to get access to all the features.
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"extensions":[{"name":"stock-nav-menu","slot":"stock-sidebar-slot","component":"stockNavMenu","online":true,"offline":true},{"name":"overview-db-link","slot":"stock-page-dashboard-slot","component":"stockOverviewLink","meta":{"name":"overview","slot":"overview-dashboard-slot","title":"overview"},"order":0,"online":true,"offline":true},{"name":"stock-overview-db","slot":"overview-dashboard-slot","component":"stockOverview"},{"name":"operations-db-link","slot":"stock-page-dashboard-slot","component":"stockOperationsLink","meta":{"name":"operations","slot":"operations-dashboard-slot","title":"operations"},"order":2,"online":true,"offline":true},{"name":"stock-operations-db","slot":"operations-dashboard-slot","component":"stockOperations"},{"name":"items-db-link","slot":"stock-page-dashboard-slot","component":"stockItemsLink","meta":{"name":"items","slot":"items-dashboard-slot","title":"items"},"order":1,"online":true,"offline":true},{"name":"stock-items-db","slot":"items-dashboard-slot","component":"stockItems"},{"name":"user-scopes-db-link","slot":"stock-page-dashboard-slot","component":"stockUserScopesLink","meta":{"name":"user-scopes","slot":"user-scopes-dashboard-slot","title":"user-scopes"},"order":3,"online":true,"offline":true},{"name":"stock-user-scopes-db","slot":"user-scopes-dashboard-slot","component":"stockUserScopes"},{"name":"sources-db-link","slot":"stock-page-dashboard-slot","component":"stockSourcesLink","meta":{"name":"sources","slot":"sources-dashboard-slot","title":"Sources"},"order":2,"online":true,"offline":true},{"name":"stock-sources-db","slot":"sources-dashboard-slot","component":"stockSources"},{"name":"locations-db-link","slot":"stock-page-dashboard-slot","component":"stockLocationsLink","meta":{"name":"locations","slot":"locations-dashboard-slot","title":"Locations"},"order":4,"online":true,"offline":true},{"name":"stock-locations-db","slot":"locations-dashboard-slot","component":"stockLocations"},{"name":"reports-db-link","slot":"stock-page-dashboard-slot","component":"stockReportsLink","meta":{"name":"reports","slot":"reports-dashboard-slot","title":"Reports"},"order":5,"online":true,"offline":true},{"name":"stock-reports-db","slot":"reports-dashboard-slot","component":"stockReports"},{"name":"settings-db-link","slot":"stock-page-dashboard-slot","component":"stockSettingsLink","meta":{"name":"settings","slot":"settings-dashboard-slot","title":"Settings"},"order":6,"online":true,"offline":true},{"name":"stock-settings-db","slot":"settings-dashboard-slot","component":"stockSettings"},{"name":"stock-management-admin-card-link","slot":"system-admin-page-card-link-slot","component":"stockManagementAdminCardLink"},{"name":"stock-operation-dialog","component":"stockOperationDialog"},{"name":"import-bulk-stock-items","component":"importBulkStockItemsDialog"},{"name":"delete-stock-modal","component":"deleteStockModal"},{"name":"delete-stock-user-scope-modal","component":"deleteUserScopeModal"},{"name":"stock-management-app-menu-item","component":"stockManagementAppMenuItem","slot":"app-menu-item-slot","meta":{"name":" Stock Management"}},{"name":"delete-stock-rule-modal","component":"deleteStockRuleModal"},{"name":"delete-packaging-unit-modal","component":"deletePackagingUnitModal"},{"name":"delete-packaging-unit-button","component":"deletePackagingUnitButton"}],"pages":[{"component":"root","route":"stock-management"}],"version":"1.0.1-pre.641"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"extensions":[{"name":"stock-nav-menu","slot":"stock-sidebar-slot","component":"stockNavMenu","online":true,"offline":true},{"name":"overview-db-link","slot":"stock-page-dashboard-slot","component":"stockOverviewLink","meta":{"name":"overview","slot":"overview-dashboard-slot","title":"overview"},"order":0,"online":true,"offline":true},{"name":"stock-overview-db","slot":"overview-dashboard-slot","component":"stockOverview"},{"name":"operations-db-link","slot":"stock-page-dashboard-slot","component":"stockOperationsLink","meta":{"name":"operations","slot":"operations-dashboard-slot","title":"operations"},"order":2,"online":true,"offline":true},{"name":"stock-operations-db","slot":"operations-dashboard-slot","component":"stockOperations"},{"name":"items-db-link","slot":"stock-page-dashboard-slot","component":"stockItemsLink","meta":{"name":"items","slot":"items-dashboard-slot","title":"items"},"order":1,"online":true,"offline":true},{"name":"stock-items-db","slot":"items-dashboard-slot","component":"stockItems"},{"name":"user-scopes-db-link","slot":"stock-page-dashboard-slot","component":"stockUserScopesLink","meta":{"name":"user-scopes","slot":"user-scopes-dashboard-slot","title":"user-scopes"},"order":3,"online":true,"offline":true},{"name":"stock-user-scopes-db","slot":"user-scopes-dashboard-slot","component":"stockUserScopes"},{"name":"sources-db-link","slot":"stock-page-dashboard-slot","component":"stockSourcesLink","meta":{"name":"sources","slot":"sources-dashboard-slot","title":"Sources"},"order":2,"online":true,"offline":true},{"name":"stock-sources-db","slot":"sources-dashboard-slot","component":"stockSources"},{"name":"locations-db-link","slot":"stock-page-dashboard-slot","component":"stockLocationsLink","meta":{"name":"locations","slot":"locations-dashboard-slot","title":"Locations"},"order":4,"online":true,"offline":true},{"name":"stock-locations-db","slot":"locations-dashboard-slot","component":"stockLocations"},{"name":"reports-db-link","slot":"stock-page-dashboard-slot","component":"stockReportsLink","meta":{"name":"reports","slot":"reports-dashboard-slot","title":"Reports"},"order":5,"online":true,"offline":true},{"name":"stock-reports-db","slot":"reports-dashboard-slot","component":"stockReports"},{"name":"settings-db-link","slot":"stock-page-dashboard-slot","component":"stockSettingsLink","meta":{"name":"settings","slot":"settings-dashboard-slot","title":"Settings"},"order":6,"online":true,"offline":true},{"name":"stock-settings-db","slot":"settings-dashboard-slot","component":"stockSettings"},{"name":"stock-management-admin-card-link","slot":"system-admin-page-card-link-slot","component":"stockManagementAdminCardLink"},{"name":"stock-operation-dialog","component":"stockOperationDialog"},{"name":"import-bulk-stock-items","component":"importBulkStockItemsDialog"},{"name":"delete-stock-modal","component":"deleteStockModal"},{"name":"delete-stock-user-scope-modal","component":"deleteUserScopeModal"},{"name":"stock-management-app-menu-item","component":"stockManagementAppMenuItem","slot":"app-menu-item-slot","meta":{"name":" Stock Management"}},{"name":"delete-stock-rule-modal","component":"deleteStockRuleModal"},{"name":"delete-packaging-unit-modal","component":"deletePackagingUnitModal"},{"name":"delete-packaging-unit-button","component":"deletePackagingUnitButton"}],"pages":[{"component":"root","route":"stock-management"}],"version":"1.0.1-pre.653"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-stock-management-app",
3
- "version": "1.0.1-pre.641",
3
+ "version": "1.0.1-pre.653",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Stock management microfrontend for OpenMRS 3.x",
6
6
  "browser": "dist/openmrs-esm-stock-management-app.js",
@@ -0,0 +1,216 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import AddEditStockItem from './add-stock-item.component';
5
+ import { StockItemDTO } from '../../core/api/types/stockItem/StockItem';
6
+
7
+ // Mock components
8
+ jest.mock('react-i18next', () => ({
9
+ useTranslation: () => ({
10
+ t: (key) => key,
11
+ }),
12
+ }));
13
+ jest.mock('@carbon/react', () => {
14
+ const originalModule = jest.requireActual('@carbon/react');
15
+ return {
16
+ ...originalModule,
17
+ useMatchMedia: jest.fn().mockReturnValue(false),
18
+ };
19
+ });
20
+
21
+ jest.mock('@carbon/react/icons', () => ({
22
+ Save: () => <div>Save Icon</div>,
23
+ }));
24
+
25
+ jest.mock('./stock-item-details/stock-item-details.component', () => ({ model }) => (
26
+ <div data-testid="stock-item-details">Stock Item Details: {model.uuid}</div>
27
+ ));
28
+
29
+ jest.mock('./packaging-units/packaging-units.component', () => ({ stockItemUuid }) => (
30
+ <div data-testid="packaging-units">Packaging Units: {stockItemUuid || 'N/A'}</div>
31
+ ));
32
+
33
+ jest.mock('./transactions/transactions.component', () => ({ stockItemUuid }) => (
34
+ <div data-testid="transactions">Transactions: {stockItemUuid || 'N/A'}</div>
35
+ ));
36
+
37
+ jest.mock('./batch-information/batch-information.component', () => ({ stockItemUuid }) => (
38
+ <div data-testid="batch-information">Batch Information: {stockItemUuid || 'N/A'}</div>
39
+ ));
40
+
41
+ jest.mock('./quantities/quantities.component', () => ({ stockItemUuid }) => (
42
+ <div data-testid="quantities">Quantities: {stockItemUuid || 'N/A'}</div>
43
+ ));
44
+
45
+ jest.mock('./stock-item-rules/stock-item-rules.component', () => ({ stockItemUuid }) => (
46
+ <div data-testid="stock-rules">Rules: {stockItemUuid || 'N/A'}</div>
47
+ ));
48
+
49
+ jest.mock('./stock-item-references/stock-item-references.component', () => ({ stockItemUuid }) => (
50
+ <div data-testid="stock-references">References: {stockItemUuid || 'N/A'}</div>
51
+ ));
52
+
53
+ // Mock window.matchMedia
54
+ Object.defineProperty(window, 'matchMedia', {
55
+ writable: true,
56
+ value: jest.fn().mockImplementation((query) => ({
57
+ matches: false,
58
+ media: query,
59
+ onchange: null,
60
+ addListener: jest.fn(),
61
+ removeListener: jest.fn(),
62
+ addEventListener: jest.fn(),
63
+ removeEventListener: jest.fn(),
64
+ dispatchEvent: jest.fn(),
65
+ })),
66
+ });
67
+
68
+ describe('AddEditStockItem', () => {
69
+ const mockModel: StockItemDTO = {
70
+ uuid: 'test-uuid-123',
71
+ isDrug: true,
72
+ drugUuid: 'drug-uuid-456',
73
+ drugName: 'Test Drug',
74
+ conceptUuid: 'concept-uuid-789',
75
+ commonName: 'Test Common Name',
76
+ acronym: 'TCN',
77
+ conceptName: 'Test Concept Name',
78
+ hasExpiration: true,
79
+ packagingUnits: [
80
+ {
81
+ uuid: 'packaging-uuid-001',
82
+ stockItemUuid: 'test-uuid-123',
83
+ packagingUomName: 'Box',
84
+ packagingUomUuid: 'uom-uuid-001',
85
+ factor: 100,
86
+ isDefaultStockOperationsUoM: true,
87
+ isDispensingUnit: false,
88
+ },
89
+ ],
90
+ permission: {
91
+ canView: true,
92
+ canEdit: true,
93
+ canApprove: false,
94
+ canReceiveItems: true,
95
+ canDisplayReceivedItems: true,
96
+ isRequisitionAndCanIssueStock: false,
97
+ canUpdateBatchInformation: true,
98
+ },
99
+ categoryUuid: 'category-uuid-001',
100
+ categoryName: 'Test Category',
101
+ preferredVendorUuid: 'vendor-uuid-001',
102
+ preferredVendorName: 'Test Vendor',
103
+ purchasePrice: 10.99,
104
+ purchasePriceUoMUuid: 'uom-uuid-002',
105
+ purchasePriceUoMName: 'Each',
106
+ dispensingUnitName: 'Tablet',
107
+ dispensingUnitUuid: 'uom-uuid-003',
108
+ dispensingUnitPackagingUoMUuid: 'uom-uuid-003',
109
+ dispensingUnitPackagingUoMName: 'Tablet',
110
+ defaultStockOperationsUoMUuid: 'uom-uuid-001',
111
+ defaultStockOperationsUoMName: 'Box',
112
+ reorderLevel: 100,
113
+ reorderLevelUoMUuid: 'uom-uuid-001',
114
+ reorderLevelUoMName: 'Box',
115
+ dateCreated: new Date('2023-05-01T10:00:00Z'),
116
+ creatorGivenName: 'John',
117
+ creatorFamilyName: 'Doe',
118
+ voided: false,
119
+ expiryNotice: 30,
120
+ };
121
+
122
+ it('renders correctly with initial state and default selected tab', () => {
123
+ render(<AddEditStockItem model={mockModel} />);
124
+ expect(screen.getByTestId('stock-item-details')).toBeInTheDocument();
125
+ expect(screen.getByText('Stock Item Details: test-uuid-123')).toBeInTheDocument();
126
+ });
127
+
128
+ it('changes selected tab when clicking on different tabs', async () => {
129
+ const user = userEvent.setup();
130
+ render(<AddEditStockItem model={mockModel} isEditing={true} />);
131
+
132
+ await user.click(screen.getByText('packagingUnits'));
133
+ expect(screen.getByTestId('packaging-units')).toBeInTheDocument();
134
+ expect(screen.getByText('Packaging Units: test-uuid-123')).toBeInTheDocument();
135
+
136
+ await user.click(screen.getByText('transactions'));
137
+ expect(screen.getByTestId('transactions')).toBeInTheDocument();
138
+ expect(screen.getByText('Transactions: test-uuid-123')).toBeInTheDocument();
139
+ });
140
+
141
+ it('disables tabs when isEditing is false', () => {
142
+ render(<AddEditStockItem model={mockModel} isEditing={false} />);
143
+
144
+ const disabledTabs = [
145
+ 'packagingUnits',
146
+ 'transactions',
147
+ 'batchInformation',
148
+ 'quantities',
149
+ 'stockRules',
150
+ 'references',
151
+ ];
152
+ disabledTabs.forEach((tabName) => {
153
+ const tab = screen.getByRole('tab', { name: tabName });
154
+ expect(tab).toHaveAttribute('aria-disabled', 'true');
155
+ });
156
+ });
157
+
158
+ it('enables tabs when isEditing is true', () => {
159
+ render(<AddEditStockItem model={mockModel} isEditing={true} />);
160
+
161
+ const enabledTabs = [
162
+ 'packagingUnits',
163
+ 'transactions',
164
+ 'batchInformation',
165
+ 'quantities',
166
+ 'stockRules',
167
+ 'references',
168
+ ];
169
+ enabledTabs.forEach((tabName) => {
170
+ const tab = screen.getByRole('tab', { name: tabName });
171
+ expect(tab).not.toHaveAttribute('aria-disabled', 'true');
172
+ });
173
+ });
174
+
175
+ it('renders correct components based on model prop', async () => {
176
+ const user = userEvent.setup();
177
+ render(<AddEditStockItem model={mockModel} isEditing={true} />);
178
+
179
+ expect(screen.getByText('Stock Item Details: test-uuid-123')).toBeInTheDocument();
180
+
181
+ await user.click(screen.getByText('packagingUnits'));
182
+ expect(screen.getByText('Packaging Units: test-uuid-123')).toBeInTheDocument();
183
+
184
+ await user.click(screen.getByText('transactions'));
185
+ expect(screen.getByText('Transactions: test-uuid-123')).toBeInTheDocument();
186
+
187
+ await user.click(screen.getByText('batchInformation'));
188
+ expect(screen.getByText('Batch Information: test-uuid-123')).toBeInTheDocument();
189
+
190
+ await user.click(screen.getByText('quantities'));
191
+ expect(screen.getByText('Quantities: test-uuid-123')).toBeInTheDocument();
192
+
193
+ await user.click(screen.getByText('stockRules'));
194
+ expect(screen.getByText('Rules: test-uuid-123')).toBeInTheDocument();
195
+
196
+ await user.click(screen.getByText('references'));
197
+ expect(screen.getByText('References: test-uuid-123')).toBeInTheDocument();
198
+ });
199
+
200
+ it('translates tab names correctly', () => {
201
+ render(<AddEditStockItem model={mockModel} />);
202
+
203
+ const tabNames = [
204
+ 'stockItemDetails',
205
+ 'packagingUnits',
206
+ 'transactions',
207
+ 'batchInformation',
208
+ 'quantities',
209
+ 'stockRules',
210
+ 'references',
211
+ ];
212
+ tabNames.forEach((tabName) => {
213
+ expect(screen.getByText(tabName)).toBeInTheDocument();
214
+ });
215
+ });
216
+ });
@@ -0,0 +1,138 @@
1
+ import React from 'react';
2
+ import { render, waitFor, screen } from '@testing-library/react';
3
+ import StockOperationSubmission from './stock-operation-submission.component';
4
+ import { StockOperationDTO } from '../../core/api/types/stockOperation/StockOperationDTO';
5
+ import { StockOperationType } from '../../core/api/types/stockOperation/StockOperationType';
6
+ import { InitializeResult } from './types';
7
+ import userEvent from '@testing-library/user-event';
8
+
9
+ const mockOnGoBack = jest.fn();
10
+ const mockOnSave = jest.fn();
11
+ const mockOnComplete = jest.fn();
12
+ const mockOnSubmit = jest.fn();
13
+ const mockOnDispatch = jest.fn();
14
+ const defaultProps = {
15
+ canEdit: true,
16
+ locked: false,
17
+ model: { approvalRequired: null } as StockOperationDTO,
18
+ operation: {
19
+ name: 'Stock Issue',
20
+ description: 'Issuing stock',
21
+ operationType: 'stockissue',
22
+ hasSource: true,
23
+ sourceType: {},
24
+ hasDestination: true,
25
+ destinationType: {},
26
+ hasRecipient: false,
27
+ recipientRequired: false,
28
+ availableWhenReserved: false,
29
+ allowExpiredBatchNumbers: false,
30
+ stockOperationTypeLocationScopes: [],
31
+ } as StockOperationType,
32
+ setup: {
33
+ isNegativeQuantityAllowed: false,
34
+ requiresBatchUuid: false,
35
+ requiresActualBatchInfo: false,
36
+ isQuantityOptional: false,
37
+ } as InitializeResult,
38
+ actions: {
39
+ onGoBack: mockOnGoBack,
40
+ onSave: mockOnSave,
41
+ onComplete: mockOnComplete,
42
+ onSubmit: mockOnSubmit,
43
+ onDispatch: mockOnDispatch,
44
+ },
45
+ };
46
+
47
+ describe('StockOperationSubmission', () => {
48
+ let user;
49
+
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+ user = userEvent.setup();
53
+ });
54
+
55
+ it('renders without crashing', () => {
56
+ render(<StockOperationSubmission {...defaultProps} />);
57
+ expect(screen.getByText(/does the transaction require approval/i)).toBeInTheDocument();
58
+ });
59
+
60
+ it('allows approval required to be selected', async () => {
61
+ render(<StockOperationSubmission {...defaultProps} />);
62
+ await user.click(screen.getByLabelText(/yes/i));
63
+ const yesRadioButton = screen.getByLabelText(/yes/i) as HTMLInputElement;
64
+ expect(yesRadioButton.checked).toBe(true);
65
+ });
66
+
67
+ it('allows approval not required to be selected', async () => {
68
+ render(<StockOperationSubmission {...defaultProps} />);
69
+ await user.click(screen.getByLabelText(/no/i));
70
+ const noRadioButton = screen.getByLabelText(/no/i) as HTMLInputElement;
71
+ expect(noRadioButton.checked).toBe(true);
72
+ });
73
+
74
+ it('calls onGoBack when Go Back button is clicked', async () => {
75
+ render(<StockOperationSubmission {...defaultProps} />);
76
+ await user.click(screen.getByText(/go back/i));
77
+ expect(mockOnGoBack).toHaveBeenCalled();
78
+ });
79
+
80
+ it('shows loading indicator when saving', async () => {
81
+ render(<StockOperationSubmission {...defaultProps} />);
82
+ await user.click(screen.getByText(/save/i));
83
+ await waitFor(() => {
84
+ expect(mockOnSave).toHaveBeenCalled();
85
+ });
86
+ });
87
+
88
+ it('calls onSave when save button is clicked', async () => {
89
+ render(<StockOperationSubmission {...defaultProps} />);
90
+ await user.click(screen.getByText(/save/i));
91
+ await waitFor(() => {
92
+ expect(mockOnSave).toHaveBeenCalled();
93
+ });
94
+ });
95
+
96
+ it('disables the save button while saving', async () => {
97
+ const mockOnSave = jest.fn<Promise<void>, [StockOperationDTO]>(
98
+ (model) => new Promise((resolve) => setTimeout(resolve, 500)),
99
+ );
100
+
101
+ const testProps = {
102
+ ...defaultProps,
103
+ actions: {
104
+ ...defaultProps.actions,
105
+ onSave: mockOnSave,
106
+ },
107
+ };
108
+
109
+ render(<StockOperationSubmission {...testProps} />);
110
+ const saveButton = screen.getByRole('button', { name: 'Save' });
111
+ await user.click(saveButton);
112
+
113
+ expect(saveButton).toBeDisabled();
114
+
115
+ await waitFor(() => {
116
+ expect(saveButton).not.toBeDisabled();
117
+ });
118
+ });
119
+
120
+ it('calls onComplete when Complete button is clicked', async () => {
121
+ render(<StockOperationSubmission {...defaultProps} />);
122
+ await user.click(screen.getByText(/complete/i));
123
+ await waitFor(() => {
124
+ expect(mockOnComplete).toHaveBeenCalled();
125
+ });
126
+ });
127
+
128
+ it('does not render buttons when locked', () => {
129
+ const lockedProps = {
130
+ ...defaultProps,
131
+ locked: true,
132
+ };
133
+
134
+ render(<StockOperationSubmission {...lockedProps} />);
135
+ expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
136
+ expect(screen.queryByRole('button', { name: /complete/i })).not.toBeInTheDocument();
137
+ });
138
+ });