@openmrs/esm-react-utils 4.0.0-pre.0 → 4.0.1-pre.206

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.
@@ -0,0 +1,276 @@
1
+ import React, { useCallback, useReducer } from "react";
2
+ import { render, screen, waitFor, within } from "@testing-library/react";
3
+ import {
4
+ attach,
5
+ ConnectedExtension,
6
+ getExtensionNameFromId,
7
+ registerExtension,
8
+ updateInternalExtensionStore,
9
+ } from "@openmrs/esm-extensions";
10
+ import {
11
+ getSyncLifecycle,
12
+ Extension,
13
+ ExtensionSlot,
14
+ openmrsComponentDecorator,
15
+ useExtensionSlotMeta,
16
+ ExtensionData,
17
+ } from ".";
18
+ import userEvent from "@testing-library/user-event";
19
+
20
+ // For some reason in the text context `isEqual` always returns true
21
+ // when using the import substitution in jest.config.js. Here's a custom
22
+ // mock.
23
+ jest.mock(
24
+ "lodash-es/isEqual",
25
+ () => (a, b) => JSON.stringify(a) == JSON.stringify(b)
26
+ );
27
+
28
+ describe("ExtensionSlot, Extension, and useExtensionSlotMeta", () => {
29
+ beforeEach(() => {
30
+ updateInternalExtensionStore(() => ({ slots: {}, extensions: {} }));
31
+ });
32
+
33
+ test("Extension receives state changes passed through (not using <Extension>)", async () => {
34
+ function EnglishExtension({ suffix }) {
35
+ return <div>English{suffix}</div>;
36
+ }
37
+ registerSimpleExtension("English", "esm-languages-app", EnglishExtension);
38
+ attach("Box", "English");
39
+ const App = openmrsComponentDecorator({
40
+ moduleName: "esm-languages-app",
41
+ featureName: "Languages",
42
+ disableTranslations: true,
43
+ })(() => {
44
+ const [suffix, toggleSuffix] = useReducer(
45
+ (suffix) => (suffix == "!" ? "?" : "!"),
46
+ "!"
47
+ );
48
+ return (
49
+ <div>
50
+ <ExtensionSlot name="Box" state={{ suffix }} />
51
+ <button onClick={toggleSuffix}>Toggle suffix</button>
52
+ </div>
53
+ );
54
+ });
55
+ render(<App />);
56
+
57
+ await waitFor(() =>
58
+ expect(screen.getByText(/English/)).toBeInTheDocument()
59
+ );
60
+ expect(screen.getByText(/English/)).toHaveTextContent("English!");
61
+ userEvent.click(screen.getByText("Toggle suffix"));
62
+ await waitFor(() =>
63
+ expect(screen.getByText(/English/)).toHaveTextContent("English?")
64
+ );
65
+ });
66
+
67
+ test("Extension receives state changes (using <Extension>)", async () => {
68
+ function HaitianCreoleExtension({ suffix }) {
69
+ return <div>Haitian Creole{suffix}</div>;
70
+ }
71
+ registerSimpleExtension(
72
+ "Haitian",
73
+ "esm-languages-app",
74
+ HaitianCreoleExtension
75
+ );
76
+ attach("Box", "Haitian");
77
+ const App = openmrsComponentDecorator({
78
+ moduleName: "esm-languages-app",
79
+ featureName: "Languages",
80
+ disableTranslations: true,
81
+ })(() => {
82
+ const [suffix, toggleSuffix] = useReducer(
83
+ (suffix) => (suffix == "!" ? "?" : "!"),
84
+ "!"
85
+ );
86
+
87
+ return (
88
+ <div>
89
+ <ExtensionSlot name="Box">
90
+ {suffix}
91
+ <Extension state={{ suffix }} />
92
+ </ExtensionSlot>
93
+ <button onClick={toggleSuffix}>Toggle suffix</button>
94
+ </div>
95
+ );
96
+ });
97
+ render(<App />);
98
+
99
+ await waitFor(() =>
100
+ expect(screen.getByText(/Haitian/)).toBeInTheDocument()
101
+ );
102
+ expect(screen.getByText(/Haitian/)).toHaveTextContent("Haitian Creole!");
103
+ userEvent.click(screen.getByText("Toggle suffix"));
104
+ await waitFor(() =>
105
+ expect(screen.getByText(/Haitian/)).toHaveTextContent("Haitian Creole?")
106
+ );
107
+ });
108
+
109
+ test("ExtensionSlot throws error if both state and children provided", () => {
110
+ const App = () => (
111
+ <ExtensionSlot name="Box" state={{ color: "red" }}>
112
+ <Extension />
113
+ </ExtensionSlot>
114
+ );
115
+ expect(() => render(<App />)).toThrowError(
116
+ expect.objectContaining({
117
+ message: expect.stringMatching(/children.*state/),
118
+ })
119
+ );
120
+ });
121
+
122
+ test("Extension Slot receives meta", async () => {
123
+ registerSimpleExtension("Spanish", "esm-languages-app", undefined, {
124
+ code: "es",
125
+ });
126
+ attach("Box", "Spanish");
127
+ const App = openmrsComponentDecorator({
128
+ moduleName: "esm-languages-app",
129
+ featureName: "Languages",
130
+ disableTranslations: true,
131
+ })(() => {
132
+ const metas = useExtensionSlotMeta("Box");
133
+ const wrapItem = useCallback(
134
+ (slot: React.ReactNode, extension: ExtensionData) => {
135
+ return (
136
+ <div>
137
+ <h1>
138
+ {metas[getExtensionNameFromId(extension.extensionId)].code}
139
+ </h1>
140
+ {slot}
141
+ </div>
142
+ );
143
+ },
144
+ [metas]
145
+ );
146
+ return (
147
+ <div>
148
+ <ExtensionSlot name="Box">
149
+ <Extension wrap={wrapItem} />
150
+ </ExtensionSlot>
151
+ </div>
152
+ );
153
+ });
154
+ render(<App />);
155
+
156
+ await waitFor(() =>
157
+ expect(screen.getByRole("heading")).toBeInTheDocument()
158
+ );
159
+ expect(screen.getByRole("heading")).toHaveTextContent("es");
160
+ await waitFor(() =>
161
+ expect(screen.getByText("Spanish")).toBeInTheDocument()
162
+ );
163
+ });
164
+
165
+ test("Both meta and state can be used at the same time", async () => {
166
+ function SwahiliExtension({ suffix }) {
167
+ return <div>Swahili{suffix}</div>;
168
+ }
169
+ registerSimpleExtension("Swahili", "esm-languages-app", SwahiliExtension, {
170
+ code: "sw",
171
+ });
172
+ attach("Box", "Swahili");
173
+ const App = openmrsComponentDecorator({
174
+ moduleName: "esm-languages-app",
175
+ featureName: "Languages",
176
+ disableTranslations: true,
177
+ })(() => {
178
+ const [suffix, toggleSuffix] = useReducer(
179
+ (suffix) => (suffix == "!" ? "?" : "!"),
180
+ "!"
181
+ );
182
+ const metas = useExtensionSlotMeta("Box");
183
+ const wrapItem = useCallback(
184
+ (slot: React.ReactNode, extension: ExtensionData) => {
185
+ return (
186
+ <div>
187
+ <h1>
188
+ {metas[getExtensionNameFromId(extension.extensionId)].code}
189
+ </h1>
190
+ {slot}
191
+ </div>
192
+ );
193
+ },
194
+ [metas]
195
+ );
196
+ return (
197
+ <div>
198
+ <ExtensionSlot name="Box">
199
+ <Extension wrap={wrapItem} state={{ suffix }} />
200
+ </ExtensionSlot>
201
+ <button onClick={toggleSuffix}>Toggle suffix</button>
202
+ </div>
203
+ );
204
+ });
205
+ render(<App />);
206
+
207
+ await waitFor(() =>
208
+ expect(screen.getByRole("heading")).toBeInTheDocument()
209
+ );
210
+ expect(screen.getByRole("heading")).toHaveTextContent("sw");
211
+ await waitFor(() =>
212
+ expect(screen.getByText(/Swahili/)).toHaveTextContent("Swahili!")
213
+ );
214
+ userEvent.click(screen.getByText("Toggle suffix"));
215
+ await waitFor(() =>
216
+ expect(screen.getByText(/Swahili/)).toHaveTextContent("Swahili?")
217
+ );
218
+ });
219
+
220
+ test("Extension Slot renders function children", async () => {
221
+ registerSimpleExtension("Urdu", "esm-languages-app", undefined, {
222
+ code: "urd",
223
+ });
224
+ registerSimpleExtension("Hindi", "esm-languages-app", undefined, {
225
+ code: "hi",
226
+ });
227
+ attach("Box", "Urdu");
228
+ attach("Box", "Hindi");
229
+ const App = openmrsComponentDecorator({
230
+ moduleName: "esm-languages-app",
231
+ featureName: "Languages",
232
+ disableTranslations: true,
233
+ })(() => {
234
+ return (
235
+ <div>
236
+ <ExtensionSlot name="Box">
237
+ {(extension: ConnectedExtension) => (
238
+ <div data-testid={extension.name}>
239
+ <h2>{extension.meta.code}</h2>
240
+ <Extension />
241
+ </div>
242
+ )}
243
+ </ExtensionSlot>
244
+ </div>
245
+ );
246
+ });
247
+ render(<App />);
248
+
249
+ await waitFor(() => expect(screen.getByTestId("Urdu")).toBeInTheDocument());
250
+ expect(
251
+ within(screen.getByTestId("Urdu")).getByRole("heading")
252
+ ).toHaveTextContent("urd");
253
+ expect(
254
+ within(screen.getByTestId("Hindi")).getByRole("heading")
255
+ ).toHaveTextContent("hi");
256
+ });
257
+ });
258
+
259
+ function registerSimpleExtension(
260
+ name: string,
261
+ moduleName: string,
262
+ Component?: React.ComponentType<any>,
263
+ meta: object = {}
264
+ ) {
265
+ const SimpleComponent = () => <div>{name}</div>;
266
+ registerExtension({
267
+ name,
268
+ moduleName,
269
+ load: getSyncLifecycle(Component ?? SimpleComponent, {
270
+ moduleName,
271
+ featureName: moduleName,
272
+ disableTranslations: true,
273
+ }),
274
+ meta,
275
+ });
276
+ }
package/src/public.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { ExtensionData } from "./ComponentContext";
1
+ export { type ExtensionData } from "./ComponentContext";
2
2
  export * from "./ConfigurableLink";
3
3
  export * from "./createUseStore";
4
4
  export * from "./Extension";
@@ -12,7 +12,6 @@ export * from "./useConnectedExtensions";
12
12
  export * from "./useConnectivity";
13
13
  export * from "./usePatient";
14
14
  export * from "./useCurrentPatient";
15
- export * from "./useExtensionSlot";
16
15
  export * from "./useExtensionSlotMeta";
17
16
  export * from "./useExtensionStore";
18
17
  export * from "./useLayoutType";
@@ -1,3 +1,5 @@
1
+ import "@testing-library/jest-dom/extend-expect";
2
+
1
3
  window.System = {
2
4
  import: (name) => import(name),
3
5
  resolve: jest.fn().mockImplementation(() => {
@@ -1,7 +1,7 @@
1
1
  /** @module @category Extension */
2
2
  import { useEffect, useState } from "react";
3
3
  import { getExtensionStore } from "@openmrs/esm-extensions";
4
- import { isEqual } from "lodash";
4
+ import isEqual from "lodash-es/isEqual";
5
5
 
6
6
  /**
7
7
  * Gets the assigned extension ids for a given extension slot name.
@@ -5,7 +5,7 @@ import {
5
5
  ExtensionStore,
6
6
  getExtensionStore,
7
7
  } from "@openmrs/esm-extensions";
8
- import { isEqual } from "lodash";
8
+ import isEqual from "lodash/isEqual";
9
9
 
10
10
  /**
11
11
  * Gets the assigned extensions for a given extension slot name.
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { render, cleanup, screen, waitFor } from "@testing-library/react";
2
+ import { render, cleanup, screen, waitFor, act } from "@testing-library/react";
3
3
  import {
4
4
  defineConfigSchema,
5
5
  temporaryConfigStore,
@@ -26,7 +26,6 @@ function clearConfig() {
26
26
 
27
27
  describe(`useConfig in root context`, () => {
28
28
  afterEach(clearConfig);
29
- afterEach(cleanup);
30
29
 
31
30
  it(`can return config as a react hook`, async () => {
32
31
  defineConfigSchema("foo-module", {
@@ -103,9 +102,11 @@ describe(`useConfig in root context`, () => {
103
102
  expect(screen.findByText("The first thing")).toBeTruthy()
104
103
  );
105
104
 
106
- temporaryConfigStore.setState({
107
- config: { "foo-module": { thing: "A new thing" } },
108
- });
105
+ act(() =>
106
+ temporaryConfigStore.setState({
107
+ config: { "foo-module": { thing: "A new thing" } },
108
+ })
109
+ );
109
110
 
110
111
  await waitFor(() => expect(screen.findByText("A new thing")).toBeTruthy());
111
112
  });
@@ -273,7 +274,7 @@ describe(`useConfig in an extension`, () => {
273
274
  );
274
275
 
275
276
  const newConfig = { "ext-module": { thing: "A new thing" } };
276
- temporaryConfigStore.setState({ config: newConfig });
277
+ act(() => temporaryConfigStore.setState({ config: newConfig }));
277
278
 
278
279
  await waitFor(() => expect(screen.findByText("A new thing")).toBeTruthy());
279
280
 
@@ -290,7 +291,7 @@ describe(`useConfig in an extension`, () => {
290
291
  },
291
292
  },
292
293
  };
293
- temporaryConfigStore.setState({ config: newConfig2 });
294
+ act(() => temporaryConfigStore.setState({ config: newConfig2 }));
294
295
 
295
296
  await waitFor(() =>
296
297
  expect(screen.findByText("Yet another thing")).toBeTruthy()
package/src/useConfig.ts CHANGED
@@ -4,11 +4,11 @@ import {
4
4
  getConfigStore,
5
5
  getExtensionsConfigStore,
6
6
  ConfigStore,
7
+ ConfigObject,
7
8
  ExtensionsConfigStore,
8
9
  getExtensionConfigFromStore,
9
10
  } from "@openmrs/esm-config";
10
11
  import { ComponentContext, ExtensionData } from "./ComponentContext";
11
- import { ConfigObject } from "@openmrs/esm-config";
12
12
  import { Store } from "unistore";
13
13
  import isEqual from "lodash-es/isEqual";
14
14
 
@@ -98,11 +98,11 @@ export function useSession(): Session {
98
98
  if (!result) {
99
99
  if (promise) {
100
100
  console.warn(
101
- "useSessionUser is in an unexpected state. Attempting to recover."
101
+ "useSession is in an unexpected state. Attempting to recover."
102
102
  );
103
103
  throw promise;
104
104
  } else {
105
- throw Error("useSessionUser is in an invalid state.");
105
+ throw Error("useSession is in an invalid state.");
106
106
  }
107
107
  }
108
108
  return result;
package/src/useVisit.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  import useSWR from "swr";
8
8
  import dayjs from "dayjs";
9
9
  import isToday from "dayjs/plugin/isToday";
10
+ import { useMemo } from "react";
10
11
 
11
12
  dayjs.extend(isToday);
12
13
 
@@ -15,6 +16,7 @@ interface VisitReturnType {
15
16
  mutate: () => void;
16
17
  isValidating: boolean;
17
18
  currentVisit: Visit | null;
19
+ isLoading: boolean;
18
20
  }
19
21
 
20
22
  /**
@@ -28,15 +30,26 @@ export function useVisit(patientUuid: string): VisitReturnType {
28
30
  const { data, error, mutate, isValidating } = useSWR<{
29
31
  data: { results: Array<Visit> };
30
32
  }>(
31
- `/ws/rest/v1/visit?patient=${patientUuid}&v=${defaultVisitCustomRepresentation}&includeInactive=false`,
33
+ patientUuid
34
+ ? `/ws/rest/v1/visit?patient=${patientUuid}&v=${defaultVisitCustomRepresentation}&includeInactive=false`
35
+ : null,
32
36
  openmrsFetch
33
37
  );
34
38
 
35
- const currentVisit =
36
- data?.data.results.find(
37
- (visit) =>
38
- visit.stopDatetime === null && dayjs(visit.startDatetime).isToday()
39
- ) ?? null;
39
+ const currentVisit = useMemo(
40
+ () =>
41
+ data?.data.results.find(
42
+ (visit) =>
43
+ visit.stopDatetime === null && dayjs(visit.startDatetime).isToday()
44
+ ) ?? null,
45
+ [data?.data.results]
46
+ );
40
47
 
41
- return { error, mutate, isValidating, currentVisit };
48
+ return {
49
+ error,
50
+ mutate,
51
+ isValidating,
52
+ currentVisit,
53
+ isLoading: !data && !error,
54
+ };
42
55
  }
@@ -1,2 +0,0 @@
1
- @openmrs/esm-react-utils:lint: cache hit, replaying output b4ee45d65c7c5ccf
2
- @openmrs/esm-react-utils:lint: $ eslint src
@@ -1,45 +0,0 @@
1
- @openmrs/esm-react-utils:test: cache hit, replaying output c183110feface1ab
2
- @openmrs/esm-react-utils:test: $ jest --passWithNoTests
3
- @openmrs/esm-react-utils:test: PASS src/useOnClickOutside.test.tsx
4
- @openmrs/esm-react-utils:test: PASS src/useSession.test.tsx
5
- @openmrs/esm-react-utils:test: PASS src/openmrsComponentDecorator.test.tsx (5.18 s)
6
- @openmrs/esm-react-utils:test: PASS src/useConfig.test.tsx (6 s)
7
- @openmrs/esm-react-utils:test:  ● Console
8
- @openmrs/esm-react-utils:test: 
9
- @openmrs/esm-react-utils:test:  console.error
10
- @openmrs/esm-react-utils:test:  Warning: An update to RenderConfig inside a test was not wrapped in act(...).
11
- @openmrs/esm-react-utils:test: 
12
- @openmrs/esm-react-utils:test:  When testing, code that causes React state updates should be wrapped into act(...):
13
- @openmrs/esm-react-utils:test: 
14
- @openmrs/esm-react-utils:test:  act(() => {
15
- @openmrs/esm-react-utils:test:  /* fire events that update state */
16
- @openmrs/esm-react-utils:test:  });
17
- @openmrs/esm-react-utils:test:  /* assert on the output */
18
- @openmrs/esm-react-utils:test: 
19
- @openmrs/esm-react-utils:test:  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
20
- @openmrs/esm-react-utils:test:  at RenderConfig (/home/brandon/Code/pih/mf/openmrs-esm-core/packages/framework/esm-react-utils/src/useConfig.test.tsx:18:29)
21
- @openmrs/esm-react-utils:test:  at Suspense
22
- @openmrs/esm-react-utils:test: 
23
- @openmrs/esm-react-utils:test:  85 | return store?.subscribe((state) => {
24
- @openmrs/esm-react-utils:test:  86 | if (state.loaded && state.config) {
25
- @openmrs/esm-react-utils:test:  > 87 | setState(state.config);
26
- @openmrs/esm-react-utils:test:  | ^
27
- @openmrs/esm-react-utils:test:  88 | }
28
- @openmrs/esm-react-utils:test:  89 | });
29
- @openmrs/esm-react-utils:test:  90 | }, [store]);
30
- @openmrs/esm-react-utils:test: 
31
- @openmrs/esm-react-utils:test:  at printWarning (../../../node_modules/react-dom/cjs/react-dom.development.js:86:30)
32
- @openmrs/esm-react-utils:test:  at error (../../../node_modules/react-dom/cjs/react-dom.development.js:60:7)
33
- @openmrs/esm-react-utils:test:  at warnIfUpdatesNotWrappedWithActDEV (../../../node_modules/react-dom/cjs/react-dom.development.js:27543:9)
34
- @openmrs/esm-react-utils:test:  at scheduleUpdateOnFiber (../../../node_modules/react-dom/cjs/react-dom.development.js:25404:5)
35
- @openmrs/esm-react-utils:test:  at dispatchSetState (../../../node_modules/react-dom/cjs/react-dom.development.js:17389:16)
36
- @openmrs/esm-react-utils:test:  at Array.setState (src/useConfig.ts:87:9)
37
- @openmrs/esm-react-utils:test:  at Object.e (../../../node_modules/unistore/src/index.js:16:19)
38
- @openmrs/esm-react-utils:test: 
39
- @openmrs/esm-react-utils:test: PASS src/ConfigurableLink.test.tsx (9.494 s)
40
- @openmrs/esm-react-utils:test: 
41
- @openmrs/esm-react-utils:test: Test Suites: 5 passed, 5 total
42
- @openmrs/esm-react-utils:test: Tests: 19 passed, 19 total
43
- @openmrs/esm-react-utils:test: Snapshots: 0 total
44
- @openmrs/esm-react-utils:test: Time: 13.693 s
45
- @openmrs/esm-react-utils:test: Ran all test suites.