@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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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,
|