@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
- const assertLoadingState = ({ getByRole }) => {
161
- getByRole("dialog", { name: "Loading" });
162
- getByRole("heading", { level: 1, name: "Loading" });
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
- it("shows/hides the loading indicator", async () => {
175
- // We haven't tried to fetch anything yet.
176
- expect(fetchMock.calls()).toHaveLength(0);
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
- const { rerender, getByRole, queryByRole } = renderWithProviders(
179
- <Stats />
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
- // We should start in the loading state.
183
- assertLoadingState({ getByRole });
188
+ const { rerender, getByRole, queryByRole } = renderWithProviders(
189
+ <Stats />
190
+ );
184
191
 
185
- // Wait a tick for the statistics to render.
186
- await new Promise(process.nextTick);
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
- rerender(<Stats />);
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
- // We should show our content without the loading state.
193
- assertNotLoadingState({ queryByRole });
194
- getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
200
+ rerender(<Stats />);
195
201
 
196
- // We haven't made another call, since the response is cached.
197
- expect(fetchMock.calls()).toHaveLength(1);
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
- it("doesn't fetch again, because response is cached", async () => {
201
- const { getByRole, queryByRole } = renderWithProviders(<Stats />);
206
+ // We haven't made another call, since the response is cached.
207
+ expect(fetchMock.calls()).toHaveLength(1);
208
+ });
202
209
 
203
- // We should show our content immediately, without entering the loading state.
204
- assertNotLoadingState({ queryByRole });
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
- // We never tried to fetch anything because the result is cached.
208
- expect(fetchMock.calls()).toHaveLength(0);
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
- it("show stats for a library, if a library is specified", async () => {
212
- const { getByRole, queryByRole, getByText } = renderWithProviders(
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
- it("shows site-wide stats when no library specified", async () => {
230
- const { getByRole, getByText, queryByRole } = renderWithProviders(
231
- <Stats />
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
- // We should show our content immediately, without entering the loading state.
235
- assertNotLoadingState({ queryByRole });
239
+ it("shows site-wide stats when no library specified", async () => {
240
+ const { getByRole, getByText, queryByRole } = renderWithProviders(
241
+ <Stats />
242
+ );
236
243
 
237
- getByRole("heading", { level: 2, name: ALL_LIBRARIES_HEADING });
238
- getByRole("heading", { level: 3, name: "Current Circulation Activity" });
239
- getByText("1.6k");
244
+ // We should show our content immediately, without entering the loading state.
245
+ assertNotLoadingState({ queryByRole });
240
246
 
241
- // We never tried to fetch anything because the result is cached.
242
- expect(fetchMock.calls()).toHaveLength(0);
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
- describe("requesting inventory reports", () => {
247
- // Convert from the API format to our in-app format.
248
- const statisticsData = normalizeStatistics(statisticsApiResponseData);
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
- describe("charting - custom tooltip", () => {
298
- const defaultLabel = "Collection X";
299
- const summaryInventory = {
300
- availableTitles: 7953,
301
- licensedTitles: 7974,
302
- meteredLicenseTitles: 7974,
303
- meteredLicensesAvailable: 75446,
304
- meteredLicensesOwned: 301541,
305
- openAccessTitles: 0,
306
- titles: 7974,
307
- unlimitedLicenseTitles: 0,
308
- };
309
- const perMediumInventory = {
310
- Audio: {
311
- availableTitles: 148,
312
- licensedTitles: 165,
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
- // The heading should be at the top and the divider (`hr`)
419
- // should be between the third and fourth statistics.
420
- expect(detailChildren).toHaveLength(10);
421
- expect(heading).toEqual(detailChildren[0]);
422
- expect(items[0]).toEqual(detailChildren[1]);
423
- expect(items[2]).toEqual(detailChildren[3]);
424
- expect(divider).toEqual(detailChildren[4]);
425
- expect(items[3]).toEqual(detailChildren[5]);
426
- expect(items[7]).toEqual(detailChildren[9]);
427
- };
428
-
429
- it("should not render when active is false", () => {
430
- // Recharts sticks some extra props
431
- const tooltipProps = populateTooltipProps({
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
- const expectedInventoryItemText = [
450
- "Titles:",
451
- "Available Titles:",
452
- "Metered License Titles:",
453
- "Licensed Titles:",
454
- "Metered Licenses Available:",
455
- "Metered Licenses Owned:",
456
- "Open Access Titles:",
457
- "Unlimited License Titles:",
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
- expectPassingTestsForActiveTooltip({
461
- tooltipProps,
462
- expectedInventoryItemText,
463
- });
464
- });
465
- it("should render without per-medium inventory", () => {
466
- const tooltipProps = populateTooltipProps({
467
- active: true,
468
- chartItem: defaultChartItemWithoutPerMediumInventory,
469
- payload: defaultPayload,
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
- const expectedInventoryItemText = [
473
- "Titles: 7,974",
474
- "Available Titles: 7,953",
475
- "Metered License Titles: 7,974",
476
- "Licensed Titles: 7,974",
477
- "Metered Licenses Available: 75,446",
478
- "Metered Licenses Owned: 301,541",
479
- "Open Access Titles: 0",
480
- "Unlimited License Titles: 0",
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
- expectPassingTestsForActiveTooltip({
484
- tooltipProps,
485
- expectedInventoryItemText,
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
- const expectedInventoryItemText = [
496
- "Titles: 7,974 (Audio: 165, Book: 7,809)",
497
- "Available Titles: 7,953 (Audio: 148, Book: 7,805)",
498
- "Metered License Titles: 7,974 (Audio: 165, Book: 7,809)",
499
- "Licensed Titles: 7,974 (Audio: 165, Book: 7,809)",
500
- "Metered Licenses Available: 75,446 (Audio: 221, Book: 75,225)",
501
- "Metered Licenses Owned: 301,541 (Audio: 392, Book: 301,149)",
502
- "Open Access Titles: 0",
503
- "Unlimited License Titles: 0",
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
- expectPassingTestsForActiveTooltip({
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
  });