@thepalaceproject/circulation-admin 1.43.0-post.9 → 1.44.0-post.3

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.
Files changed (73) hide show
  1. package/CLAUDE.md +22 -10
  2. package/README.md +11 -5
  3. package/dist/circulation-admin.js +1 -1
  4. package/jest.config.js +11 -44
  5. package/package.json +1 -5
  6. package/tests/jest/components/AdvancedSearchBuilder.test.tsx +111 -0
  7. package/tests/jest/components/Announcement.test.tsx +76 -0
  8. package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
  9. package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
  10. package/tests/jest/components/Autocomplete.test.tsx +67 -0
  11. package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
  12. package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
  13. package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
  14. package/tests/jest/components/BookEditForm.test.tsx +352 -0
  15. package/tests/jest/components/BookEditor.test.tsx +20 -21
  16. package/tests/jest/components/CatalogPage.test.tsx +158 -0
  17. package/tests/jest/components/CatalogServices.test.tsx +38 -1
  18. package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
  19. package/tests/jest/components/Classifications.test.tsx +166 -0
  20. package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
  21. package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
  22. package/tests/jest/components/Collections.test.tsx +335 -7
  23. package/tests/jest/components/ColorPicker.test.tsx +58 -0
  24. package/tests/jest/components/ComplaintForm.test.tsx +171 -0
  25. package/tests/jest/components/Complaints.test.tsx +259 -0
  26. package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
  27. package/tests/jest/components/ContextProvider.test.tsx +138 -0
  28. package/tests/jest/components/Contributors.test.tsx +157 -0
  29. package/tests/jest/components/CustomListEditor.test.tsx +367 -0
  30. package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
  31. package/tests/jest/components/CustomLists.test.tsx +621 -4
  32. package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
  33. package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
  34. package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
  35. package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
  36. package/tests/jest/components/EditableConfigList.test.tsx +598 -7
  37. package/tests/jest/components/EditableInput.test.tsx +250 -1
  38. package/tests/jest/components/EditorField.test.tsx +95 -0
  39. package/tests/jest/components/GenreForm.test.tsx +152 -0
  40. package/tests/jest/components/Header.test.tsx +421 -0
  41. package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
  42. package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
  43. package/tests/jest/components/InputList.test.tsx +493 -0
  44. package/tests/jest/components/Lane.test.tsx +132 -10
  45. package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
  46. package/tests/jest/components/LaneEditor.test.tsx +377 -9
  47. package/tests/jest/components/Lanes.test.tsx +446 -0
  48. package/tests/jest/components/LanesSidebar.test.tsx +621 -0
  49. package/tests/jest/components/Libraries.test.tsx +48 -0
  50. package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
  51. package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
  52. package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
  53. package/tests/jest/components/MetadataServices.test.tsx +37 -1
  54. package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
  55. package/tests/jest/components/PairedMenus.test.tsx +220 -0
  56. package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
  57. package/tests/jest/components/ProtocolFormField.test.tsx +443 -7
  58. package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
  59. package/tests/jest/components/SelfTests.test.tsx +601 -0
  60. package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
  61. package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
  62. package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
  63. package/tests/jest/components/Stats.test.tsx +0 -7
  64. package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
  65. package/tests/jest/components/ToolTip.test.tsx +46 -0
  66. package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
  67. package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
  68. package/tests/jest/features/book.test.ts +75 -80
  69. package/tests/jest/fixtures/classificationsData.ts +45 -0
  70. package/tests/jest/fixtures/genreData.ts +811 -0
  71. package/tests/jest/README.md +0 -6
  72. package/tests/jest/legacy-setup.ts +0 -13
  73. package/tests/jest/legacyJsdomEnvironment.js +0 -49
@@ -0,0 +1,458 @@
1
+ import * as React from "react";
2
+ import { render, screen, within, fireEvent } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import ClassificationsForm from "../../../src/components/ClassificationsForm";
6
+ import { BookData } from "../../../src/interfaces";
7
+ import genreData from "../fixtures/genreData";
8
+
9
+ // ClassificationsForm is a plain (unconnected) class component, so a bare
10
+ // `render` is enough — no store/context/router needed.
11
+
12
+ const noValuesBook = (): BookData => ({
13
+ id: "1",
14
+ title: "title",
15
+ audience: undefined,
16
+ targetAgeRange: ["12", "16"],
17
+ fiction: undefined,
18
+ categories: ["Space Opera"],
19
+ });
20
+
21
+ const fullBook = (): BookData => ({
22
+ id: "1",
23
+ title: "title",
24
+ audience: "Young Adult",
25
+ targetAgeRange: ["12", "16"],
26
+ fiction: true,
27
+ categories: ["Space Opera"],
28
+ });
29
+
30
+ type RenderProps = Partial<React.ComponentProps<typeof ClassificationsForm>>;
31
+
32
+ const renderForm = (book: BookData, props: RenderProps = {}) => {
33
+ const editClassifications = jest.fn();
34
+ const result = render(
35
+ <ClassificationsForm
36
+ book={book}
37
+ genreTree={genreData}
38
+ editClassifications={editClassifications}
39
+ {...props}
40
+ />
41
+ );
42
+ return { ...result, editClassifications };
43
+ };
44
+
45
+ const audienceSelect = (container: HTMLElement) =>
46
+ container.querySelector<HTMLSelectElement>("select[name='audience']");
47
+ const fictionRadio = (container: HTMLElement, value: string) =>
48
+ container.querySelector<HTMLInputElement>(
49
+ `input[name='fiction'][value='${value}']`
50
+ );
51
+ const removeButtons = (container: HTMLElement) =>
52
+ Array.from(container.querySelectorAll<HTMLElement>(".with-remove-button"));
53
+
54
+ // Selects a top-level genre in the GenreForm and clicks "Add".
55
+ const addGenre = async (container: HTMLElement, genre: string) => {
56
+ const user = userEvent.setup();
57
+ const option = container.querySelector<HTMLOptionElement>(
58
+ `select[name='genre'] option[value='${genre}']`
59
+ );
60
+ fireEvent.click(option);
61
+ await user.click(screen.getByRole("button", { name: "Add" }));
62
+ };
63
+
64
+ // Selects a genre + subgenre in the GenreForm and clicks "Add".
65
+ const addSubgenre = async (
66
+ container: HTMLElement,
67
+ genre: string,
68
+ subgenre: string
69
+ ) => {
70
+ const user = userEvent.setup();
71
+ fireEvent.click(
72
+ container.querySelector<HTMLOptionElement>(
73
+ `select[name='genre'] option[value='${genre}']`
74
+ )
75
+ );
76
+ fireEvent.click(
77
+ container.querySelector<HTMLOptionElement>(
78
+ `select[name='subgenre'] option[value='${subgenre}']`
79
+ )
80
+ );
81
+ await user.click(screen.getByRole("button", { name: "Add" }));
82
+ };
83
+
84
+ describe("ClassificationsForm", () => {
85
+ let confirmSpy: jest.SpyInstance;
86
+ let alertSpy: jest.SpyInstance;
87
+
88
+ beforeEach(() => {
89
+ confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(true);
90
+ alertSpy = jest.spyOn(window, "alert").mockImplementation(() => undefined);
91
+ });
92
+
93
+ afterEach(() => {
94
+ jest.restoreAllMocks();
95
+ });
96
+
97
+ describe("rendering without classification values", () => {
98
+ it("shows no audience or fiction classification selected", () => {
99
+ const { container } = renderForm(noValuesBook());
100
+
101
+ const audience = audienceSelect(container);
102
+ expect(screen.getByText("Audience")).toBeInTheDocument();
103
+ expect(audience).toHaveValue("None");
104
+ // The "None" option is only present when no audience is selected.
105
+ expect(within(audience).getAllByRole("option")).toHaveLength(7);
106
+
107
+ const none = fictionRadio(container, "none");
108
+ const fiction = fictionRadio(container, "fiction");
109
+ const nonfiction = fictionRadio(container, "nonfiction");
110
+
111
+ expect(none).toHaveAttribute("type", "radio");
112
+ expect(none).toBeChecked();
113
+ expect(fiction).not.toBeChecked();
114
+ expect(nonfiction).not.toBeChecked();
115
+
116
+ const group = container.querySelector(".fiction-radio-input");
117
+ expect(group).toHaveTextContent("None");
118
+ expect(group).toHaveTextContent("Fiction");
119
+ expect(group).toHaveTextContent("Nonfiction");
120
+ });
121
+
122
+ it("does not submit without an audience or fiction classification", async () => {
123
+ const user = userEvent.setup();
124
+ const { container, editClassifications } = renderForm(noValuesBook());
125
+
126
+ await user.click(screen.getByRole("button", { name: "Save" }));
127
+
128
+ expect(editClassifications).not.toHaveBeenCalled();
129
+ expect(audienceSelect(container)).toHaveValue("None");
130
+ });
131
+
132
+ it("renders error messages without an audience or fiction classification", async () => {
133
+ const user = userEvent.setup();
134
+ const { container, editClassifications } = renderForm(noValuesBook());
135
+
136
+ await user.click(screen.getByRole("button", { name: "Save" }));
137
+
138
+ const alert = container.querySelector(".alert-danger");
139
+ expect(alert).toBeInTheDocument();
140
+ expect(alert).toHaveTextContent("No Audience classification selected.");
141
+ expect(alert).toHaveTextContent("No Fiction classification selected.");
142
+ expect(editClassifications).not.toHaveBeenCalled();
143
+ });
144
+
145
+ it("moves focus to the error message so it is announced", () => {
146
+ jest.useFakeTimers();
147
+ // Watch the focus() call itself: the component focuses the alert on a
148
+ // 500ms timer. Use fireEvent (not userEvent, which focuses the target it
149
+ // clicks) so the only focus() we observe is the intended one.
150
+ const focusSpy = jest.spyOn(HTMLElement.prototype, "focus");
151
+ try {
152
+ const { container } = renderForm(noValuesBook());
153
+
154
+ fireEvent.click(screen.getByRole("button", { name: "Save" }));
155
+ expect(focusSpy).not.toHaveBeenCalled();
156
+
157
+ jest.advanceTimersByTime(500);
158
+
159
+ const alert = container.querySelector(".alert-danger");
160
+ expect(focusSpy).toHaveBeenCalledTimes(1);
161
+ expect(focusSpy.mock.instances[0]).toBe(alert);
162
+ } finally {
163
+ focusSpy.mockRestore();
164
+ jest.useRealTimers();
165
+ }
166
+ });
167
+
168
+ it("does not submit if you didn't select an audience", async () => {
169
+ const user = userEvent.setup();
170
+ const { container, editClassifications } = renderForm(noValuesBook());
171
+
172
+ // Choose a fiction classification but leave the audience unset.
173
+ await user.click(fictionRadio(container, "fiction"));
174
+ await user.click(screen.getByRole("button", { name: "Save" }));
175
+
176
+ expect(editClassifications).not.toHaveBeenCalled();
177
+ expect(container.querySelector(".alert-danger")).toBeInTheDocument();
178
+
179
+ // Now choose an audience and the form submits, clearing the error.
180
+ await user.selectOptions(audienceSelect(container), "Adult");
181
+ await user.click(screen.getByRole("button", { name: "Save" }));
182
+
183
+ expect(editClassifications).toHaveBeenCalledTimes(1);
184
+ expect(container.querySelector(".alert-danger")).not.toBeInTheDocument();
185
+ });
186
+
187
+ it("does not submit if you didn't select a fiction classification", async () => {
188
+ const user = userEvent.setup();
189
+ const { container, editClassifications } = renderForm(noValuesBook());
190
+
191
+ // Choose an audience but leave the fiction classification unset.
192
+ await user.selectOptions(audienceSelect(container), "Adult");
193
+ await user.click(screen.getByRole("button", { name: "Save" }));
194
+
195
+ expect(editClassifications).not.toHaveBeenCalled();
196
+ expect(fictionRadio(container, "nonfiction")).not.toBeChecked();
197
+
198
+ // Now choose a fiction classification and the form submits.
199
+ await user.click(fictionRadio(container, "nonfiction"));
200
+ await user.click(screen.getByRole("button", { name: "Save" }));
201
+
202
+ expect(editClassifications).toHaveBeenCalledTimes(1);
203
+ });
204
+ });
205
+
206
+ describe("rendering", () => {
207
+ it("shows an editable select with audience options", () => {
208
+ const { container } = renderForm(fullBook());
209
+
210
+ const audience = audienceSelect(container);
211
+ expect(screen.getByText("Audience")).toBeInTheDocument();
212
+ expect(audience).toHaveValue("Young Adult");
213
+ // The "None" option is not rendered once an audience is selected.
214
+ expect(within(audience).getAllByRole("option")).toHaveLength(6);
215
+ });
216
+
217
+ it("shows editable inputs with min and max target age", () => {
218
+ const { container } = renderForm(fullBook());
219
+
220
+ expect(
221
+ container.querySelector("input[name='target_age_min']")
222
+ ).toHaveValue("12");
223
+ expect(
224
+ container.querySelector("input[name='target_age_max']")
225
+ ).toHaveValue("16");
226
+ });
227
+
228
+ it("shows editable radio buttons with fiction status", () => {
229
+ const { container } = renderForm(fullBook());
230
+
231
+ // With a fiction value set, the "None" fiction radio is not rendered.
232
+ expect(fictionRadio(container, "none")).toBeNull();
233
+ expect(fictionRadio(container, "fiction")).toBeChecked();
234
+ expect(fictionRadio(container, "nonfiction")).not.toBeChecked();
235
+
236
+ const group = container.querySelector(".fiction-radio-input");
237
+ expect(group).toHaveTextContent("Fiction");
238
+ expect(group).toHaveTextContent("Nonfiction");
239
+ });
240
+
241
+ it("shows the book's full genres with remove buttons", () => {
242
+ const { container } = renderForm(fullBook());
243
+
244
+ const buttons = removeButtons(container);
245
+ expect(buttons).toHaveLength(1);
246
+ expect(buttons[0]).toHaveTextContent("Science Fiction > Space Opera");
247
+ expect(
248
+ within(buttons[0]).getByRole("button", { name: /Delete/ })
249
+ ).toBeInTheDocument();
250
+ });
251
+
252
+ it("shows the book's full genres even if inconsistent with fiction status", () => {
253
+ const editClassifications = jest.fn();
254
+ const { container, rerender } = render(
255
+ <ClassificationsForm
256
+ book={fullBook()}
257
+ genreTree={genreData}
258
+ editClassifications={editClassifications}
259
+ />
260
+ );
261
+
262
+ rerender(
263
+ <ClassificationsForm
264
+ book={{ ...fullBook(), fiction: false }}
265
+ genreTree={genreData}
266
+ editClassifications={editClassifications}
267
+ />
268
+ );
269
+
270
+ const buttons = removeButtons(container);
271
+ expect(buttons).toHaveLength(1);
272
+ expect(buttons[0]).toHaveTextContent("Science Fiction > Space Opera");
273
+ });
274
+
275
+ it("shows a genre form", () => {
276
+ const { container } = renderForm(fullBook());
277
+
278
+ const genreSelect = container.querySelector("select[name='genre']");
279
+ expect(genreSelect).toBeInTheDocument();
280
+ expect(genreSelect).not.toBeDisabled();
281
+ expect(screen.getByText("Add Genre")).toBeInTheDocument();
282
+ });
283
+
284
+ it("shows a submit button", () => {
285
+ renderForm(fullBook());
286
+ expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument();
287
+ });
288
+ });
289
+
290
+ describe("behavior", () => {
291
+ it("shows and hides target age inputs when audience changes", async () => {
292
+ const user = userEvent.setup();
293
+ const { container } = renderForm(fullBook());
294
+
295
+ expect(
296
+ container.querySelector("input[name='target_age_min']")
297
+ ).toBeInTheDocument();
298
+ expect(
299
+ container.querySelector("input[name='target_age_max']")
300
+ ).toBeInTheDocument();
301
+
302
+ await user.selectOptions(audienceSelect(container), "Adult");
303
+ expect(
304
+ container.querySelector("input[name='target_age_min']")
305
+ ).not.toBeInTheDocument();
306
+ expect(
307
+ container.querySelector("input[name='target_age_max']")
308
+ ).not.toBeInTheDocument();
309
+
310
+ await user.selectOptions(audienceSelect(container), "Children");
311
+ expect(
312
+ container.querySelector("input[name='target_age_min']")
313
+ ).toBeInTheDocument();
314
+ expect(
315
+ container.querySelector("input[name='target_age_max']")
316
+ ).toBeInTheDocument();
317
+ });
318
+
319
+ it("changes both fiction status radio buttons", async () => {
320
+ const user = userEvent.setup();
321
+ const { container } = renderForm(fullBook());
322
+
323
+ expect(fictionRadio(container, "fiction")).toBeChecked();
324
+ expect(fictionRadio(container, "nonfiction")).not.toBeChecked();
325
+
326
+ // Switching to nonfiction prompts for confirmation and clears the
327
+ // (fiction) genre.
328
+ await user.click(fictionRadio(container, "nonfiction"));
329
+ expect(confirmSpy).toHaveBeenCalled();
330
+ expect(removeButtons(container)).toHaveLength(0);
331
+ expect(fictionRadio(container, "fiction")).not.toBeChecked();
332
+ expect(fictionRadio(container, "nonfiction")).toBeChecked();
333
+
334
+ await user.click(fictionRadio(container, "fiction"));
335
+ expect(fictionRadio(container, "fiction")).toBeChecked();
336
+ expect(fictionRadio(container, "nonfiction")).not.toBeChecked();
337
+ });
338
+
339
+ it("adds a genre only after validating it against the audience", async () => {
340
+ const { container } = renderForm(fullBook());
341
+
342
+ expect(removeButtons(container)).toHaveLength(1);
343
+
344
+ // Erotica is not allowed for a Young Adult audience: it is rejected with
345
+ // an alert and not added.
346
+ await addGenre(container, "Erotica");
347
+ expect(alertSpy).toHaveBeenCalled();
348
+ let buttons = removeButtons(container);
349
+ expect(buttons).toHaveLength(1);
350
+ expect(buttons[0]).not.toHaveTextContent("Erotica");
351
+
352
+ // Folklore is allowed and gets added.
353
+ await addGenre(container, "Folklore");
354
+ buttons = removeButtons(container);
355
+ expect(buttons).toHaveLength(2);
356
+ expect(buttons.some((b) => b.textContent.includes("Folklore"))).toBe(
357
+ true
358
+ );
359
+ });
360
+
361
+ it("removes a genre when its remove button is clicked", async () => {
362
+ const user = userEvent.setup();
363
+ const { container } = renderForm(fullBook());
364
+
365
+ const button = removeButtons(container)[0];
366
+ await user.click(within(button).getByRole("button", { name: /Delete/ }));
367
+
368
+ expect(removeButtons(container)).toHaveLength(0);
369
+ });
370
+
371
+ it("submits data when the submit button is clicked", async () => {
372
+ const user = userEvent.setup();
373
+ const { editClassifications } = renderForm(fullBook());
374
+
375
+ await user.click(screen.getByRole("button", { name: "Save" }));
376
+
377
+ expect(editClassifications).toHaveBeenCalledTimes(1);
378
+ const data = editClassifications.mock.calls[0][0] as FormData;
379
+ expect(data.get("audience")).toBe("Young Adult");
380
+ expect(data.get("target_age_min")).toBe("12");
381
+ expect(data.get("target_age_max")).toBe("16");
382
+ expect(data.get("fiction")).toBe("fiction");
383
+ expect(data.getAll("genres")).toEqual(["Space Opera"]);
384
+ });
385
+
386
+ it("updates state upon receiving new state-related props", () => {
387
+ const editClassifications = jest.fn();
388
+ const { container, rerender } = render(
389
+ <ClassificationsForm
390
+ book={fullBook()}
391
+ genreTree={genreData}
392
+ editClassifications={editClassifications}
393
+ />
394
+ );
395
+
396
+ rerender(
397
+ <ClassificationsForm
398
+ book={{
399
+ ...fullBook(),
400
+ audience: "Adult",
401
+ fiction: false,
402
+ categories: ["Cooking"],
403
+ }}
404
+ genreTree={genreData}
405
+ editClassifications={editClassifications}
406
+ />
407
+ );
408
+
409
+ expect(audienceSelect(container)).toHaveValue("Adult");
410
+ expect(fictionRadio(container, "nonfiction")).toBeChecked();
411
+ expect(fictionRadio(container, "fiction")).not.toBeChecked();
412
+
413
+ const buttons = removeButtons(container);
414
+ expect(buttons).toHaveLength(1);
415
+ expect(buttons[0]).toHaveTextContent("Food & Health > Cooking");
416
+ });
417
+
418
+ it("doesn't reset user edits upon receiving new state-unrelated props", async () => {
419
+ const user = userEvent.setup();
420
+ const editClassifications = jest.fn();
421
+ const { container, rerender } = render(
422
+ <ClassificationsForm
423
+ book={fullBook()}
424
+ genreTree={genreData}
425
+ editClassifications={editClassifications}
426
+ />
427
+ );
428
+
429
+ // The user switches to nonfiction (clearing the fiction genre) and adds a
430
+ // nonfiction genre.
431
+ await user.click(fictionRadio(container, "nonfiction"));
432
+ expect(fictionRadio(container, "nonfiction")).toBeChecked();
433
+ expect(removeButtons(container)).toHaveLength(0);
434
+
435
+ await addSubgenre(container, "Food & Health", "Cooking");
436
+ expect(removeButtons(container)).toHaveLength(1);
437
+ expect(container.querySelector(".with-remove-button")).toHaveTextContent(
438
+ "Food & Health > Cooking"
439
+ );
440
+
441
+ // A re-render caused by an unrelated prop change (disabling the form on
442
+ // submit) must not discard those edits.
443
+ rerender(
444
+ <ClassificationsForm
445
+ book={fullBook()}
446
+ genreTree={genreData}
447
+ editClassifications={editClassifications}
448
+ disabled={true}
449
+ />
450
+ );
451
+
452
+ expect(fictionRadio(container, "nonfiction")).toBeChecked();
453
+ expect(container.querySelector(".with-remove-button")).toHaveTextContent(
454
+ "Food & Health > Cooking"
455
+ );
456
+ });
457
+ });
458
+ });
@@ -0,0 +1,57 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ import ClassificationsTable from "../../../src/components/ClassificationsTable";
5
+ import classificationsData from "../fixtures/classificationsData";
6
+
7
+ // Mirrors ClassificationsTable's own `readableType`: it strips the genres URL
8
+ // prefix so the displayed type is human-readable.
9
+ const readableType = (type: string) =>
10
+ type.replace(
11
+ /http:\/\/librarysimplified\.org\/terms\/genres\/([^/]+)\//,
12
+ "$1"
13
+ );
14
+
15
+ describe("ClassificationsTable", () => {
16
+ describe("rendering", () => {
17
+ it("should show header", () => {
18
+ render(<ClassificationsTable classifications={classificationsData} />);
19
+
20
+ expect(
21
+ screen.getByRole("heading", {
22
+ level: 3,
23
+ name: "Related Classifications",
24
+ })
25
+ ).toBeInTheDocument();
26
+ });
27
+
28
+ it("should show column headers", () => {
29
+ render(<ClassificationsTable classifications={classificationsData} />);
30
+
31
+ const headers = screen.getAllByRole("columnheader");
32
+ expect(headers.map((header) => header.textContent)).toStrictEqual([
33
+ "Type",
34
+ "Name",
35
+ "Source",
36
+ "Weight",
37
+ ]);
38
+ });
39
+
40
+ it("should show one row for each classification", () => {
41
+ const { container } = render(
42
+ <ClassificationsTable classifications={classificationsData} />
43
+ );
44
+
45
+ const rows = container.querySelectorAll("tbody tr");
46
+ expect(rows).toHaveLength(classificationsData.length);
47
+ rows.forEach((row, i) => {
48
+ const cells = row.querySelectorAll("td");
49
+ const c = classificationsData[i];
50
+ expect(cells[0].textContent).toBe(readableType(c.type));
51
+ expect(cells[1].textContent).toBe(c.name);
52
+ expect(cells[2].textContent).toBe(c.source);
53
+ expect(parseInt(cells[3].textContent, 10)).toBe(c.weight);
54
+ });
55
+ });
56
+ });
57
+ });