@thepalaceproject/circulation-admin 1.22.0-post.4 → 1.22.0-post.6
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,8 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { render } from "@testing-library/react";
|
|
3
|
-
import LibraryStats
|
|
4
|
-
ALL_LIBRARIES_HEADING,
|
|
5
|
-
} from "../../../src/components/LibraryStats";
|
|
3
|
+
import { ALL_LIBRARIES_HEADING } from "../../../src/components/LibraryStats";
|
|
6
4
|
import { CustomTooltip } from "../../../src/components/StatsCollectionsBarChart";
|
|
7
5
|
import {
|
|
8
6
|
componentWithProviders,
|
|
@@ -28,6 +26,8 @@ import { api } from "../../../src/features/api/apiSlice";
|
|
|
28
26
|
|
|
29
27
|
const normalizedData = normalizeStatistics(statisticsApiResponseData);
|
|
30
28
|
|
|
29
|
+
global.ResizeObserver = require("resize-observer-polyfill");
|
|
30
|
+
|
|
31
31
|
describe("Dashboard Statistics", () => {
|
|
32
32
|
// NB: This adds test to the already existing tests in:
|
|
33
33
|
// - `src/components/__tests__/LibraryStats-test.tsx`.
|
|
@@ -44,6 +44,14 @@ describe("Dashboard Statistics", () => {
|
|
|
44
44
|
Response,
|
|
45
45
|
});
|
|
46
46
|
|
|
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
|
+
|
|
47
55
|
describe("query hook correctly handles fetch responses", () => {
|
|
48
56
|
const wrapper = componentWithProviders();
|
|
49
57
|
|
|
@@ -153,359 +161,483 @@ describe("Dashboard Statistics", () => {
|
|
|
153
161
|
afterAll(() => {
|
|
154
162
|
fetchMock.restore();
|
|
155
163
|
});
|
|
156
|
-
afterEach(() => {
|
|
157
|
-
fetchMock.resetHistory();
|
|
158
|
-
});
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
const assertNotLoadingState = ({ queryByRole }) => {
|
|
165
|
-
const missingLoadingDialog = queryByRole("dialog", { name: "Loading" });
|
|
166
|
-
const missingLoadingHeading = queryByRole("heading", {
|
|
167
|
-
level: 1,
|
|
168
|
-
name: "Loading",
|
|
165
|
+
describe("correctly handles fetching and caching", () => {
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
fetchMock.resetHistory();
|
|
169
168
|
});
|
|
170
|
-
expect(missingLoadingDialog).not.toBeInTheDocument();
|
|
171
|
-
expect(missingLoadingHeading).not.toBeInTheDocument();
|
|
172
|
-
};
|
|
173
169
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
};
|
|
177
183
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
it("shows/hides the loading indicator", async () => {
|
|
185
|
+
// We haven't tried to fetch anything yet.
|
|
186
|
+
expect(fetchMock.calls()).toHaveLength(0);
|
|
181
187
|
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
const { rerender, getByRole, queryByRole } = renderWithProviders(
|
|
189
|
+
<Stats />
|
|
190
|
+
);
|
|
184
191
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// Now we've fetched something.
|
|
188
|
-
expect(fetchMock.calls()).toHaveLength(1);
|
|
192
|
+
// We should start in the loading state.
|
|
193
|
+
assertLoadingState({ getByRole });
|
|
189
194
|
|
|
190
|
-
|
|
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);
|
|
191
199
|
|
|
192
|
-
|
|
193
|
-
assertNotLoadingState({ queryByRole });
|
|
194
|
-
getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
|
|
200
|
+
rerender(<Stats />);
|
|
195
201
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
// We should show our content without the loading state.
|
|
203
|
+
assertNotLoadingState({ queryByRole });
|
|
204
|
+
getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
|
|
199
205
|
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
// We haven't made another call, since the response is cached.
|
|
207
|
+
expect(fetchMock.calls()).toHaveLength(1);
|
|
208
|
+
});
|
|
202
209
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
|
|
210
|
+
it("doesn't fetch again, because response is cached", async () => {
|
|
211
|
+
const { getByRole, queryByRole } = renderWithProviders(<Stats />);
|
|
206
212
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
// We should show our content immediately, without entering the loading state.
|
|
214
|
+
assertNotLoadingState({ queryByRole });
|
|
215
|
+
getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
|
|
210
216
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
<Stats library={sampleLibraryKey} />
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
// We should show our content immediately, without entering the loading state.
|
|
217
|
-
assertNotLoadingState({ queryByRole });
|
|
218
|
-
getByRole("heading", {
|
|
219
|
-
level: 2,
|
|
220
|
-
name: `${sampleLibraryName} Dashboard`,
|
|
217
|
+
// We never tried to fetch anything because the result is cached.
|
|
218
|
+
expect(fetchMock.calls()).toHaveLength(0);
|
|
221
219
|
});
|
|
222
|
-
getByRole("heading", { level: 3, name: "Current Circulation Activity" });
|
|
223
|
-
getByText("623");
|
|
224
|
-
|
|
225
|
-
// We never tried to fetch anything because the result is cached.
|
|
226
|
-
expect(fetchMock.calls()).toHaveLength(0);
|
|
227
|
-
});
|
|
228
220
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
});
|
|
233
238
|
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
it("shows site-wide stats when no library specified", async () => {
|
|
240
|
+
const { getByRole, getByText, queryByRole } = renderWithProviders(
|
|
241
|
+
<Stats />
|
|
242
|
+
);
|
|
236
243
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
getByText("1.6k");
|
|
244
|
+
// We should show our content immediately, without entering the loading state.
|
|
245
|
+
assertNotLoadingState({ queryByRole });
|
|
240
246
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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");
|
|
245
253
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const librariesStatsTestDataByKey = statisticsData.libraries.reduce(
|
|
250
|
-
(map, library) => ({ ...map, [library.key]: library }),
|
|
251
|
-
{}
|
|
252
|
-
);
|
|
253
|
-
const sampleStatsData = librariesStatsTestDataByKey[sampleLibraryKey];
|
|
254
|
-
|
|
255
|
-
const systemAdmin = [{ role: "system" }];
|
|
256
|
-
const managerAll = [{ role: "manager-all" }];
|
|
257
|
-
const librarianAll = [{ role: "librarian-all" }];
|
|
258
|
-
|
|
259
|
-
const baseContextProviderProps = {
|
|
260
|
-
csrfToken: "",
|
|
261
|
-
featureFlags: { reportsOnlyForSysadmins: false },
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const renderFor = (
|
|
265
|
-
onlySysadmins: boolean,
|
|
266
|
-
roles: { role: string; library?: string }[]
|
|
267
|
-
) => {
|
|
268
|
-
const contextProviderProps: ContextProviderProps = {
|
|
269
|
-
...baseContextProviderProps,
|
|
270
|
-
featureFlags: { reportsOnlyForSysadmins: onlySysadmins },
|
|
271
|
-
roles,
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const { container, queryByRole } = renderWithProviders(
|
|
275
|
-
<LibraryStats stats={sampleStatsData} library={sampleLibraryKey} />,
|
|
276
|
-
{ contextProviderProps }
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const result = queryByRole("button", { name: "⬇︎" });
|
|
280
|
-
// Clean up the container after each render.
|
|
281
|
-
document.body.removeChild(container);
|
|
282
|
-
return result;
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
it("shows inventory reports only for sysadmins, if feature flag set", async () => {
|
|
286
|
-
// If the feature flag is set, the button should be visible only to sysadmins.
|
|
287
|
-
expect(renderFor(true, systemAdmin)).not.toBeNull();
|
|
288
|
-
expect(renderFor(true, managerAll)).toBeNull();
|
|
289
|
-
expect(renderFor(true, librarianAll)).toBeNull();
|
|
290
|
-
// If the feature flag is false, the button should be visible to all users.
|
|
291
|
-
expect(renderFor(false, systemAdmin)).not.toBeNull();
|
|
292
|
-
expect(renderFor(false, managerAll)).not.toBeNull();
|
|
293
|
-
expect(renderFor(false, librarianAll)).not.toBeNull();
|
|
254
|
+
// We never tried to fetch anything because the result is cached.
|
|
255
|
+
expect(fetchMock.calls()).toHaveLength(0);
|
|
256
|
+
});
|
|
294
257
|
});
|
|
295
|
-
});
|
|
296
258
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
meteredLicenseTitles: 165,
|
|
314
|
-
meteredLicensesAvailable: 221,
|
|
315
|
-
meteredLicensesOwned: 392,
|
|
316
|
-
openAccessTitles: 0,
|
|
317
|
-
titles: 165,
|
|
318
|
-
unlimitedLicenseTitles: 0,
|
|
319
|
-
},
|
|
320
|
-
Book: {
|
|
321
|
-
availableTitles: 7805,
|
|
322
|
-
licensedTitles: 7809,
|
|
323
|
-
meteredLicenseTitles: 7809,
|
|
324
|
-
meteredLicensesAvailable: 75225,
|
|
325
|
-
meteredLicensesOwned: 301149,
|
|
326
|
-
openAccessTitles: 0,
|
|
327
|
-
titles: 7809,
|
|
328
|
-
unlimitedLicenseTitles: 0,
|
|
329
|
-
},
|
|
330
|
-
};
|
|
331
|
-
const defaultChartItemWithoutPerMediumInventory = {
|
|
332
|
-
name: defaultLabel,
|
|
333
|
-
...summaryInventory,
|
|
334
|
-
};
|
|
335
|
-
const defaultChartItemWithPerMediumInventory = {
|
|
336
|
-
...defaultChartItemWithoutPerMediumInventory,
|
|
337
|
-
_by_medium: perMediumInventory,
|
|
338
|
-
};
|
|
339
|
-
const defaultPayload = [
|
|
340
|
-
{
|
|
341
|
-
fill: "#606060",
|
|
342
|
-
dataKey: "meteredLicenseTitles",
|
|
343
|
-
name: "Metered License Titles",
|
|
344
|
-
color: "#606060",
|
|
345
|
-
value: 7974,
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
fill: "#404040",
|
|
349
|
-
dataKey: "unlimitedLicenseTitles",
|
|
350
|
-
name: "Unlimited License Titles",
|
|
351
|
-
color: "#404040",
|
|
352
|
-
value: 0,
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
fill: "#202020",
|
|
356
|
-
dataKey: "openAccessTitles",
|
|
357
|
-
name: "Open Access Titles",
|
|
358
|
-
color: "#202020",
|
|
359
|
-
value: 0,
|
|
360
|
-
},
|
|
361
|
-
];
|
|
362
|
-
|
|
363
|
-
const populateTooltipProps = ({
|
|
364
|
-
active = true,
|
|
365
|
-
label = defaultLabel,
|
|
366
|
-
payload = [],
|
|
367
|
-
chartItem = undefined,
|
|
368
|
-
}) => {
|
|
369
|
-
const constructedChartItem = !chartItem
|
|
370
|
-
? chartItem
|
|
371
|
-
: {
|
|
372
|
-
...chartItem,
|
|
373
|
-
name: label,
|
|
374
|
-
};
|
|
375
|
-
const constructedPayload = payload.map((entry) => ({
|
|
376
|
-
...entry,
|
|
377
|
-
payload: constructedChartItem,
|
|
378
|
-
}));
|
|
379
|
-
return {
|
|
380
|
-
active,
|
|
381
|
-
label,
|
|
382
|
-
payload: constructedPayload,
|
|
383
|
-
};
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Helper function to test passing tests for a tooltip
|
|
388
|
-
*
|
|
389
|
-
* @param tooltipProps - passed to the <CustomTooltip /> component
|
|
390
|
-
* @param expectedInventoryItemText - the expected inventory item text content
|
|
391
|
-
*/
|
|
392
|
-
const expectPassingTestsForActiveTooltip = ({
|
|
393
|
-
tooltipProps,
|
|
394
|
-
expectedInventoryItemText,
|
|
395
|
-
}) => {
|
|
396
|
-
const { container, getByRole } = render(
|
|
397
|
-
<CustomTooltip {...tooltipProps} />
|
|
398
|
-
);
|
|
399
|
-
const tooltipContent = container.querySelector(".customTooltip");
|
|
400
|
-
|
|
401
|
-
const detail = tooltipContent.querySelector(".customTooltipDetail");
|
|
402
|
-
const detailChildren = detail.children;
|
|
403
|
-
const heading = getByRole("heading", { level: 1, name: "Collection X" });
|
|
404
|
-
const items = tooltipContent.querySelectorAll("p.customTooltipItem");
|
|
405
|
-
const divider = detail.querySelector("hr");
|
|
406
|
-
|
|
407
|
-
expect(heading).toHaveTextContent("Collection X");
|
|
408
|
-
|
|
409
|
-
// Eight (8) metrics in the following order.
|
|
410
|
-
expect(items).toHaveLength(8);
|
|
411
|
-
// The expected inventory item labels array should be the same length.
|
|
412
|
-
expect(expectedInventoryItemText).toHaveLength(items.length);
|
|
413
|
-
// And the items should contain at least the expected text.
|
|
414
|
-
Array.from(items).forEach((item, index) => {
|
|
415
|
-
expect(item).toHaveTextContent(expectedInventoryItemText[index]);
|
|
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
|
+
});
|
|
416
275
|
});
|
|
417
276
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
active: false,
|
|
433
|
-
chartItem: defaultChartItemWithPerMediumInventory,
|
|
434
|
-
payload: defaultPayload,
|
|
277
|
+
it("shows the right groups with/out a library", () => {
|
|
278
|
+
const { getAllByRole } = renderWithProviders(<Stats />);
|
|
279
|
+
|
|
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
|
+
});
|
|
435
291
|
});
|
|
436
|
-
|
|
437
|
-
const { container } = render(<CustomTooltip {...tooltipProps} />);
|
|
438
|
-
const tooltipContent = container.querySelectorAll(".customTooltip");
|
|
439
|
-
|
|
440
|
-
expect(tooltipContent).toHaveLength(0);
|
|
441
292
|
});
|
|
442
|
-
it("should render when active is true", () => {
|
|
443
|
-
const tooltipProps = populateTooltipProps({
|
|
444
|
-
active: true,
|
|
445
|
-
chartItem: defaultChartItemWithoutPerMediumInventory,
|
|
446
|
-
payload: defaultPayload,
|
|
447
|
-
});
|
|
448
293
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
"
|
|
456
|
-
"
|
|
457
|
-
"
|
|
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",
|
|
458
305
|
];
|
|
459
306
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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);
|
|
470
334
|
});
|
|
471
335
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
});
|
|
482
378
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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 { container, getByRole, queryByRole } = renderWithProviders(
|
|
393
|
+
<Stats library={sampleLibraryKey} />,
|
|
394
|
+
{
|
|
395
|
+
contextProviderProps,
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// We should always render a Usage reports group when a library is specified.
|
|
400
|
+
getByRole("heading", {
|
|
401
|
+
level: 3,
|
|
402
|
+
name: statGroupToHeading.usageReports,
|
|
403
|
+
});
|
|
404
|
+
const usageReportLink = getByRole("link", { name: /View Usage/i });
|
|
405
|
+
expect(usageReportLink).toHaveAttribute("href", fakeQuickSightHref);
|
|
406
|
+
|
|
407
|
+
const result = queryByRole("button", { name: /Request Report/i });
|
|
408
|
+
|
|
409
|
+
// Clean up the container after each render.
|
|
410
|
+
document.body.removeChild(container);
|
|
411
|
+
return result;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// If the feature flag is set, the button should be visible only to sysadmins.
|
|
415
|
+
expect(renderFor(true, systemAdmin)).not.toBeNull();
|
|
416
|
+
expect(renderFor(true, managerAll)).toBeNull();
|
|
417
|
+
expect(renderFor(true, librarianAll)).toBeNull();
|
|
418
|
+
// If the feature flag is false, the button should be visible to all users.
|
|
419
|
+
expect(renderFor(false, systemAdmin)).not.toBeNull();
|
|
420
|
+
expect(renderFor(false, managerAll)).not.toBeNull();
|
|
421
|
+
expect(renderFor(false, librarianAll)).not.toBeNull();
|
|
486
422
|
});
|
|
487
423
|
});
|
|
488
|
-
it("should render additional detail with per-medium inventory", () => {
|
|
489
|
-
const tooltipProps = populateTooltipProps({
|
|
490
|
-
active: true,
|
|
491
|
-
chartItem: defaultChartItemWithPerMediumInventory,
|
|
492
|
-
payload: defaultPayload,
|
|
493
|
-
});
|
|
494
424
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
425
|
+
describe("charting - custom tooltip", () => {
|
|
426
|
+
const defaultLabel = "Collection X";
|
|
427
|
+
const summaryInventory = {
|
|
428
|
+
availableTitles: 7953,
|
|
429
|
+
licensedTitles: 7974,
|
|
430
|
+
meteredLicenseTitles: 7974,
|
|
431
|
+
meteredLicensesAvailable: 75446,
|
|
432
|
+
meteredLicensesOwned: 301541,
|
|
433
|
+
openAccessTitles: 0,
|
|
434
|
+
titles: 7974,
|
|
435
|
+
unlimitedLicenseTitles: 0,
|
|
436
|
+
};
|
|
437
|
+
const perMediumInventory = {
|
|
438
|
+
Audio: {
|
|
439
|
+
availableTitles: 148,
|
|
440
|
+
licensedTitles: 165,
|
|
441
|
+
meteredLicenseTitles: 165,
|
|
442
|
+
meteredLicensesAvailable: 221,
|
|
443
|
+
meteredLicensesOwned: 392,
|
|
444
|
+
openAccessTitles: 0,
|
|
445
|
+
titles: 165,
|
|
446
|
+
unlimitedLicenseTitles: 0,
|
|
447
|
+
},
|
|
448
|
+
Book: {
|
|
449
|
+
availableTitles: 7805,
|
|
450
|
+
licensedTitles: 7809,
|
|
451
|
+
meteredLicenseTitles: 7809,
|
|
452
|
+
meteredLicensesAvailable: 75225,
|
|
453
|
+
meteredLicensesOwned: 301149,
|
|
454
|
+
openAccessTitles: 0,
|
|
455
|
+
titles: 7809,
|
|
456
|
+
unlimitedLicenseTitles: 0,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
const defaultChartItemWithoutPerMediumInventory = {
|
|
460
|
+
name: defaultLabel,
|
|
461
|
+
...summaryInventory,
|
|
462
|
+
};
|
|
463
|
+
const defaultChartItemWithPerMediumInventory = {
|
|
464
|
+
...defaultChartItemWithoutPerMediumInventory,
|
|
465
|
+
_by_medium: perMediumInventory,
|
|
466
|
+
};
|
|
467
|
+
const defaultPayload = [
|
|
468
|
+
{
|
|
469
|
+
fill: "#606060",
|
|
470
|
+
dataKey: "meteredLicenseTitles",
|
|
471
|
+
name: "Metered License Titles",
|
|
472
|
+
color: "#606060",
|
|
473
|
+
value: 7974,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
fill: "#404040",
|
|
477
|
+
dataKey: "unlimitedLicenseTitles",
|
|
478
|
+
name: "Unlimited License Titles",
|
|
479
|
+
color: "#404040",
|
|
480
|
+
value: 0,
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
fill: "#202020",
|
|
484
|
+
dataKey: "openAccessTitles",
|
|
485
|
+
name: "Open Access Titles",
|
|
486
|
+
color: "#202020",
|
|
487
|
+
value: 0,
|
|
488
|
+
},
|
|
504
489
|
];
|
|
505
490
|
|
|
506
|
-
|
|
491
|
+
const populateTooltipProps = ({
|
|
492
|
+
active = true,
|
|
493
|
+
label = defaultLabel,
|
|
494
|
+
payload = [],
|
|
495
|
+
chartItem = undefined,
|
|
496
|
+
}) => {
|
|
497
|
+
const constructedChartItem = !chartItem
|
|
498
|
+
? chartItem
|
|
499
|
+
: {
|
|
500
|
+
...chartItem,
|
|
501
|
+
name: label,
|
|
502
|
+
};
|
|
503
|
+
const constructedPayload = payload.map((entry) => ({
|
|
504
|
+
...entry,
|
|
505
|
+
payload: constructedChartItem,
|
|
506
|
+
}));
|
|
507
|
+
return {
|
|
508
|
+
active,
|
|
509
|
+
label,
|
|
510
|
+
payload: constructedPayload,
|
|
511
|
+
};
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Helper function to test passing tests for a tooltip
|
|
516
|
+
*
|
|
517
|
+
* @param tooltipProps - passed to the <CustomTooltip /> component
|
|
518
|
+
* @param expectedInventoryItemText - the expected inventory item text content
|
|
519
|
+
*/
|
|
520
|
+
const expectPassingTestsForActiveTooltip = ({
|
|
507
521
|
tooltipProps,
|
|
508
522
|
expectedInventoryItemText,
|
|
523
|
+
}) => {
|
|
524
|
+
const { container, getByRole } = render(
|
|
525
|
+
<CustomTooltip {...tooltipProps} />
|
|
526
|
+
);
|
|
527
|
+
const tooltipContent = container.querySelector(".customTooltip");
|
|
528
|
+
|
|
529
|
+
const detail = tooltipContent.querySelector(".customTooltipDetail");
|
|
530
|
+
const detailChildren = detail.children;
|
|
531
|
+
const heading = getByRole("heading", {
|
|
532
|
+
level: 1,
|
|
533
|
+
name: "Collection X",
|
|
534
|
+
});
|
|
535
|
+
const items = tooltipContent.querySelectorAll("p.customTooltipItem");
|
|
536
|
+
const divider = detail.querySelector("hr");
|
|
537
|
+
|
|
538
|
+
expect(heading).toHaveTextContent("Collection X");
|
|
539
|
+
|
|
540
|
+
// Eight (8) metrics in the following order.
|
|
541
|
+
expect(items).toHaveLength(8);
|
|
542
|
+
// The expected inventory item labels array should be the same length.
|
|
543
|
+
expect(expectedInventoryItemText).toHaveLength(items.length);
|
|
544
|
+
// And the items should contain at least the expected text.
|
|
545
|
+
Array.from(items).forEach((item, index) => {
|
|
546
|
+
expect(item).toHaveTextContent(expectedInventoryItemText[index]);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// The heading should be at the top and the divider (`hr`)
|
|
550
|
+
// should be between the third and fourth statistics.
|
|
551
|
+
expect(detailChildren).toHaveLength(10);
|
|
552
|
+
expect(heading).toEqual(detailChildren[0]);
|
|
553
|
+
expect(items[0]).toEqual(detailChildren[1]);
|
|
554
|
+
expect(items[2]).toEqual(detailChildren[3]);
|
|
555
|
+
expect(divider).toEqual(detailChildren[4]);
|
|
556
|
+
expect(items[3]).toEqual(detailChildren[5]);
|
|
557
|
+
expect(items[7]).toEqual(detailChildren[9]);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
it("should not render when active is false", () => {
|
|
561
|
+
// Recharts sticks some extra props
|
|
562
|
+
const tooltipProps = populateTooltipProps({
|
|
563
|
+
active: false,
|
|
564
|
+
chartItem: defaultChartItemWithPerMediumInventory,
|
|
565
|
+
payload: defaultPayload,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const { container } = render(<CustomTooltip {...tooltipProps} />);
|
|
569
|
+
const tooltipContent = container.querySelectorAll(".customTooltip");
|
|
570
|
+
|
|
571
|
+
expect(tooltipContent).toHaveLength(0);
|
|
572
|
+
});
|
|
573
|
+
it("should render when active is true", () => {
|
|
574
|
+
const tooltipProps = populateTooltipProps({
|
|
575
|
+
active: true,
|
|
576
|
+
chartItem: defaultChartItemWithoutPerMediumInventory,
|
|
577
|
+
payload: defaultPayload,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const expectedInventoryItemText = [
|
|
581
|
+
"Titles:",
|
|
582
|
+
"Available Titles:",
|
|
583
|
+
"Metered License Titles:",
|
|
584
|
+
"Licensed Titles:",
|
|
585
|
+
"Metered Licenses Available:",
|
|
586
|
+
"Metered Licenses Owned:",
|
|
587
|
+
"Open Access Titles:",
|
|
588
|
+
"Unlimited License Titles:",
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
expectPassingTestsForActiveTooltip({
|
|
592
|
+
tooltipProps,
|
|
593
|
+
expectedInventoryItemText,
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
it("should render without per-medium inventory", () => {
|
|
597
|
+
const tooltipProps = populateTooltipProps({
|
|
598
|
+
active: true,
|
|
599
|
+
chartItem: defaultChartItemWithoutPerMediumInventory,
|
|
600
|
+
payload: defaultPayload,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const expectedInventoryItemText = [
|
|
604
|
+
"Titles: 7,974",
|
|
605
|
+
"Available Titles: 7,953",
|
|
606
|
+
"Metered License Titles: 7,974",
|
|
607
|
+
"Licensed Titles: 7,974",
|
|
608
|
+
"Metered Licenses Available: 75,446",
|
|
609
|
+
"Metered Licenses Owned: 301,541",
|
|
610
|
+
"Open Access Titles: 0",
|
|
611
|
+
"Unlimited License Titles: 0",
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
expectPassingTestsForActiveTooltip({
|
|
615
|
+
tooltipProps,
|
|
616
|
+
expectedInventoryItemText,
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
it("should render additional detail with per-medium inventory", () => {
|
|
620
|
+
const tooltipProps = populateTooltipProps({
|
|
621
|
+
active: true,
|
|
622
|
+
chartItem: defaultChartItemWithPerMediumInventory,
|
|
623
|
+
payload: defaultPayload,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const expectedInventoryItemText = [
|
|
627
|
+
"Titles: 7,974 (Audio: 165, Book: 7,809)",
|
|
628
|
+
"Available Titles: 7,953 (Audio: 148, Book: 7,805)",
|
|
629
|
+
"Metered License Titles: 7,974 (Audio: 165, Book: 7,809)",
|
|
630
|
+
"Licensed Titles: 7,974 (Audio: 165, Book: 7,809)",
|
|
631
|
+
"Metered Licenses Available: 75,446 (Audio: 221, Book: 75,225)",
|
|
632
|
+
"Metered Licenses Owned: 301,541 (Audio: 392, Book: 301,149)",
|
|
633
|
+
"Open Access Titles: 0",
|
|
634
|
+
"Unlimited License Titles: 0",
|
|
635
|
+
];
|
|
636
|
+
|
|
637
|
+
expectPassingTestsForActiveTooltip({
|
|
638
|
+
tooltipProps,
|
|
639
|
+
expectedInventoryItemText,
|
|
640
|
+
});
|
|
509
641
|
});
|
|
510
642
|
});
|
|
511
643
|
});
|