@openmrs/esm-error-handling 10.0.1-pre.5664 → 10.0.1-pre.5670

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.
@@ -1 +1 @@
1
- Successfully compiled: 1 file with swc (201.53ms)
1
+ Successfully compiled: 1 file with swc (223.09ms)
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8CA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,QAKvC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,KAExB,aAAa,OAAO,UAK7B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmCA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,QAKvC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,KAExB,aAAa,OAAO,UAK7B"}
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  window.onerror = function(message, _source, _lineno, _colno, error) {
6
6
  console.error('Unexpected error: ', error ?? message);
7
7
  dispatchToastShown({
8
- description: getErrorMessage(error ?? message, 'Oops! An unexpected error occurred.'),
8
+ description: toToastDescription(error, 'Oops! An unexpected error occurred.'),
9
9
  kind: 'error',
10
10
  title: 'Error'
11
11
  });
@@ -14,27 +14,18 @@ window.onerror = function(message, _source, _lineno, _colno, error) {
14
14
  window.onunhandledrejection = function(event) {
15
15
  console.error('Unhandled rejection: ', event.reason);
16
16
  dispatchToastShown({
17
- // `event.reason` is whatever the promise rejected with, frequently an Error
18
- // object. `showToast` expects `description` to be a renderable, non-empty
19
- // value, so resolve it to a string message rather than passing the raw
20
- // rejection (which renders as "[object Object]" and fails the non-empty check).
21
- description: getErrorMessage(event.reason, 'Oops! An unhandled promise rejection occurred.'),
17
+ description: toToastDescription(event.reason, 'Oops! An unhandled promise rejection occurred.'),
22
18
  kind: 'error',
23
19
  title: 'Error'
24
20
  });
25
21
  };
26
22
  /**
27
- * Resolves an arbitrary thrown/rejected value to a user-facing, non-empty
28
- * message string suitable for use as a toast description, falling back to the
29
- * provided message when no usable message can be derived.
30
- */ function getErrorMessage(reason, fallback) {
31
- if (reason instanceof Error) {
32
- return reason.message || fallback;
33
- }
34
- if (typeof reason === 'string' && reason.trim().length > 0) {
35
- return reason;
36
- }
37
- return fallback;
23
+ * Tries to turn whatever reached the global error handler into
24
+ * a sensible string, returning the caller's fallback if a non-empty
25
+ * string could not be constructed.
26
+ */ function toToastDescription(thing, fallback) {
27
+ const { message } = ensureErrorObject(thing);
28
+ return typeof message === 'string' && message ? message : fallback;
38
29
  }
39
30
  /**
40
31
  * Reports an error to the global error handler. The error will be displayed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-error-handling",
3
- "version": "10.0.1-pre.5664",
3
+ "version": "10.0.1-pre.5670",
4
4
  "license": "MPL-2.0",
5
5
  "description": "An ES module to help with error handling",
6
6
  "type": "module",
@@ -46,10 +46,10 @@
46
46
  "access": "public"
47
47
  },
48
48
  "peerDependencies": {
49
- "@openmrs/esm-globals": "^10.0.1-pre.5664"
49
+ "@openmrs/esm-globals": "^10.0.1-pre.5670"
50
50
  },
51
51
  "devDependencies": {
52
- "@openmrs/esm-globals": "10.0.1-pre.5664",
52
+ "@openmrs/esm-globals": "10.0.1-pre.5670",
53
53
  "@swc/cli": "0.8.1",
54
54
  "@swc/core": "1.15.21",
55
55
  "@vitest/coverage-v8": "^4.1.2",
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import { dispatchToastShown } from '@openmrs/esm-globals';
7
7
  window.onerror = function (message, _source, _lineno, _colno, error) {
8
8
  console.error('Unexpected error: ', error ?? message);
9
9
  dispatchToastShown({
10
- description: getErrorMessage(error ?? message, 'Oops! An unexpected error occurred.'),
10
+ description: toToastDescription(error, 'Oops! An unexpected error occurred.'),
11
11
  kind: 'error',
12
12
  title: 'Error',
13
13
  });
@@ -17,31 +17,20 @@ window.onerror = function (message, _source, _lineno, _colno, error) {
17
17
  window.onunhandledrejection = function (event: PromiseRejectionEvent) {
18
18
  console.error('Unhandled rejection: ', event.reason);
19
19
  dispatchToastShown({
20
- // `event.reason` is whatever the promise rejected with, frequently an Error
21
- // object. `showToast` expects `description` to be a renderable, non-empty
22
- // value, so resolve it to a string message rather than passing the raw
23
- // rejection (which renders as "[object Object]" and fails the non-empty check).
24
- description: getErrorMessage(event.reason, 'Oops! An unhandled promise rejection occurred.'),
20
+ description: toToastDescription(event.reason, 'Oops! An unhandled promise rejection occurred.'),
25
21
  kind: 'error',
26
22
  title: 'Error',
27
23
  });
28
24
  };
29
25
 
30
26
  /**
31
- * Resolves an arbitrary thrown/rejected value to a user-facing, non-empty
32
- * message string suitable for use as a toast description, falling back to the
33
- * provided message when no usable message can be derived.
27
+ * Tries to turn whatever reached the global error handler into
28
+ * a sensible string, returning the caller's fallback if a non-empty
29
+ * string could not be constructed.
34
30
  */
35
- function getErrorMessage(reason: unknown, fallback: string): string {
36
- if (reason instanceof Error) {
37
- return reason.message || fallback;
38
- }
39
-
40
- if (typeof reason === 'string' && reason.trim().length > 0) {
41
- return reason;
42
- }
43
-
44
- return fallback;
31
+ function toToastDescription(thing: unknown, fallback: string) {
32
+ const { message } = ensureErrorObject(thing);
33
+ return typeof message === 'string' && message ? message : fallback;
45
34
  }
46
35
 
47
36
  /**
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
1
+ import { beforeEach, describe, it, expect, vi } from 'vitest';
2
2
  import { dispatchToastShown } from '@openmrs/esm-globals';
3
3
  import { reportError } from './index';
4
4
 
@@ -10,6 +10,33 @@ vi.useFakeTimers();
10
10
 
11
11
  const mockDispatchToastShown = vi.mocked(dispatchToastShown);
12
12
 
13
+ function errorWithObjectMessage() {
14
+ const error = new Error('replaced below');
15
+ (error as unknown as { message: unknown }).message = { nested: 'value' };
16
+ return error;
17
+ }
18
+
19
+ beforeEach(() => {
20
+ vi.spyOn(console, 'error').mockImplementation(() => {});
21
+ });
22
+
23
+ /**
24
+ * The shapes a thrown value can take when it reaches the global handlers, as the `error`
25
+ * argument of `window.onerror` or as `event.reason` in `window.onunhandledrejection`.
26
+ * `null` in `expected` means the handler's fallback text is expected.
27
+ */
28
+ const errorShapes: Array<{ label: string; reason: unknown; expected: string | null }> = [
29
+ { label: 'an Error instance', reason: new Error('something exploded'), expected: 'something exploded' },
30
+ { label: 'an Error with an empty message', reason: new Error(''), expected: null },
31
+ { label: 'a string', reason: 'something failed', expected: 'something failed' },
32
+ { label: 'a plain object', reason: { foo: 'bar' }, expected: 'Object thrown as error: {"foo":"bar"}' },
33
+ { label: 'null', reason: null, expected: "'null' was thrown as an error" },
34
+ { label: 'undefined', reason: undefined, expected: "'undefined' was thrown as an error" },
35
+ // `Error#message` is writable, so an Error can reach the handlers carrying a non-string message.
36
+ // `ensureErrorObject()` passes such an Error through untouched, so the guard has to be downstream.
37
+ { label: 'an Error whose message is not a string', reason: errorWithObjectMessage(), expected: null },
38
+ ];
39
+
13
40
  describe('error handler', () => {
14
41
  it('transforms non-Error inputs into valid Error objects', () => {
15
42
  expect(() => {
@@ -34,40 +61,38 @@ describe('error handler', () => {
34
61
  });
35
62
  });
36
63
 
37
- describe('window.onunhandledrejection', () => {
38
- beforeEach(() => {
39
- mockDispatchToastShown.mockClear();
40
- vi.spyOn(console, 'error').mockImplementation(() => {});
41
- });
64
+ describe('Global error handler', () => {
65
+ // Browsers call `window.onerror` with the message first and the thrown value fifth.
66
+ function triggerOnError(error: unknown) {
67
+ window.onerror?.('Uncaught error', undefined, undefined, undefined, error as Error);
68
+ }
42
69
 
43
- // The handler only reads `event.reason`, so a minimal stand-in avoids
44
- // constructing a real (and itself-unhandled) PromiseRejectionEvent.
45
- const fireRejection = (reason: unknown) => window.onunhandledrejection?.({ reason } as PromiseRejectionEvent);
70
+ const fallback = 'Oops! An unexpected error occurred.';
46
71
 
47
- it('shows a toast with the Error message as a string description', () => {
48
- fireRejection(new Error('Something broke'));
72
+ it.each(errorShapes)('shows a string description for $label', ({ reason, expected }) => {
73
+ triggerOnError(reason);
49
74
 
50
- expect(mockDispatchToastShown).toHaveBeenCalledWith(
51
- expect.objectContaining({ description: 'Something broke', kind: 'error', title: 'Error' }),
52
- );
53
- expect(typeof mockDispatchToastShown.mock.calls[0][0].description).toBe('string');
75
+ expect(mockDispatchToastShown).toHaveBeenCalledOnce();
76
+ const { description } = mockDispatchToastShown.mock.calls[0][0];
77
+ // The regression itself: an object here is unrenderable as a React child, so the toast never appears.
78
+ expect(typeof description).toBe('string');
79
+ expect(description).toBe(expected ?? fallback);
54
80
  });
81
+ });
55
82
 
56
- it('shows a string reason directly', () => {
57
- fireRejection('plain string reason');
83
+ describe('Global rejection handler', () => {
84
+ function triggerOnUnhandledRejection(reason: unknown) {
85
+ (window.onunhandledrejection as (event: PromiseRejectionEvent) => void)({ reason } as PromiseRejectionEvent);
86
+ }
58
87
 
59
- expect(mockDispatchToastShown).toHaveBeenCalledWith(
60
- expect.objectContaining({ description: 'plain string reason' }),
61
- );
62
- });
88
+ const fallback = 'Oops! An unhandled promise rejection occurred.';
89
+
90
+ it.each(errorShapes)('shows a string description for $label', ({ reason, expected }) => {
91
+ triggerOnUnhandledRejection(reason);
63
92
 
64
- it('falls back to a friendly message for empty/non-string reasons', () => {
65
- for (const reason of [undefined, null, new Error(''), { some: 'object' }]) {
66
- mockDispatchToastShown.mockClear();
67
- fireRejection(reason);
68
- expect(mockDispatchToastShown.mock.calls[0][0].description).toBe(
69
- 'Oops! An unhandled promise rejection occurred.',
70
- );
71
- }
93
+ expect(mockDispatchToastShown).toHaveBeenCalledOnce();
94
+ const { description } = mockDispatchToastShown.mock.calls[0][0];
95
+ expect(typeof description).toBe('string');
96
+ expect(description).toBe(expected ?? fallback);
72
97
  });
73
98
  });