@thepalaceproject/circulation-admin 1.21.0-post.6 → 1.22.0-post.1

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/package.json CHANGED
@@ -149,5 +149,5 @@
149
149
  "*.{js,jsx,ts,tsx,css,md}": "prettier --write",
150
150
  "*.{js,css,md}": "prettier --write"
151
151
  },
152
- "version": "1.21.0-post.6"
152
+ "version": "1.22.0-post.1"
153
153
  }
@@ -1,6 +1,7 @@
1
1
  import { StatisticsData } from "../../src/interfaces";
2
2
 
3
3
  export const testLibraryKey = "lyrasis-reads";
4
+ export const testLibraryName = "LYRASIS Reads";
4
5
  export const noCollectionsLibraryKey = "unfunded-library";
5
6
  export const noInventoryLibraryKey = "unfunded-library";
6
7
  export const noPatronsLibraryKey = "unused-library";
@@ -3,24 +3,245 @@ import { render } from "@testing-library/react";
3
3
  import LibraryStats, {
4
4
  CustomTooltip,
5
5
  } from "../../../src/components/LibraryStats";
6
- import { renderWithProviders } from "../testUtils/withProviders";
6
+ import {
7
+ componentWithProviders,
8
+ renderWithProviders,
9
+ } from "../testUtils/withProviders";
7
10
  import { ContextProviderProps } from "../../../src/components/ContextProvider";
8
11
 
9
12
  import {
10
13
  statisticsApiResponseData,
11
14
  testLibraryKey as sampleLibraryKey,
15
+ testLibraryName as sampleLibraryName,
12
16
  } from "../../__data__/statisticsApiResponseData";
13
- import { normalizeStatistics } from "../../../src/components/Stats";
17
+
18
+ import { normalizeStatistics } from "../../../src/features/stats/normalizeStatistics";
19
+ import { useGetStatsQuery } from "../../../src/features/stats/statsSlice";
20
+ import * as fetchMock from "fetch-mock-jest";
21
+ import { STATS_API_ENDPOINT } from "../../../src/features/stats/statsSlice";
22
+ import Stats from "../../../src/components/Stats";
23
+ import { renderHook } from "@testing-library/react-hooks";
24
+ import { FetchErrorData } from "@thepalaceproject/web-opds-client/lib/interfaces";
25
+ import { store } from "../../../src/store";
26
+ import { api } from "../../../src/features/api/apiSlice";
27
+
28
+ const normalizedData = normalizeStatistics(statisticsApiResponseData);
14
29
 
15
30
  describe("Dashboard Statistics", () => {
16
31
  // NB: This adds test to the already existing tests in:
17
- // - `src/components/__tests__/Stats-test.tsx`.
18
32
  // - `src/components/__tests__/LibraryStats-test.tsx`.
19
33
  // - `src/components/__tests__/SingleStatListItem-test.tsx`.
20
34
  //
21
35
  // Those tests should eventually be migrated here and
22
36
  // adapted to the Jest/React Testing Library paradigm.
23
37
 
38
+ // Configure standard constructors so that RTK Query works in tests with FetchMockJest
39
+ Object.assign(fetchMock.config, {
40
+ fetch,
41
+ Headers,
42
+ Request,
43
+ Response,
44
+ });
45
+
46
+ describe("query hook correctly handles fetch responses", () => {
47
+ const wrapper = componentWithProviders();
48
+
49
+ beforeEach(() => {
50
+ store.dispatch(api.util.resetApiState());
51
+ fetchMock.restore();
52
+ });
53
+ afterAll(() => {
54
+ store.dispatch(api.util.resetApiState());
55
+ fetchMock.restore();
56
+ });
57
+
58
+ it("returns data when fetch successful", async () => {
59
+ fetchMock.get(
60
+ `path:${STATS_API_ENDPOINT}`,
61
+ {
62
+ body: JSON.stringify(statisticsApiResponseData),
63
+ status: 200,
64
+ },
65
+ { overwriteRoutes: true }
66
+ );
67
+
68
+ const { result, waitFor } = renderHook(() => useGetStatsQuery(), {
69
+ wrapper,
70
+ });
71
+
72
+ // Expect loading status immediately after first use of the hook.
73
+ let { isSuccess, isError, error, data } = result.current;
74
+ expect(isSuccess).toBe(false);
75
+ expect(isError).toBe(false);
76
+ expect(error).toBe(undefined);
77
+ expect(data).toEqual(undefined);
78
+
79
+ // Once loaded, we should have our data.
80
+ await waitFor(() => !result.current.isLoading);
81
+ ({ isSuccess, isError, error, data } = result.current);
82
+
83
+ expect(isSuccess).toBe(true);
84
+ expect(isError).toBe(false);
85
+ expect(error).toBe(undefined);
86
+ expect(data).toEqual(normalizedData);
87
+
88
+ // But if we use the hook again, we should get the data back from
89
+ // the cache immediately, without loading state.
90
+ const { result: result2 } = renderHook(() => useGetStatsQuery(), {
91
+ wrapper,
92
+ });
93
+ ({ isSuccess, isError, error, data } = result2.current);
94
+
95
+ expect(isSuccess).toBe(true);
96
+ expect(isError).toBe(false);
97
+ expect(error).toBe(undefined);
98
+ expect(data).toEqual(normalizedData);
99
+ });
100
+
101
+ it("returns error and no data when request fails", async () => {
102
+ fetchMock.get(
103
+ `path:${STATS_API_ENDPOINT}`,
104
+ {
105
+ status: 500,
106
+ },
107
+ {
108
+ overwriteRoutes: true,
109
+ }
110
+ );
111
+
112
+ const { result, waitFor } = renderHook(() => useGetStatsQuery(), {
113
+ wrapper,
114
+ });
115
+
116
+ // Expect loading status immediately after first use of the hook.
117
+ let { isSuccess, isError, error, data } = result.current;
118
+ expect(isSuccess).toBe(false);
119
+ expect(isError).toBe(false);
120
+ expect(error).toBe(undefined);
121
+ expect(data).toEqual(undefined);
122
+
123
+ await waitFor(() => !result.current.isLoading);
124
+ ({ isSuccess, isError, error, data } = result.current);
125
+
126
+ expect(isSuccess).toBe(false);
127
+ expect(isError).toBe(true);
128
+ expect(data).toBe(undefined);
129
+ expect((error as FetchErrorData).status).toBe(500);
130
+
131
+ // But if we use the hook again, we should get our error back from
132
+ // the cache immediately, without loading state.
133
+ const { result: result2 } = renderHook(() => useGetStatsQuery(), {
134
+ wrapper,
135
+ });
136
+ ({ isSuccess, isError, error, data } = result2.current);
137
+
138
+ expect(isSuccess).toBe(false);
139
+ expect(isError).toBe(true);
140
+ expect(data).toBe(undefined);
141
+ expect((error as FetchErrorData).status).toBe(500);
142
+ });
143
+ });
144
+
145
+ describe("rendering", () => {
146
+ beforeAll(() => {
147
+ fetchMock.get(`path:${STATS_API_ENDPOINT}`, {
148
+ body: JSON.stringify(statisticsApiResponseData),
149
+ status: 200,
150
+ });
151
+ });
152
+ afterAll(() => {
153
+ fetchMock.restore();
154
+ });
155
+ afterEach(() => {
156
+ fetchMock.resetHistory();
157
+ });
158
+
159
+ const assertLoadingState = ({ getByRole }) => {
160
+ getByRole("dialog", { name: "Loading" });
161
+ getByRole("heading", { level: 1, name: "Loading" });
162
+ };
163
+ const assertNotLoadingState = ({ queryByRole }) => {
164
+ const missingLoadingDialog = queryByRole("dialog", { name: "Loading" });
165
+ const missingLoadingHeading = queryByRole("heading", {
166
+ level: 1,
167
+ name: "Loading",
168
+ });
169
+ expect(missingLoadingDialog).not.toBeInTheDocument();
170
+ expect(missingLoadingHeading).not.toBeInTheDocument();
171
+ };
172
+
173
+ it("shows/hides the loading indicator", async () => {
174
+ // We haven't tried to fetch anything yet.
175
+ expect(fetchMock.calls()).toHaveLength(0);
176
+
177
+ const { rerender, getByRole, queryByRole } = renderWithProviders(
178
+ <Stats />
179
+ );
180
+
181
+ // We should start in the loading state.
182
+ assertLoadingState({ getByRole });
183
+
184
+ // Wait a tick for the statistics to render.
185
+ await new Promise(process.nextTick);
186
+ // Now we've fetched something.
187
+ expect(fetchMock.calls()).toHaveLength(1);
188
+
189
+ rerender(<Stats />);
190
+
191
+ // We should show our content without the loading state.
192
+ assertNotLoadingState({ queryByRole });
193
+ getByRole("heading", { level: 2, name: "Statistics for All Libraries" });
194
+
195
+ // We haven't made another call, since the response is cached.
196
+ expect(fetchMock.calls()).toHaveLength(1);
197
+ });
198
+
199
+ it("doesn't fetch again, because response is cached", async () => {
200
+ const { getByRole, queryByRole } = renderWithProviders(<Stats />);
201
+
202
+ // We should show our content immediately, without entering the loading state.
203
+ assertNotLoadingState({ queryByRole });
204
+ getByRole("heading", { level: 2, name: "Statistics for All Libraries" });
205
+
206
+ // We never tried to fetch anything because the result is cached.
207
+ expect(fetchMock.calls()).toHaveLength(0);
208
+ });
209
+
210
+ it("show stats for a library, if a library is specified", async () => {
211
+ const { getByRole, queryByRole, getByText } = renderWithProviders(
212
+ <Stats library={sampleLibraryKey} />
213
+ );
214
+
215
+ // We should show our content immediately, without entering the loading state.
216
+ assertNotLoadingState({ queryByRole });
217
+ getByRole("heading", {
218
+ level: 2,
219
+ name: `${sampleLibraryName} Statistics`,
220
+ });
221
+ getByRole("heading", { level: 3, name: "Patrons" });
222
+ getByText("132");
223
+
224
+ // We never tried to fetch anything because the result is cached.
225
+ expect(fetchMock.calls()).toHaveLength(0);
226
+ });
227
+
228
+ it("shows site-wide stats when no library specified", async () => {
229
+ const { getByRole, getByText, queryByRole } = renderWithProviders(
230
+ <Stats />
231
+ );
232
+
233
+ // We should show our content immediately, without entering the loading state.
234
+ assertNotLoadingState({ queryByRole });
235
+
236
+ getByRole("heading", { level: 2, name: "Statistics for All Libraries" });
237
+ getByRole("heading", { level: 3, name: "Patrons" });
238
+ getByText("145");
239
+
240
+ // We never tried to fetch anything because the result is cached.
241
+ expect(fetchMock.calls()).toHaveLength(0);
242
+ });
243
+ });
244
+
24
245
  describe("requesting inventory reports", () => {
25
246
  // Convert from the API format to our in-app format.
26
247
  const statisticsData = normalizeStatistics(statisticsApiResponseData);
@@ -212,9 +433,7 @@ describe("Dashboard Statistics", () => {
212
433
  payload: defaultPayload,
213
434
  });
214
435
 
215
- const { container, getByRole } = render(
216
- <CustomTooltip {...tooltipProps} />
217
- );
436
+ const { container } = render(<CustomTooltip {...tooltipProps} />);
218
437
  const tooltipContent = container.querySelectorAll(".customTooltip");
219
438
 
220
439
  expect(tooltipContent).toHaveLength(0);
@@ -47,7 +47,7 @@ export const componentWithProviders = ({
47
47
  featureFlags: defaultFeatureFlags,
48
48
  },
49
49
  queryClient = new QueryClient(),
50
- }: TestProviderWrapperOptions): React.FunctionComponent => {
50
+ }: TestProviderWrapperOptions = {}): React.FunctionComponent => {
51
51
  const effectiveContextProviderProps = {
52
52
  ...defaultContextProviderProps,
53
53
  ...contextProviderProps,