@openmrs/esm-patient-label-printing-app 11.3.1-patch.9508
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 +37 -0
- package/README.md +3 -0
- package/dist/173.js +1 -0
- package/dist/173.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/326.js +1 -0
- package/dist/326.js.map +1 -0
- package/dist/336.js +1 -0
- package/dist/336.js.map +1 -0
- package/dist/350.js +2 -0
- package/dist/350.js.LICENSE.txt +39 -0
- package/dist/350.js.map +1 -0
- package/dist/41.js +2 -0
- package/dist/41.js.LICENSE.txt +9 -0
- package/dist/41.js.map +1 -0
- package/dist/588.js +2 -0
- package/dist/588.js.LICENSE.txt +50 -0
- package/dist/588.js.map +1 -0
- package/dist/913.js +2 -0
- package/dist/913.js.LICENSE.txt +32 -0
- package/dist/913.js.map +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-patient-label-printing-app.js +1 -0
- package/dist/openmrs-esm-patient-label-printing-app.js.buildmanifest.json +344 -0
- package/dist/openmrs-esm-patient-label-printing-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +52 -0
- package/src/config-schema.ts +13 -0
- package/src/declarations.d.ts +4 -0
- package/src/hooks/useStickerPdfPrinter.test.tsx +237 -0
- package/src/hooks/useStickerPdfPrinter.tsx +93 -0
- package/src/index.ts +18 -0
- package/src/print-identifier-sticker/print-identifier-sticker-action-button.component.tsx +62 -0
- package/src/print-identifier-sticker/print-identifier-sticker-action-button.scss +3 -0
- package/src/print-identifier-sticker/print-identifier-sticker-action-button.test.tsx +127 -0
- package/src/routes.json +15 -0
- package/translations/en.json +3 -0
- package/tsconfig.json +4 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { useStickerPdfPrinter } from './useStickerPdfPrinter';
|
|
3
|
+
|
|
4
|
+
describe('useStickerPdfPrinter', () => {
|
|
5
|
+
let mockContentWindow: any;
|
|
6
|
+
let afterPrintHandler: (() => void) | null = null;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
afterPrintHandler = null;
|
|
10
|
+
|
|
11
|
+
// Create a mock contentWindow with all required methods
|
|
12
|
+
mockContentWindow = {
|
|
13
|
+
print: jest.fn(),
|
|
14
|
+
focus: jest.fn(),
|
|
15
|
+
addEventListener: jest.fn((event: string, handler: () => void) => {
|
|
16
|
+
if (event === 'afterprint') {
|
|
17
|
+
afterPrintHandler = handler;
|
|
18
|
+
}
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Mock HTMLIFrameElement.prototype.contentWindow to return our mock
|
|
23
|
+
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
|
24
|
+
configurable: true,
|
|
25
|
+
get: () => mockContentWindow,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
Object.defineProperty(HTMLIFrameElement.prototype, 'src', {
|
|
29
|
+
configurable: true,
|
|
30
|
+
set: function (value) {
|
|
31
|
+
this._src = value;
|
|
32
|
+
// Trigger onload asynchronously to simulate real behavior
|
|
33
|
+
if (this.onload) {
|
|
34
|
+
Promise.resolve().then(() => {
|
|
35
|
+
if (this.onload) {
|
|
36
|
+
this.onload({} as Event);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
get: function () {
|
|
42
|
+
return this._src;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Mock document.hasFocus to support the polling mechanism
|
|
47
|
+
document.hasFocus = jest.fn().mockReturnValue(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
jest.restoreAllMocks();
|
|
52
|
+
jest.useRealTimers();
|
|
53
|
+
afterPrintHandler = null;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const waitForIframeLoad = () => {
|
|
57
|
+
// Wait for next tick to allow iframe onload to trigger
|
|
58
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const triggerPrintCompletion = () => {
|
|
62
|
+
// Simulate the print dialog closing by triggering afterprint event
|
|
63
|
+
if (afterPrintHandler) {
|
|
64
|
+
afterPrintHandler();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
it('should provide printPdf function and isPrinting state', () => {
|
|
69
|
+
const { result } = renderHook(() => useStickerPdfPrinter());
|
|
70
|
+
|
|
71
|
+
expect(result.current.isPrinting).toBe(false);
|
|
72
|
+
expect(typeof result.current.printPdf).toBe('function');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should set isPrinting to true when printing starts', () => {
|
|
76
|
+
const { result } = renderHook(() => useStickerPdfPrinter());
|
|
77
|
+
|
|
78
|
+
act(() => {
|
|
79
|
+
result.current.printPdf('http://example.com/test.pdf');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(result.current.isPrinting).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should reject concurrent print requests with an error', async () => {
|
|
86
|
+
const { result } = renderHook(() => useStickerPdfPrinter());
|
|
87
|
+
|
|
88
|
+
act(() => {
|
|
89
|
+
result.current.printPdf('http://example.com/test.pdf');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await expect(result.current.printPdf('http://example.com/test2.pdf')).rejects.toThrow('Print already in progress');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should reset isPrinting to false when printing completes', async () => {
|
|
96
|
+
const { result } = renderHook(() => useStickerPdfPrinter());
|
|
97
|
+
|
|
98
|
+
act(() => {
|
|
99
|
+
result.current.printPdf('http://example.com/test.pdf');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.current.isPrinting).toBe(true);
|
|
103
|
+
|
|
104
|
+
// Wait for iframe to load
|
|
105
|
+
await act(async () => {
|
|
106
|
+
await waitForIframeLoad();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Simulate print completion
|
|
110
|
+
act(() => {
|
|
111
|
+
triggerPrintCompletion();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(result.current.isPrinting).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return a promise that resolves when printing completes', async () => {
|
|
120
|
+
const { result } = renderHook(() => useStickerPdfPrinter());
|
|
121
|
+
|
|
122
|
+
let resolved = false;
|
|
123
|
+
let printPromise: Promise<void>;
|
|
124
|
+
|
|
125
|
+
act(() => {
|
|
126
|
+
printPromise = result.current.printPdf('http://example.com/test.pdf').then(() => {
|
|
127
|
+
resolved = true;
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(resolved).toBe(false);
|
|
132
|
+
|
|
133
|
+
// Wait for iframe to load
|
|
134
|
+
await act(async () => {
|
|
135
|
+
await waitForIframeLoad();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Simulate print completion
|
|
139
|
+
act(() => {
|
|
140
|
+
triggerPrintCompletion();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(resolved).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await printPromise!;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should allow printing again after previous print completes', async () => {
|
|
151
|
+
const { result } = renderHook(() => useStickerPdfPrinter());
|
|
152
|
+
|
|
153
|
+
// First print
|
|
154
|
+
act(() => {
|
|
155
|
+
result.current.printPdf('http://example.com/test1.pdf');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await act(async () => {
|
|
159
|
+
await waitForIframeLoad();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
act(() => {
|
|
163
|
+
triggerPrintCompletion();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await waitFor(() => {
|
|
167
|
+
expect(result.current.isPrinting).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Second print should succeed
|
|
171
|
+
act(() => {
|
|
172
|
+
result.current.printPdf('http://example.com/test2.pdf');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.current.isPrinting).toBe(true);
|
|
176
|
+
|
|
177
|
+
await act(async () => {
|
|
178
|
+
await waitForIframeLoad();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
act(() => {
|
|
182
|
+
triggerPrintCompletion();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await waitFor(() => {
|
|
186
|
+
expect(result.current.isPrinting).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should reset isPrinting after timeout when print cannot be detected as complete', async () => {
|
|
191
|
+
jest.useFakeTimers();
|
|
192
|
+
const { result } = renderHook(() => useStickerPdfPrinter());
|
|
193
|
+
|
|
194
|
+
act(() => {
|
|
195
|
+
result.current.printPdf('http://example.com/test.pdf');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result.current.isPrinting).toBe(true);
|
|
199
|
+
|
|
200
|
+
// Fast-forward time to trigger iframe load, then advance past timeout
|
|
201
|
+
// The iframe onload will be triggered via Promise.resolve() which needs runAllTimers
|
|
202
|
+
await act(async () => {
|
|
203
|
+
await jest.runAllTimersAsync();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Verify timeout mechanism resets isPrinting (afterprint never fired)
|
|
207
|
+
expect(result.current.isPrinting).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should handle errors gracefully and reset isPrinting state', async () => {
|
|
211
|
+
// Mock contentWindow to return null to simulate an error
|
|
212
|
+
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
|
213
|
+
configurable: true,
|
|
214
|
+
get: () => null,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const { result } = renderHook(() => useStickerPdfPrinter());
|
|
218
|
+
|
|
219
|
+
let resolved = false;
|
|
220
|
+
act(() => {
|
|
221
|
+
result.current.printPdf('http://example.com/test.pdf').then(() => {
|
|
222
|
+
resolved = true;
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Wait for iframe to attempt loading and trigger error path
|
|
227
|
+
await act(async () => {
|
|
228
|
+
await waitForIframeLoad();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await waitFor(() => {
|
|
232
|
+
expect(result.current.isPrinting).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(resolved).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
export const useStickerPdfPrinter = () => {
|
|
5
|
+
const { t } = useTranslation();
|
|
6
|
+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
7
|
+
const [isPrinting, setIsPrinting] = useState(false);
|
|
8
|
+
|
|
9
|
+
const printPdf = useCallback(
|
|
10
|
+
(url: string) => {
|
|
11
|
+
if (isPrinting) {
|
|
12
|
+
return Promise.reject(new Error(t('printInProgress', 'Print already in progress')));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return new Promise<void>((resolve) => {
|
|
16
|
+
setIsPrinting(true);
|
|
17
|
+
|
|
18
|
+
if (!iframeRef.current) {
|
|
19
|
+
const iframe = document.createElement('iframe');
|
|
20
|
+
iframe.name = 'pdfPrinterFrame';
|
|
21
|
+
iframe.setAttribute('aria-hidden', 'true');
|
|
22
|
+
Object.assign(iframe.style, {
|
|
23
|
+
position: 'fixed',
|
|
24
|
+
width: '0',
|
|
25
|
+
height: '0',
|
|
26
|
+
border: 'none',
|
|
27
|
+
visibility: 'hidden',
|
|
28
|
+
pointerEvents: 'none',
|
|
29
|
+
});
|
|
30
|
+
iframeRef.current = iframe;
|
|
31
|
+
document.body.appendChild(iframe);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const iframe = iframeRef.current;
|
|
35
|
+
let hasClosed = false;
|
|
36
|
+
|
|
37
|
+
const handleLoad = () => {
|
|
38
|
+
try {
|
|
39
|
+
const contentWindow = iframe.contentWindow;
|
|
40
|
+
if (!contentWindow) throw new Error('No content window');
|
|
41
|
+
|
|
42
|
+
const cleanup = () => {
|
|
43
|
+
if (hasClosed) return;
|
|
44
|
+
hasClosed = true;
|
|
45
|
+
setIsPrinting(false);
|
|
46
|
+
resolve();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
contentWindow.addEventListener('afterprint', cleanup, { once: true });
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Cross-origin, use polling fallback
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
contentWindow.focus();
|
|
56
|
+
contentWindow.print();
|
|
57
|
+
|
|
58
|
+
let wasFocused = false;
|
|
59
|
+
const pollInterval = setInterval(() => {
|
|
60
|
+
const hasFocus = document.hasFocus();
|
|
61
|
+
if (hasFocus && wasFocused) cleanup();
|
|
62
|
+
if (!hasFocus) wasFocused = true;
|
|
63
|
+
}, 250);
|
|
64
|
+
|
|
65
|
+
setTimeout(cleanup, 30000);
|
|
66
|
+
setTimeout(() => clearInterval(pollInterval), 30000);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
setIsPrinting(false);
|
|
69
|
+
resolve();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
iframe.onload = handleLoad;
|
|
74
|
+
iframe.onerror = () => {
|
|
75
|
+
setIsPrinting(false);
|
|
76
|
+
resolve();
|
|
77
|
+
};
|
|
78
|
+
iframe.src = url;
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
[t, isPrinting],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
return () => {
|
|
86
|
+
if (iframeRef.current?.parentNode) {
|
|
87
|
+
iframeRef.current.parentNode.removeChild(iframeRef.current);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
return { printPdf, isPrinting };
|
|
93
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfigSchema, getAsyncLifecycle } from '@openmrs/esm-framework';
|
|
2
|
+
import { configSchema } from './config-schema';
|
|
3
|
+
|
|
4
|
+
const moduleName = '@openmrs/esm-patient-label-printing-app';
|
|
5
|
+
|
|
6
|
+
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
7
|
+
|
|
8
|
+
export function startupApp() {
|
|
9
|
+
defineConfigSchema(moduleName, configSchema);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const printIdentifierStickerActionButton = getAsyncLifecycle(
|
|
13
|
+
() => import('./print-identifier-sticker/print-identifier-sticker-action-button.component'),
|
|
14
|
+
{
|
|
15
|
+
featureName: 'patient-actions-slot-print-identifier-sticker-button',
|
|
16
|
+
moduleName,
|
|
17
|
+
},
|
|
18
|
+
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { OverflowMenuItem } from '@carbon/react';
|
|
4
|
+
import { showSnackbar, getCoreTranslation, useConfig, UserHasAccess, restBaseUrl } from '@openmrs/esm-framework';
|
|
5
|
+
import styles from './print-identifier-sticker-action-button.scss';
|
|
6
|
+
import { useStickerPdfPrinter } from '../hooks/useStickerPdfPrinter';
|
|
7
|
+
import type { ConfigObject } from '../config-schema';
|
|
8
|
+
|
|
9
|
+
interface PrintIdentifierStickerOverflowMenuItemProps {
|
|
10
|
+
patient: fhir.Patient;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PrintIdentifierStickerOverflowMenuItem: React.FC<PrintIdentifierStickerOverflowMenuItemProps> = ({ patient }) => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const { showPrintIdentifierStickerButton } = useConfig<ConfigObject>();
|
|
16
|
+
const { printPdf, isPrinting } = useStickerPdfPrinter();
|
|
17
|
+
|
|
18
|
+
const isVisible = useMemo(() => {
|
|
19
|
+
if (!patient?.id) return false;
|
|
20
|
+
return showPrintIdentifierStickerButton;
|
|
21
|
+
}, [showPrintIdentifierStickerButton, patient?.id]);
|
|
22
|
+
|
|
23
|
+
const getPdfUrl = useCallback(() => {
|
|
24
|
+
if (!patient?.id) {
|
|
25
|
+
throw new Error(t('patientIdNotFound', 'Patient ID not found'));
|
|
26
|
+
}
|
|
27
|
+
return `${window.openmrsBase}${restBaseUrl}/patientdocuments/patientIdSticker?patientUuid=${patient.id}`;
|
|
28
|
+
}, [patient?.id, t]);
|
|
29
|
+
|
|
30
|
+
const handlePrint = useCallback(async () => {
|
|
31
|
+
if (isPrinting) return;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await printPdf(getPdfUrl());
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
37
|
+
showSnackbar({
|
|
38
|
+
kind: 'error',
|
|
39
|
+
title: getCoreTranslation('printError', 'Print Error'),
|
|
40
|
+
subtitle: getCoreTranslation('printErrorExplainer', '', { errorLocation: errorMessage }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}, [getPdfUrl, printPdf, isPrinting]);
|
|
44
|
+
|
|
45
|
+
const buttonText = useMemo(() => {
|
|
46
|
+
return isPrinting
|
|
47
|
+
? getCoreTranslation('printing', 'Printing...')
|
|
48
|
+
: getCoreTranslation('printIdentifierSticker', 'Print identifier sticker');
|
|
49
|
+
}, [isPrinting]);
|
|
50
|
+
|
|
51
|
+
if (!isVisible) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<UserHasAccess privilege="App: Can generate a Patient Identity Sticker">
|
|
57
|
+
<OverflowMenuItem className={styles.menuitem} itemText={buttonText} onClick={handlePrint} disabled={isPrinting} />
|
|
58
|
+
</UserHasAccess>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default PrintIdentifierStickerOverflowMenuItem;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { screen } from '@testing-library/react';
|
|
4
|
+
import { getDefaultsFromConfigSchema, showSnackbar, useConfig, UserHasAccess } from '@openmrs/esm-framework';
|
|
5
|
+
import { mockFhirPatient } from '__mocks__';
|
|
6
|
+
import { renderWithSwr } from 'tools';
|
|
7
|
+
import { useStickerPdfPrinter } from '../hooks/useStickerPdfPrinter';
|
|
8
|
+
import { configSchema, type ConfigObject } from '../config-schema';
|
|
9
|
+
import PrintIdentifierStickerOverflowMenuItem from './print-identifier-sticker-action-button.component';
|
|
10
|
+
|
|
11
|
+
jest.mock('../hooks/useStickerPdfPrinter');
|
|
12
|
+
jest.mock('@openmrs/esm-framework', () => ({
|
|
13
|
+
...jest.requireActual('@openmrs/esm-framework'),
|
|
14
|
+
UserHasAccess: jest.fn(({ children }) => children),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
|
|
18
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
19
|
+
const mockUseStickerPdfPrinter = jest.mocked(useStickerPdfPrinter);
|
|
20
|
+
const mockUserHasAccess = jest.mocked(UserHasAccess);
|
|
21
|
+
const mockPrintPdf = jest.fn();
|
|
22
|
+
|
|
23
|
+
describe('PrintIdentifierStickerOverflowMenuItem', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockUseConfig.mockReturnValue({
|
|
26
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
27
|
+
showPrintIdentifierStickerButton: true,
|
|
28
|
+
} as ConfigObject);
|
|
29
|
+
mockPrintPdf.mockResolvedValue(undefined);
|
|
30
|
+
mockUseStickerPdfPrinter.mockReturnValue({
|
|
31
|
+
printPdf: mockPrintPdf,
|
|
32
|
+
isPrinting: false,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders the print button when enabled in config', () => {
|
|
37
|
+
renderWithSwr(<PrintIdentifierStickerOverflowMenuItem patient={mockFhirPatient} />);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByRole('menuitem', { name: /print identifier sticker/i })).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not render the button when disabled in config', () => {
|
|
43
|
+
mockUseConfig.mockReturnValue({
|
|
44
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
45
|
+
showPrintIdentifierStickerButton: false,
|
|
46
|
+
} as ConfigObject);
|
|
47
|
+
|
|
48
|
+
renderWithSwr(<PrintIdentifierStickerOverflowMenuItem patient={mockFhirPatient} />);
|
|
49
|
+
|
|
50
|
+
expect(screen.queryByRole('menuitem', { name: /print identifier sticker/i })).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('does not render the button when patient ID is missing', () => {
|
|
54
|
+
const patientWithoutId = { ...mockFhirPatient, id: undefined } as fhir.Patient;
|
|
55
|
+
|
|
56
|
+
renderWithSwr(<PrintIdentifierStickerOverflowMenuItem patient={patientWithoutId} />);
|
|
57
|
+
|
|
58
|
+
expect(screen.queryByRole('menuitem', { name: /print identifier sticker/i })).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('triggers print when button is clicked', async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
renderWithSwr(<PrintIdentifierStickerOverflowMenuItem patient={mockFhirPatient} />);
|
|
64
|
+
|
|
65
|
+
const printButton = screen.getByRole('menuitem', { name: /print identifier sticker/i });
|
|
66
|
+
await user.click(printButton);
|
|
67
|
+
|
|
68
|
+
expect(mockPrintPdf).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(mockPrintPdf).toHaveBeenCalledWith(expect.stringContaining(mockFhirPatient.id));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('shows error notification when print fails', async () => {
|
|
73
|
+
const user = userEvent.setup();
|
|
74
|
+
const errorMessage = 'Network error';
|
|
75
|
+
mockPrintPdf.mockRejectedValueOnce(new Error(errorMessage));
|
|
76
|
+
|
|
77
|
+
renderWithSwr(<PrintIdentifierStickerOverflowMenuItem patient={mockFhirPatient} />);
|
|
78
|
+
|
|
79
|
+
const printButton = screen.getByRole('menuitem', { name: /print identifier sticker/i });
|
|
80
|
+
await user.click(printButton);
|
|
81
|
+
|
|
82
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith({
|
|
83
|
+
kind: 'error',
|
|
84
|
+
title: 'Print error',
|
|
85
|
+
subtitle: expect.stringContaining(errorMessage),
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('shows loading state when printing', () => {
|
|
90
|
+
mockUseStickerPdfPrinter.mockReturnValue({
|
|
91
|
+
printPdf: mockPrintPdf,
|
|
92
|
+
isPrinting: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
renderWithSwr(<PrintIdentifierStickerOverflowMenuItem patient={mockFhirPatient} />);
|
|
96
|
+
|
|
97
|
+
const printButton = screen.getByRole('menuitem', { name: /printing/i });
|
|
98
|
+
expect(printButton).toBeInTheDocument();
|
|
99
|
+
expect(printButton).toBeDisabled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('prevents multiple print calls when already printing', async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
mockUseStickerPdfPrinter.mockReturnValue({
|
|
105
|
+
printPdf: mockPrintPdf,
|
|
106
|
+
isPrinting: true,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
renderWithSwr(<PrintIdentifierStickerOverflowMenuItem patient={mockFhirPatient} />);
|
|
110
|
+
|
|
111
|
+
const printButton = screen.getByRole('menuitem', { name: /printing/i });
|
|
112
|
+
await user.click(printButton);
|
|
113
|
+
|
|
114
|
+
expect(mockPrintPdf).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('checks for the correct privilege when rendering', () => {
|
|
118
|
+
renderWithSwr(<PrintIdentifierStickerOverflowMenuItem patient={mockFhirPatient} />);
|
|
119
|
+
|
|
120
|
+
expect(mockUserHasAccess).toHaveBeenCalledWith(
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
privilege: 'App: Can generate a Patient Identity Sticker',
|
|
123
|
+
}),
|
|
124
|
+
expect.anything(),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
package/src/routes.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {
|
|
4
|
+
"patientdocuments": "^1.0.0-SNAPSHOT"
|
|
5
|
+
},
|
|
6
|
+
"extensions": [
|
|
7
|
+
{
|
|
8
|
+
"name": "print-identifier-sticker-button",
|
|
9
|
+
"slot": "patient-actions-slot",
|
|
10
|
+
"component": "printIdentifierStickerActionButton",
|
|
11
|
+
"online": true,
|
|
12
|
+
"offline": true
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('openmrs/default-webpack-config');
|