@thepalaceproject/circulation-admin 1.22.0 → 1.23.0
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/dist/20fd1704ea223900efa9.woff2 +0 -0
- package/dist/4692b9ec53fd5972caa2.ttf +0 -0
- package/dist/5be1347c682810f199c7.eot +0 -0
- package/dist/82b1212e45a2bc35dd73.woff +0 -0
- package/dist/be810be3a3e14c682a25.woff2 +0 -0
- package/dist/circulation-admin.css +4 -4
- package/dist/circulation-admin.js +1 -1
- package/dist/f691f37e57f04c152e23.woff +0 -0
- package/package.json +3 -2
- package/tests/__data__/statisticsApiResponseData.ts +1 -0
- package/tests/jest/businessRules/roleBasedAccess.test.ts +158 -0
- package/tests/jest/components/Stats.test.tsx +609 -243
- package/tests/jest/context/AppContext.test.tsx +72 -0
- package/tests/jest/testUtils/withProviders.tsx +3 -3
- package/webpack.common.js +9 -5
- package/dist/45eb1d236a736caa24dd.woff2 +0 -1
- package/dist/4c6f1cd9993ba8a53b8e.ttf +0 -1
- package/dist/5e9505a87e4d8ecb2017.eot +0 -1
- package/dist/7a065a1c0cb2d586cecb.woff +0 -1
- package/dist/7d6ec71e2466a9fd777f.woff2 +0 -1
- package/dist/dec4ea00820558e24672.ttf +0 -1
- package/dist/e5c0c62d732823225aaa.eot +0 -1
- package/dist/f34ea237f268661e9d00.woff +0 -1
- /package/dist/{1e59d2330b4c6deb84b340635ed36249.ttf → 1e59d2330b4c6deb84b3.ttf} +0 -0
- /package/dist/{8b43027f47b20503057dfbbaa9401fef.eot → 8b43027f47b20503057d.eot} +0 -0
|
@@ -1,291 +1,657 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { render } from "@testing-library/react";
|
|
3
|
-
import LibraryStats
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { ALL_LIBRARIES_HEADING } from "../../../src/components/LibraryStats";
|
|
4
|
+
import { CustomTooltip } from "../../../src/components/StatsCollectionsBarChart";
|
|
5
|
+
import {
|
|
6
|
+
componentWithProviders,
|
|
7
|
+
renderWithProviders,
|
|
8
|
+
} from "../testUtils/withProviders";
|
|
7
9
|
import { ContextProviderProps } from "../../../src/components/ContextProvider";
|
|
8
10
|
|
|
9
11
|
import {
|
|
10
12
|
statisticsApiResponseData,
|
|
11
13
|
testLibraryKey as sampleLibraryKey,
|
|
14
|
+
testLibraryName as sampleLibraryName,
|
|
12
15
|
} from "../../__data__/statisticsApiResponseData";
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
import { normalizeStatistics } from "../../../src/features/stats/normalizeStatistics";
|
|
18
|
+
import { useGetStatsQuery } from "../../../src/features/stats/statsSlice";
|
|
19
|
+
import * as fetchMock from "fetch-mock-jest";
|
|
20
|
+
import { STATS_API_ENDPOINT } from "../../../src/features/stats/statsSlice";
|
|
21
|
+
import Stats from "../../../src/components/Stats";
|
|
22
|
+
import { renderHook } from "@testing-library/react-hooks";
|
|
23
|
+
import { FetchErrorData } from "@thepalaceproject/web-opds-client/lib/interfaces";
|
|
24
|
+
import { store } from "../../../src/store";
|
|
25
|
+
import { api } from "../../../src/features/api/apiSlice";
|
|
26
|
+
|
|
27
|
+
const normalizedData = normalizeStatistics(statisticsApiResponseData);
|
|
28
|
+
|
|
29
|
+
global.ResizeObserver = require("resize-observer-polyfill");
|
|
14
30
|
|
|
15
31
|
describe("Dashboard Statistics", () => {
|
|
16
32
|
// NB: This adds test to the already existing tests in:
|
|
17
|
-
// - `src/components/__tests__/Stats-test.tsx`.
|
|
18
33
|
// - `src/components/__tests__/LibraryStats-test.tsx`.
|
|
19
34
|
// - `src/components/__tests__/SingleStatListItem-test.tsx`.
|
|
20
35
|
//
|
|
21
36
|
// Those tests should eventually be migrated here and
|
|
22
37
|
// adapted to the Jest/React Testing Library paradigm.
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const sampleStatsData = librariesStatsTestDataByKey[sampleLibraryKey];
|
|
32
|
-
|
|
33
|
-
const systemAdmin = [{ role: "system" }];
|
|
34
|
-
const managerAll = [{ role: "manager-all" }];
|
|
35
|
-
const librarianAll = [{ role: "librarian-all" }];
|
|
36
|
-
|
|
37
|
-
const baseContextProviderProps = {
|
|
38
|
-
csrfToken: "",
|
|
39
|
-
featureFlags: { reportsOnlyForSysadmins: false },
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const renderFor = (
|
|
43
|
-
onlySysadmins: boolean,
|
|
44
|
-
roles: { role: string; library?: string }[]
|
|
45
|
-
) => {
|
|
46
|
-
const contextProviderProps: ContextProviderProps = {
|
|
47
|
-
...baseContextProviderProps,
|
|
48
|
-
featureFlags: { reportsOnlyForSysadmins: onlySysadmins },
|
|
49
|
-
roles,
|
|
50
|
-
};
|
|
39
|
+
// Configure standard constructors so that RTK Query works in tests with FetchMockJest
|
|
40
|
+
Object.assign(fetchMock.config, {
|
|
41
|
+
fetch,
|
|
42
|
+
Headers,
|
|
43
|
+
Request,
|
|
44
|
+
Response,
|
|
45
|
+
});
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
const statGroupToHeading = {
|
|
48
|
+
patrons: "Current Circulation Activity",
|
|
49
|
+
circulations: "Circulation Totals",
|
|
50
|
+
inventory: "Inventory",
|
|
51
|
+
usageReports: "Usage and Reports",
|
|
52
|
+
collections: "Configured Collections",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe("query hook correctly handles fetch responses", () => {
|
|
56
|
+
const wrapper = componentWithProviders();
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// If the feature flag is set, the button should be visible only to sysadmins.
|
|
65
|
-
expect(renderFor(true, systemAdmin)).not.toBeNull();
|
|
66
|
-
expect(renderFor(true, managerAll)).toBeNull();
|
|
67
|
-
expect(renderFor(true, librarianAll)).toBeNull();
|
|
68
|
-
// If the feature flag is false, the button should be visible to all users.
|
|
69
|
-
expect(renderFor(false, systemAdmin)).not.toBeNull();
|
|
70
|
-
expect(renderFor(false, managerAll)).not.toBeNull();
|
|
71
|
-
expect(renderFor(false, librarianAll)).not.toBeNull();
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
store.dispatch(api.util.resetApiState());
|
|
60
|
+
fetchMock.restore();
|
|
61
|
+
});
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
store.dispatch(api.util.resetApiState());
|
|
64
|
+
fetchMock.restore();
|
|
72
65
|
});
|
|
73
|
-
});
|
|
74
66
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
openAccessTitles: 0,
|
|
84
|
-
titles: 7974,
|
|
85
|
-
unlimitedLicenseTitles: 0,
|
|
86
|
-
};
|
|
87
|
-
const perMediumInventory = {
|
|
88
|
-
Audio: {
|
|
89
|
-
availableTitles: 148,
|
|
90
|
-
licensedTitles: 165,
|
|
91
|
-
meteredLicenseTitles: 165,
|
|
92
|
-
meteredLicensesAvailable: 221,
|
|
93
|
-
meteredLicensesOwned: 392,
|
|
94
|
-
openAccessTitles: 0,
|
|
95
|
-
titles: 165,
|
|
96
|
-
unlimitedLicenseTitles: 0,
|
|
97
|
-
},
|
|
98
|
-
Book: {
|
|
99
|
-
availableTitles: 7805,
|
|
100
|
-
licensedTitles: 7809,
|
|
101
|
-
meteredLicenseTitles: 7809,
|
|
102
|
-
meteredLicensesAvailable: 75225,
|
|
103
|
-
meteredLicensesOwned: 301149,
|
|
104
|
-
openAccessTitles: 0,
|
|
105
|
-
titles: 7809,
|
|
106
|
-
unlimitedLicenseTitles: 0,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
const defaultChartItemWithoutPerMediumInventory = {
|
|
110
|
-
name: defaultLabel,
|
|
111
|
-
...summaryInventory,
|
|
112
|
-
};
|
|
113
|
-
const defaultChartItemWithPerMediumInventory = {
|
|
114
|
-
...defaultChartItemWithoutPerMediumInventory,
|
|
115
|
-
_by_medium: perMediumInventory,
|
|
116
|
-
};
|
|
117
|
-
const defaultPayload = [
|
|
118
|
-
{
|
|
119
|
-
fill: "#606060",
|
|
120
|
-
dataKey: "meteredLicenseTitles",
|
|
121
|
-
name: "Metered License Titles",
|
|
122
|
-
color: "#606060",
|
|
123
|
-
value: 7974,
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
fill: "#404040",
|
|
127
|
-
dataKey: "unlimitedLicenseTitles",
|
|
128
|
-
name: "Unlimited License Titles",
|
|
129
|
-
color: "#404040",
|
|
130
|
-
value: 0,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
fill: "#202020",
|
|
134
|
-
dataKey: "openAccessTitles",
|
|
135
|
-
name: "Open Access Titles",
|
|
136
|
-
color: "#202020",
|
|
137
|
-
value: 0,
|
|
138
|
-
},
|
|
139
|
-
];
|
|
140
|
-
|
|
141
|
-
const populateTooltipProps = ({
|
|
142
|
-
active = true,
|
|
143
|
-
label = defaultLabel,
|
|
144
|
-
payload = [],
|
|
145
|
-
chartItem = undefined,
|
|
146
|
-
}) => {
|
|
147
|
-
const constructedChartItem = !chartItem
|
|
148
|
-
? chartItem
|
|
149
|
-
: {
|
|
150
|
-
...chartItem,
|
|
151
|
-
name: label,
|
|
152
|
-
};
|
|
153
|
-
const constructedPayload = payload.map((entry) => ({
|
|
154
|
-
...entry,
|
|
155
|
-
payload: constructedChartItem,
|
|
156
|
-
}));
|
|
157
|
-
return {
|
|
158
|
-
active,
|
|
159
|
-
label,
|
|
160
|
-
payload: constructedPayload,
|
|
161
|
-
};
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Helper function to test passing tests for a tooltip
|
|
166
|
-
*
|
|
167
|
-
* @param tooltipProps - passed to the <CustomTooltip /> component
|
|
168
|
-
* @param expectedInventoryItemText - the expected inventory item text content
|
|
169
|
-
*/
|
|
170
|
-
const expectPassingTestsForActiveTooltip = ({
|
|
171
|
-
tooltipProps,
|
|
172
|
-
expectedInventoryItemText,
|
|
173
|
-
}) => {
|
|
174
|
-
const { container, getByRole } = render(
|
|
175
|
-
<CustomTooltip {...tooltipProps} />
|
|
67
|
+
it("returns data when fetch successful", async () => {
|
|
68
|
+
fetchMock.get(
|
|
69
|
+
`path:${STATS_API_ENDPOINT}`,
|
|
70
|
+
{
|
|
71
|
+
body: JSON.stringify(statisticsApiResponseData),
|
|
72
|
+
status: 200,
|
|
73
|
+
},
|
|
74
|
+
{ overwriteRoutes: true }
|
|
176
75
|
);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const detailChildren = detail.children;
|
|
181
|
-
const heading = getByRole("heading", { level: 1, name: "Collection X" });
|
|
182
|
-
const items = tooltipContent.querySelectorAll("p.customTooltipItem");
|
|
183
|
-
const divider = detail.querySelector("hr");
|
|
184
|
-
|
|
185
|
-
expect(heading).toHaveTextContent("Collection X");
|
|
186
|
-
|
|
187
|
-
// Eight (8) metrics in the following order.
|
|
188
|
-
expect(items).toHaveLength(8);
|
|
189
|
-
// The expected inventory item labels array should be the same length.
|
|
190
|
-
expect(expectedInventoryItemText).toHaveLength(items.length);
|
|
191
|
-
// And the items should contain at least the expected text.
|
|
192
|
-
Array.from(items).forEach((item, index) => {
|
|
193
|
-
expect(item).toHaveTextContent(expectedInventoryItemText[index]);
|
|
76
|
+
|
|
77
|
+
const { result, waitFor } = renderHook(() => useGetStatsQuery(), {
|
|
78
|
+
wrapper,
|
|
194
79
|
});
|
|
195
80
|
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
expect(
|
|
199
|
-
expect(
|
|
200
|
-
expect(
|
|
201
|
-
expect(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
81
|
+
// Expect loading status immediately after first use of the hook.
|
|
82
|
+
let { isSuccess, isError, error, data } = result.current;
|
|
83
|
+
expect(isSuccess).toBe(false);
|
|
84
|
+
expect(isError).toBe(false);
|
|
85
|
+
expect(error).toBe(undefined);
|
|
86
|
+
expect(data).toEqual(undefined);
|
|
87
|
+
|
|
88
|
+
// Once loaded, we should have our data.
|
|
89
|
+
await waitFor(() => !result.current.isLoading);
|
|
90
|
+
({ isSuccess, isError, error, data } = result.current);
|
|
91
|
+
|
|
92
|
+
expect(isSuccess).toBe(true);
|
|
93
|
+
expect(isError).toBe(false);
|
|
94
|
+
expect(error).toBe(undefined);
|
|
95
|
+
expect(data).toEqual(normalizedData);
|
|
96
|
+
|
|
97
|
+
// But if we use the hook again, we should get the data back from
|
|
98
|
+
// the cache immediately, without loading state.
|
|
99
|
+
const { result: result2 } = renderHook(() => useGetStatsQuery(), {
|
|
100
|
+
wrapper,
|
|
213
101
|
});
|
|
102
|
+
({ isSuccess, isError, error, data } = result2.current);
|
|
103
|
+
|
|
104
|
+
expect(isSuccess).toBe(true);
|
|
105
|
+
expect(isError).toBe(false);
|
|
106
|
+
expect(error).toBe(undefined);
|
|
107
|
+
expect(data).toEqual(normalizedData);
|
|
108
|
+
});
|
|
214
109
|
|
|
215
|
-
|
|
216
|
-
|
|
110
|
+
it("returns error and no data when request fails", async () => {
|
|
111
|
+
fetchMock.get(
|
|
112
|
+
`path:${STATS_API_ENDPOINT}`,
|
|
113
|
+
{
|
|
114
|
+
status: 500,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
overwriteRoutes: true,
|
|
118
|
+
}
|
|
217
119
|
);
|
|
218
|
-
const tooltipContent = container.querySelectorAll(".customTooltip");
|
|
219
120
|
|
|
220
|
-
|
|
121
|
+
const { result, waitFor } = renderHook(() => useGetStatsQuery(), {
|
|
122
|
+
wrapper,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Expect loading status immediately after first use of the hook.
|
|
126
|
+
let { isSuccess, isError, error, data } = result.current;
|
|
127
|
+
expect(isSuccess).toBe(false);
|
|
128
|
+
expect(isError).toBe(false);
|
|
129
|
+
expect(error).toBe(undefined);
|
|
130
|
+
expect(data).toEqual(undefined);
|
|
131
|
+
|
|
132
|
+
await waitFor(() => !result.current.isLoading);
|
|
133
|
+
({ isSuccess, isError, error, data } = result.current);
|
|
134
|
+
|
|
135
|
+
expect(isSuccess).toBe(false);
|
|
136
|
+
expect(isError).toBe(true);
|
|
137
|
+
expect(data).toBe(undefined);
|
|
138
|
+
expect((error as FetchErrorData).status).toBe(500);
|
|
139
|
+
|
|
140
|
+
// But if we use the hook again, we should get our error back from
|
|
141
|
+
// the cache immediately, without loading state.
|
|
142
|
+
const { result: result2 } = renderHook(() => useGetStatsQuery(), {
|
|
143
|
+
wrapper,
|
|
144
|
+
});
|
|
145
|
+
({ isSuccess, isError, error, data } = result2.current);
|
|
146
|
+
|
|
147
|
+
expect(isSuccess).toBe(false);
|
|
148
|
+
expect(isError).toBe(true);
|
|
149
|
+
expect(data).toBe(undefined);
|
|
150
|
+
expect((error as FetchErrorData).status).toBe(500);
|
|
221
151
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("rendering", () => {
|
|
155
|
+
beforeAll(() => {
|
|
156
|
+
fetchMock.get(`path:${STATS_API_ENDPOINT}`, {
|
|
157
|
+
body: JSON.stringify(statisticsApiResponseData),
|
|
158
|
+
status: 200,
|
|
227
159
|
});
|
|
160
|
+
});
|
|
161
|
+
afterAll(() => {
|
|
162
|
+
fetchMock.restore();
|
|
163
|
+
});
|
|
228
164
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"Licensed Titles:",
|
|
234
|
-
"Metered Licenses Available:",
|
|
235
|
-
"Metered Licenses Owned:",
|
|
236
|
-
"Open Access Titles:",
|
|
237
|
-
"Unlimited License Titles:",
|
|
238
|
-
];
|
|
165
|
+
describe("correctly handles fetching and caching", () => {
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
fetchMock.resetHistory();
|
|
168
|
+
});
|
|
239
169
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
170
|
+
const assertLoadingState = ({ getByRole }) => {
|
|
171
|
+
getByRole("dialog", { name: "Loading" });
|
|
172
|
+
getByRole("heading", { level: 1, name: "Loading" });
|
|
173
|
+
};
|
|
174
|
+
const assertNotLoadingState = ({ queryByRole }) => {
|
|
175
|
+
const missingLoadingDialog = queryByRole("dialog", { name: "Loading" });
|
|
176
|
+
const missingLoadingHeading = queryByRole("heading", {
|
|
177
|
+
level: 1,
|
|
178
|
+
name: "Loading",
|
|
179
|
+
});
|
|
180
|
+
expect(missingLoadingDialog).not.toBeInTheDocument();
|
|
181
|
+
expect(missingLoadingHeading).not.toBeInTheDocument();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
it("shows/hides the loading indicator", async () => {
|
|
185
|
+
// We haven't tried to fetch anything yet.
|
|
186
|
+
expect(fetchMock.calls()).toHaveLength(0);
|
|
187
|
+
|
|
188
|
+
const { rerender, getByRole, queryByRole } = renderWithProviders(
|
|
189
|
+
<Stats />
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// We should start in the loading state.
|
|
193
|
+
assertLoadingState({ getByRole });
|
|
194
|
+
|
|
195
|
+
// Wait a tick for the statistics to render.
|
|
196
|
+
await new Promise(process.nextTick);
|
|
197
|
+
// Now we've fetched something.
|
|
198
|
+
expect(fetchMock.calls()).toHaveLength(1);
|
|
199
|
+
|
|
200
|
+
rerender(<Stats />);
|
|
201
|
+
|
|
202
|
+
// We should show our content without the loading state.
|
|
203
|
+
assertNotLoadingState({ queryByRole });
|
|
204
|
+
getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
|
|
205
|
+
|
|
206
|
+
// We haven't made another call, since the response is cached.
|
|
207
|
+
expect(fetchMock.calls()).toHaveLength(1);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("doesn't fetch again, because response is cached", async () => {
|
|
211
|
+
const { getByRole, queryByRole } = renderWithProviders(<Stats />);
|
|
212
|
+
|
|
213
|
+
// We should show our content immediately, without entering the loading state.
|
|
214
|
+
assertNotLoadingState({ queryByRole });
|
|
215
|
+
getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
|
|
216
|
+
|
|
217
|
+
// We never tried to fetch anything because the result is cached.
|
|
218
|
+
expect(fetchMock.calls()).toHaveLength(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("show stats for a library, if a library is specified", async () => {
|
|
222
|
+
const { getByRole, queryByRole, getByText } = renderWithProviders(
|
|
223
|
+
<Stats library={sampleLibraryKey} />
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// We should show our content immediately, without entering the loading state.
|
|
227
|
+
assertNotLoadingState({ queryByRole });
|
|
228
|
+
getByRole("heading", {
|
|
229
|
+
level: 2,
|
|
230
|
+
name: `${sampleLibraryName} Dashboard`,
|
|
231
|
+
});
|
|
232
|
+
getByRole("heading", { level: 3, name: statGroupToHeading.patrons });
|
|
233
|
+
getByText("21");
|
|
234
|
+
|
|
235
|
+
// We never tried to fetch anything because the result is cached.
|
|
236
|
+
expect(fetchMock.calls()).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("shows site-wide stats when no library specified", async () => {
|
|
240
|
+
const { getByRole, getByText, queryByRole } = renderWithProviders(
|
|
241
|
+
<Stats />
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// We should show our content immediately, without entering the loading state.
|
|
245
|
+
assertNotLoadingState({ queryByRole });
|
|
246
|
+
|
|
247
|
+
getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
|
|
248
|
+
getByRole("heading", {
|
|
249
|
+
level: 3,
|
|
250
|
+
name: "Current Circulation Activity",
|
|
251
|
+
});
|
|
252
|
+
getByText("1.6k");
|
|
253
|
+
|
|
254
|
+
// We never tried to fetch anything because the result is cached.
|
|
255
|
+
expect(fetchMock.calls()).toHaveLength(0);
|
|
243
256
|
});
|
|
244
257
|
});
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
258
|
+
|
|
259
|
+
describe("has correct statistics groups", () => {
|
|
260
|
+
it("shows the right groups with a library", () => {
|
|
261
|
+
const { getAllByRole } = renderWithProviders(
|
|
262
|
+
<Stats library={sampleLibraryKey} />
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const groupHeadings = getAllByRole("heading", { level: 3 });
|
|
266
|
+
const expectedHeadings = [
|
|
267
|
+
statGroupToHeading.patrons,
|
|
268
|
+
statGroupToHeading.usageReports,
|
|
269
|
+
statGroupToHeading.collections,
|
|
270
|
+
];
|
|
271
|
+
expect(groupHeadings).toHaveLength(3);
|
|
272
|
+
groupHeadings.forEach((heading, index) => {
|
|
273
|
+
expect(heading).toHaveTextContent(expectedHeadings[index]);
|
|
274
|
+
});
|
|
250
275
|
});
|
|
251
276
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"Available Titles: 7,953",
|
|
255
|
-
"Metered License Titles: 7,974",
|
|
256
|
-
"Licensed Titles: 7,974",
|
|
257
|
-
"Metered Licenses Available: 75,446",
|
|
258
|
-
"Metered Licenses Owned: 301,541",
|
|
259
|
-
"Open Access Titles: 0",
|
|
260
|
-
"Unlimited License Titles: 0",
|
|
261
|
-
];
|
|
277
|
+
it("shows the right groups with/out a library", () => {
|
|
278
|
+
const { getAllByRole } = renderWithProviders(<Stats />);
|
|
262
279
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
280
|
+
const groupHeadings = getAllByRole("heading", { level: 3 });
|
|
281
|
+
const expectedHeadings = [
|
|
282
|
+
statGroupToHeading.patrons,
|
|
283
|
+
statGroupToHeading.circulations,
|
|
284
|
+
statGroupToHeading.inventory,
|
|
285
|
+
statGroupToHeading.collections,
|
|
286
|
+
];
|
|
287
|
+
expect(groupHeadings).toHaveLength(4);
|
|
288
|
+
groupHeadings.forEach((heading, index) => {
|
|
289
|
+
expect(heading).toHaveTextContent(expectedHeadings[index]);
|
|
290
|
+
});
|
|
266
291
|
});
|
|
267
292
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
293
|
+
|
|
294
|
+
describe("shows the correct UI with/out sysadmin role", () => {
|
|
295
|
+
const systemAdmin = [{ role: "system" }];
|
|
296
|
+
const managerAll = [{ role: "manager-all" }];
|
|
297
|
+
const librarianAll = [{ role: "librarian-all" }];
|
|
298
|
+
|
|
299
|
+
const collectionNames = [
|
|
300
|
+
"New BiblioBoard Test",
|
|
301
|
+
"New Bibliotheca Test Collection",
|
|
302
|
+
"Palace Bookshelf",
|
|
303
|
+
"TEST Baker & Taylor",
|
|
304
|
+
"TEST Palace Marketplace",
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
it("tests BarChart component", () => {
|
|
308
|
+
const contextProviderProps: Partial<ContextProviderProps> = {
|
|
309
|
+
roles: systemAdmin,
|
|
310
|
+
dashboardCollectionsBarChart: { width: 800 },
|
|
311
|
+
};
|
|
312
|
+
const { container, getByRole } = renderWithProviders(
|
|
313
|
+
<Stats library={sampleLibraryKey} />,
|
|
314
|
+
{ contextProviderProps }
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const collectionsHeading = getByRole("heading", {
|
|
318
|
+
level: 3,
|
|
319
|
+
name: statGroupToHeading.collections,
|
|
320
|
+
});
|
|
321
|
+
const collectionsGroup = collectionsHeading.closest(".stat-group");
|
|
322
|
+
const barChartAxisTick = collectionsGroup.querySelectorAll(
|
|
323
|
+
".recharts-cartesian-axis-tick"
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// We expect the first ticks to be along the y-axis, which
|
|
327
|
+
// should have our collection names.
|
|
328
|
+
collectionNames.forEach((name, index) => {
|
|
329
|
+
expect(barChartAxisTick[index]).toHaveTextContent(name);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Clean up the container after each render.
|
|
333
|
+
document.body.removeChild(container);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("shows collection bar chart for sysadmins, but list for others", () => {
|
|
337
|
+
// We'll use this function to test multiple scenarios.
|
|
338
|
+
const testFor = (
|
|
339
|
+
expectBarChart: boolean,
|
|
340
|
+
roles: { role: string; library?: string }[]
|
|
341
|
+
) => {
|
|
342
|
+
const contextProviderProps: Partial<ContextProviderProps> = { roles };
|
|
343
|
+
const { container, getByRole } = renderWithProviders(
|
|
344
|
+
<Stats library={sampleLibraryKey} />,
|
|
345
|
+
{ contextProviderProps }
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const collectionsHeading = getByRole("heading", {
|
|
349
|
+
level: 3,
|
|
350
|
+
name: statGroupToHeading.collections,
|
|
351
|
+
});
|
|
352
|
+
const collectionsGroup = collectionsHeading.closest(".stat-group");
|
|
353
|
+
|
|
354
|
+
if (expectBarChart) {
|
|
355
|
+
collectionsGroup.querySelector(".recharts-responsive-container");
|
|
356
|
+
} else {
|
|
357
|
+
const list = collectionsGroup.querySelector("ul");
|
|
358
|
+
const items = list.querySelectorAll("li");
|
|
359
|
+
expect(items.length).toBe(collectionNames.length);
|
|
360
|
+
|
|
361
|
+
collectionNames.forEach((name: string) => {
|
|
362
|
+
expect(list).toHaveTextContent(name);
|
|
363
|
+
});
|
|
364
|
+
items.forEach((item, index) => {
|
|
365
|
+
expect(item).toHaveTextContent(collectionNames[index]);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Clean up the container after each render.
|
|
370
|
+
document.body.removeChild(container);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// If the feature flag is set, the button should be visible only to sysadmins.
|
|
374
|
+
testFor(true, systemAdmin);
|
|
375
|
+
testFor(false, managerAll);
|
|
376
|
+
testFor(false, librarianAll);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("shows inventory reports only for sysadmins, if sysadmin-only flag set", () => {
|
|
380
|
+
const fakeQuickSightHref = "https://example.com/fakeQS";
|
|
381
|
+
|
|
382
|
+
// We'll use this function to test multiple scenarios.
|
|
383
|
+
const renderFor = (
|
|
384
|
+
onlySysadmins: boolean,
|
|
385
|
+
roles: { role: string; library?: string }[]
|
|
386
|
+
) => {
|
|
387
|
+
const contextProviderProps: Partial<ContextProviderProps> = {
|
|
388
|
+
featureFlags: { reportsOnlyForSysadmins: onlySysadmins },
|
|
389
|
+
roles,
|
|
390
|
+
quicksightPagePath: fakeQuickSightHref,
|
|
391
|
+
};
|
|
392
|
+
const {
|
|
393
|
+
container,
|
|
394
|
+
getByRole,
|
|
395
|
+
queryByRole,
|
|
396
|
+
queryByText,
|
|
397
|
+
} = renderWithProviders(<Stats library={sampleLibraryKey} />, {
|
|
398
|
+
contextProviderProps,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// We should always render a Usage reports group when a library is specified.
|
|
402
|
+
getByRole("heading", {
|
|
403
|
+
level: 3,
|
|
404
|
+
name: statGroupToHeading.usageReports,
|
|
405
|
+
});
|
|
406
|
+
const usageReportLink = getByRole("link", { name: /View Usage/i });
|
|
407
|
+
expect(usageReportLink).toHaveAttribute("href", fakeQuickSightHref);
|
|
408
|
+
|
|
409
|
+
const requestButton = queryByRole("button", {
|
|
410
|
+
name: /Request Report/i,
|
|
411
|
+
});
|
|
412
|
+
const blurb = queryByText(
|
|
413
|
+
/These reports provide up-to-date data on both inventory and holds/i
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// The inventory report blurb should be visible only when the button is.
|
|
417
|
+
if (requestButton) {
|
|
418
|
+
expect(blurb).not.toBeNull();
|
|
419
|
+
} else {
|
|
420
|
+
expect(blurb).toBeNull();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Clean up the container after each render.
|
|
424
|
+
document.body.removeChild(container);
|
|
425
|
+
return requestButton;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// If the feature flag is set, the button should be visible only to sysadmins.
|
|
429
|
+
expect(renderFor(true, systemAdmin)).not.toBeNull();
|
|
430
|
+
expect(renderFor(true, managerAll)).toBeNull();
|
|
431
|
+
expect(renderFor(true, librarianAll)).toBeNull();
|
|
432
|
+
// If the feature flag is false, the button should be visible to all users.
|
|
433
|
+
expect(renderFor(false, systemAdmin)).not.toBeNull();
|
|
434
|
+
expect(renderFor(false, managerAll)).not.toBeNull();
|
|
435
|
+
expect(renderFor(false, librarianAll)).not.toBeNull();
|
|
273
436
|
});
|
|
437
|
+
});
|
|
274
438
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
439
|
+
describe("charting - custom tooltip", () => {
|
|
440
|
+
const defaultLabel = "Collection X";
|
|
441
|
+
const summaryInventory = {
|
|
442
|
+
availableTitles: 7953,
|
|
443
|
+
licensedTitles: 7974,
|
|
444
|
+
meteredLicenseTitles: 7974,
|
|
445
|
+
meteredLicensesAvailable: 75446,
|
|
446
|
+
meteredLicensesOwned: 301541,
|
|
447
|
+
openAccessTitles: 0,
|
|
448
|
+
titles: 7974,
|
|
449
|
+
unlimitedLicenseTitles: 0,
|
|
450
|
+
};
|
|
451
|
+
const perMediumInventory = {
|
|
452
|
+
Audio: {
|
|
453
|
+
availableTitles: 148,
|
|
454
|
+
licensedTitles: 165,
|
|
455
|
+
meteredLicenseTitles: 165,
|
|
456
|
+
meteredLicensesAvailable: 221,
|
|
457
|
+
meteredLicensesOwned: 392,
|
|
458
|
+
openAccessTitles: 0,
|
|
459
|
+
titles: 165,
|
|
460
|
+
unlimitedLicenseTitles: 0,
|
|
461
|
+
},
|
|
462
|
+
Book: {
|
|
463
|
+
availableTitles: 7805,
|
|
464
|
+
licensedTitles: 7809,
|
|
465
|
+
meteredLicenseTitles: 7809,
|
|
466
|
+
meteredLicensesAvailable: 75225,
|
|
467
|
+
meteredLicensesOwned: 301149,
|
|
468
|
+
openAccessTitles: 0,
|
|
469
|
+
titles: 7809,
|
|
470
|
+
unlimitedLicenseTitles: 0,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
const defaultChartItemWithoutPerMediumInventory = {
|
|
474
|
+
name: defaultLabel,
|
|
475
|
+
...summaryInventory,
|
|
476
|
+
};
|
|
477
|
+
const defaultChartItemWithPerMediumInventory = {
|
|
478
|
+
...defaultChartItemWithoutPerMediumInventory,
|
|
479
|
+
_by_medium: perMediumInventory,
|
|
480
|
+
};
|
|
481
|
+
const defaultPayload = [
|
|
482
|
+
{
|
|
483
|
+
fill: "#606060",
|
|
484
|
+
dataKey: "meteredLicenseTitles",
|
|
485
|
+
name: "Metered License Titles",
|
|
486
|
+
color: "#606060",
|
|
487
|
+
value: 7974,
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
fill: "#404040",
|
|
491
|
+
dataKey: "unlimitedLicenseTitles",
|
|
492
|
+
name: "Unlimited License Titles",
|
|
493
|
+
color: "#404040",
|
|
494
|
+
value: 0,
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
fill: "#202020",
|
|
498
|
+
dataKey: "openAccessTitles",
|
|
499
|
+
name: "Open Access Titles",
|
|
500
|
+
color: "#202020",
|
|
501
|
+
value: 0,
|
|
502
|
+
},
|
|
284
503
|
];
|
|
285
504
|
|
|
286
|
-
|
|
505
|
+
const populateTooltipProps = ({
|
|
506
|
+
active = true,
|
|
507
|
+
label = defaultLabel,
|
|
508
|
+
payload = [],
|
|
509
|
+
chartItem = undefined,
|
|
510
|
+
}) => {
|
|
511
|
+
const constructedChartItem = !chartItem
|
|
512
|
+
? chartItem
|
|
513
|
+
: {
|
|
514
|
+
...chartItem,
|
|
515
|
+
name: label,
|
|
516
|
+
};
|
|
517
|
+
const constructedPayload = payload.map((entry) => ({
|
|
518
|
+
...entry,
|
|
519
|
+
payload: constructedChartItem,
|
|
520
|
+
}));
|
|
521
|
+
return {
|
|
522
|
+
active,
|
|
523
|
+
label,
|
|
524
|
+
payload: constructedPayload,
|
|
525
|
+
};
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Helper function to test passing tests for a tooltip
|
|
530
|
+
*
|
|
531
|
+
* @param tooltipProps - passed to the <CustomTooltip /> component
|
|
532
|
+
* @param expectedInventoryItemText - the expected inventory item text content
|
|
533
|
+
*/
|
|
534
|
+
const expectPassingTestsForActiveTooltip = ({
|
|
287
535
|
tooltipProps,
|
|
288
536
|
expectedInventoryItemText,
|
|
537
|
+
}) => {
|
|
538
|
+
const { container, getByRole } = render(
|
|
539
|
+
<CustomTooltip {...tooltipProps} />
|
|
540
|
+
);
|
|
541
|
+
const tooltipContent = container.querySelector(".customTooltip");
|
|
542
|
+
|
|
543
|
+
const detail = tooltipContent.querySelector(".customTooltipDetail");
|
|
544
|
+
const detailChildren = detail.children;
|
|
545
|
+
const heading = getByRole("heading", {
|
|
546
|
+
level: 1,
|
|
547
|
+
name: "Collection X",
|
|
548
|
+
});
|
|
549
|
+
const items = tooltipContent.querySelectorAll("p.customTooltipItem");
|
|
550
|
+
const divider = detail.querySelector("hr");
|
|
551
|
+
|
|
552
|
+
expect(heading).toHaveTextContent("Collection X");
|
|
553
|
+
|
|
554
|
+
// Eight (8) metrics in the following order.
|
|
555
|
+
expect(items).toHaveLength(8);
|
|
556
|
+
// The expected inventory item labels array should be the same length.
|
|
557
|
+
expect(expectedInventoryItemText).toHaveLength(items.length);
|
|
558
|
+
// And the items should contain at least the expected text.
|
|
559
|
+
Array.from(items).forEach((item, index) => {
|
|
560
|
+
expect(item).toHaveTextContent(expectedInventoryItemText[index]);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// The heading should be at the top and the divider (`hr`)
|
|
564
|
+
// should be between the third and fourth statistics.
|
|
565
|
+
expect(detailChildren).toHaveLength(10);
|
|
566
|
+
expect(heading).toEqual(detailChildren[0]);
|
|
567
|
+
expect(items[0]).toEqual(detailChildren[1]);
|
|
568
|
+
expect(items[2]).toEqual(detailChildren[3]);
|
|
569
|
+
expect(divider).toEqual(detailChildren[4]);
|
|
570
|
+
expect(items[3]).toEqual(detailChildren[5]);
|
|
571
|
+
expect(items[7]).toEqual(detailChildren[9]);
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
it("should not render when active is false", () => {
|
|
575
|
+
// Recharts sticks some extra props
|
|
576
|
+
const tooltipProps = populateTooltipProps({
|
|
577
|
+
active: false,
|
|
578
|
+
chartItem: defaultChartItemWithPerMediumInventory,
|
|
579
|
+
payload: defaultPayload,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const { container } = render(<CustomTooltip {...tooltipProps} />);
|
|
583
|
+
const tooltipContent = container.querySelectorAll(".customTooltip");
|
|
584
|
+
|
|
585
|
+
expect(tooltipContent).toHaveLength(0);
|
|
586
|
+
});
|
|
587
|
+
it("should render when active is true", () => {
|
|
588
|
+
const tooltipProps = populateTooltipProps({
|
|
589
|
+
active: true,
|
|
590
|
+
chartItem: defaultChartItemWithoutPerMediumInventory,
|
|
591
|
+
payload: defaultPayload,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const expectedInventoryItemText = [
|
|
595
|
+
"Titles:",
|
|
596
|
+
"Available Titles:",
|
|
597
|
+
"Metered License Titles:",
|
|
598
|
+
"Licensed Titles:",
|
|
599
|
+
"Metered Licenses Available:",
|
|
600
|
+
"Metered Licenses Owned:",
|
|
601
|
+
"Open Access Titles:",
|
|
602
|
+
"Unlimited License Titles:",
|
|
603
|
+
];
|
|
604
|
+
|
|
605
|
+
expectPassingTestsForActiveTooltip({
|
|
606
|
+
tooltipProps,
|
|
607
|
+
expectedInventoryItemText,
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
it("should render without per-medium inventory", () => {
|
|
611
|
+
const tooltipProps = populateTooltipProps({
|
|
612
|
+
active: true,
|
|
613
|
+
chartItem: defaultChartItemWithoutPerMediumInventory,
|
|
614
|
+
payload: defaultPayload,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const expectedInventoryItemText = [
|
|
618
|
+
"Titles: 7,974",
|
|
619
|
+
"Available Titles: 7,953",
|
|
620
|
+
"Metered License Titles: 7,974",
|
|
621
|
+
"Licensed Titles: 7,974",
|
|
622
|
+
"Metered Licenses Available: 75,446",
|
|
623
|
+
"Metered Licenses Owned: 301,541",
|
|
624
|
+
"Open Access Titles: 0",
|
|
625
|
+
"Unlimited License Titles: 0",
|
|
626
|
+
];
|
|
627
|
+
|
|
628
|
+
expectPassingTestsForActiveTooltip({
|
|
629
|
+
tooltipProps,
|
|
630
|
+
expectedInventoryItemText,
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
it("should render additional detail with per-medium inventory", () => {
|
|
634
|
+
const tooltipProps = populateTooltipProps({
|
|
635
|
+
active: true,
|
|
636
|
+
chartItem: defaultChartItemWithPerMediumInventory,
|
|
637
|
+
payload: defaultPayload,
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const expectedInventoryItemText = [
|
|
641
|
+
"Titles: 7,974 (Audio: 165, Book: 7,809)",
|
|
642
|
+
"Available Titles: 7,953 (Audio: 148, Book: 7,805)",
|
|
643
|
+
"Metered License Titles: 7,974 (Audio: 165, Book: 7,809)",
|
|
644
|
+
"Licensed Titles: 7,974 (Audio: 165, Book: 7,809)",
|
|
645
|
+
"Metered Licenses Available: 75,446 (Audio: 221, Book: 75,225)",
|
|
646
|
+
"Metered Licenses Owned: 301,541 (Audio: 392, Book: 301,149)",
|
|
647
|
+
"Open Access Titles: 0",
|
|
648
|
+
"Unlimited License Titles: 0",
|
|
649
|
+
];
|
|
650
|
+
|
|
651
|
+
expectPassingTestsForActiveTooltip({
|
|
652
|
+
tooltipProps,
|
|
653
|
+
expectedInventoryItemText,
|
|
654
|
+
});
|
|
289
655
|
});
|
|
290
656
|
});
|
|
291
657
|
});
|