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

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,352 @@
1
+ import * as React from "react";
2
+ import { installFormDataShim } from "../testUtils/formDataShim";
3
+ import { render, screen, waitFor, within } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+
6
+ import BookEditForm from "../../../src/components/BookEditForm";
7
+ import {
8
+ BookData,
9
+ RolesData,
10
+ MediaData,
11
+ LanguagesData,
12
+ } from "../../../src/interfaces";
13
+ installFormDataShim();
14
+
15
+ describe("BookEditForm", () => {
16
+ const roles: RolesData = {
17
+ aut: "Author",
18
+ nar: "Narrator",
19
+ };
20
+
21
+ const media: MediaData = {
22
+ "http://schema.org/AudioObject": "Audio",
23
+ "http://schema.org/Book": "Book",
24
+ };
25
+
26
+ const languages: LanguagesData = {
27
+ eng: ["English"],
28
+ spa: ["Spanish", "Castilian"],
29
+ };
30
+
31
+ const bookData: BookData = {
32
+ id: "id",
33
+ title: "title",
34
+ subtitle: "subtitle",
35
+ authors: [{ name: "An Author", role: "aut" }],
36
+ contributors: [
37
+ { name: "A Narrator", role: "nar" },
38
+ { name: "Another Narrator", role: "nar" },
39
+ ],
40
+ fiction: true,
41
+ audience: "Young Adult",
42
+ targetAgeRange: ["12", "16"],
43
+ summary: "summary",
44
+ series: "series",
45
+ seriesPosition: 3,
46
+ medium: "http://schema.org/AudioObject",
47
+ language: "eng",
48
+ publisher: "publisher",
49
+ imprint: "imprint",
50
+ issued: "2017-04-03",
51
+ rating: 4,
52
+ editLink: {
53
+ href: "href",
54
+ rel: "edit",
55
+ },
56
+ };
57
+
58
+ type Props = React.ComponentProps<typeof BookEditForm>;
59
+
60
+ const makeProps = (overrides: Partial<Props> = {}): Props => ({
61
+ ...(bookData as Props),
62
+ roles,
63
+ media,
64
+ languages,
65
+ disabled: false,
66
+ refresh: jest.fn(),
67
+ editBook: jest.fn().mockResolvedValue(undefined),
68
+ ...overrides,
69
+ });
70
+
71
+ const renderForm = (overrides: Partial<Props> = {}) => {
72
+ const props = makeProps(overrides);
73
+ const result = render(<BookEditForm {...props} />);
74
+ const rerender = (nextOverrides: Partial<Props>) =>
75
+ result.rerender(<BookEditForm {...makeProps(nextOverrides)} />);
76
+ return { ...result, rerender, props };
77
+ };
78
+
79
+ const inputsByName = (container: HTMLElement, name: string) =>
80
+ Array.from(
81
+ container.querySelectorAll<HTMLInputElement>(`input[name="${name}"]`)
82
+ );
83
+ const selectsByName = (container: HTMLElement, name: string) =>
84
+ Array.from(
85
+ container.querySelectorAll<HTMLSelectElement>(`select[name="${name}"]`)
86
+ );
87
+
88
+ describe("rendering", () => {
89
+ it("shows editable input with title", () => {
90
+ const { container } = renderForm();
91
+ expect(screen.getByText("Title")).toBeInTheDocument();
92
+ expect(container.querySelector('input[name="title"]')).toHaveValue(
93
+ "title"
94
+ );
95
+ });
96
+
97
+ it("shows editable input with subtitle", () => {
98
+ const { container } = renderForm();
99
+ expect(screen.getByText("Subtitle")).toBeInTheDocument();
100
+ expect(container.querySelector('input[name="subtitle"]')).toHaveValue(
101
+ "subtitle"
102
+ );
103
+ });
104
+
105
+ it("shows authors and contributors", () => {
106
+ const { container } = renderForm();
107
+
108
+ const names = inputsByName(container, "contributor-name");
109
+ const contributorRoles = selectsByName(container, "contributor-role");
110
+ expect(names).toHaveLength(4);
111
+ expect(contributorRoles).toHaveLength(4);
112
+
113
+ expect(names[0]).toHaveValue("An Author");
114
+ expect(contributorRoles[0]).toHaveValue("Author");
115
+ expect(names[1]).toHaveValue("A Narrator");
116
+ expect(contributorRoles[1]).toHaveValue("Narrator");
117
+ expect(names[2]).toHaveValue("Another Narrator");
118
+ expect(contributorRoles[2]).toHaveValue("Narrator");
119
+
120
+ // The last inputs are for adding a new contributor.
121
+ expect(names[3]).toHaveValue("");
122
+ expect(contributorRoles[3]).toHaveValue("Author");
123
+ expect(
124
+ container.querySelector("button.add-contributor")
125
+ ).toBeInTheDocument();
126
+
127
+ // Existing authors and contributors are removable.
128
+ expect(
129
+ container.querySelectorAll(".with-remove-button .remove-btn")
130
+ ).toHaveLength(3);
131
+
132
+ // All role inputs have the same options.
133
+ [contributorRoles[1], contributorRoles[3]].forEach((select) => {
134
+ const options = within(select).getAllByRole("option");
135
+ expect(options).toHaveLength(2);
136
+ expect(options[0]).toHaveValue("Author");
137
+ expect(options[1]).toHaveValue("Narrator");
138
+ });
139
+ });
140
+
141
+ it("shows editable input with series", () => {
142
+ const { container } = renderForm();
143
+ expect(container.querySelector('input[name="series"]')).toHaveValue(
144
+ "series"
145
+ );
146
+ });
147
+
148
+ it("shows editable input with series position", () => {
149
+ const { container } = renderForm();
150
+ expect(
151
+ container.querySelector('input[name="series_position"]')
152
+ ).toHaveValue("3");
153
+ });
154
+
155
+ it("shows editable input with medium", () => {
156
+ const { container } = renderForm();
157
+ expect(screen.getByText("Medium")).toBeInTheDocument();
158
+ expect(container.querySelector('select[name="medium"]')).toHaveValue(
159
+ "Audio"
160
+ );
161
+ });
162
+
163
+ it("shows a language field", () => {
164
+ const { container, rerender } = renderForm();
165
+ expect(screen.getByText("Language")).toBeInTheDocument();
166
+ expect(container.querySelector('input[name="language"]')).toHaveValue(
167
+ "English"
168
+ );
169
+
170
+ rerender({ language: "fre" });
171
+ expect(container.querySelector('input[name="language"]')).toHaveValue(
172
+ "fre"
173
+ );
174
+ });
175
+
176
+ it("shows editable input with publisher", () => {
177
+ const { container } = renderForm();
178
+ expect(screen.getByText("Publisher")).toBeInTheDocument();
179
+ expect(container.querySelector('input[name="publisher"]')).toHaveValue(
180
+ "publisher"
181
+ );
182
+ });
183
+
184
+ it("shows editable input with imprint", () => {
185
+ const { container } = renderForm();
186
+ expect(screen.getByText("Imprint")).toBeInTheDocument();
187
+ expect(container.querySelector('input[name="imprint"]')).toHaveValue(
188
+ "imprint"
189
+ );
190
+ });
191
+
192
+ it("shows editable input with publication date", () => {
193
+ const { container } = renderForm();
194
+ expect(screen.getByText("Publication Date")).toBeInTheDocument();
195
+ expect(container.querySelector('input[name="issued"]')).toHaveValue(
196
+ "2017-04-03"
197
+ );
198
+ });
199
+
200
+ it("shows editable input with rating", () => {
201
+ const { container } = renderForm();
202
+ expect(screen.getByText(/Rating/)).toBeInTheDocument();
203
+ const rating = container.querySelector<HTMLInputElement>(
204
+ 'input[name="rating"]'
205
+ );
206
+ expect(rating).toHaveValue(4);
207
+ });
208
+
209
+ it("shows editable textarea with summary", () => {
210
+ const { container } = renderForm();
211
+ const editor = container.querySelector<HTMLElement>(".editor");
212
+ expect(within(editor).getByText("Summary")).toBeInTheDocument();
213
+ expect(editor.querySelector(".DraftEditor-root")).toHaveTextContent(
214
+ "summary"
215
+ );
216
+ });
217
+ });
218
+
219
+ it("removes a contributor", async () => {
220
+ const user = userEvent.setup();
221
+ const { container } = renderForm();
222
+
223
+ let removeButtons = screen.getAllByRole("button", { name: "Delete" });
224
+ expect(removeButtons).toHaveLength(3);
225
+
226
+ // Remove the first narrator (index 1: An Author, A Narrator, Another Narrator).
227
+ await user.click(removeButtons[1]);
228
+
229
+ removeButtons = screen.getAllByRole("button", { name: "Delete" });
230
+ expect(removeButtons).toHaveLength(2);
231
+
232
+ const names = inputsByName(container, "contributor-name");
233
+ const contributorRoles = selectsByName(container, "contributor-role");
234
+ expect(names).toHaveLength(3);
235
+ expect(contributorRoles).toHaveLength(3);
236
+ expect(names[0]).toHaveValue("An Author");
237
+ expect(contributorRoles[0]).toHaveValue("Author");
238
+ expect(names[1]).toHaveValue("Another Narrator");
239
+ expect(contributorRoles[1]).toHaveValue("Narrator");
240
+ expect(names[2]).toHaveValue("");
241
+ expect(contributorRoles[2]).toHaveValue("Author");
242
+ });
243
+
244
+ it("adds a contributor", async () => {
245
+ const user = userEvent.setup();
246
+ const { container } = renderForm();
247
+
248
+ let names = inputsByName(container, "contributor-name");
249
+ let contributorRoles = selectsByName(container, "contributor-role");
250
+ expect(names).toHaveLength(4);
251
+ expect(contributorRoles).toHaveLength(4);
252
+
253
+ // Fill in the empty "add" name field (the last one); its role defaults to
254
+ // Author. Typing enables the Add button.
255
+ await user.type(names[3], "New Author");
256
+ await user.click(container.querySelector("button.add-contributor"));
257
+
258
+ names = inputsByName(container, "contributor-name");
259
+ contributorRoles = selectsByName(container, "contributor-role");
260
+ expect(names).toHaveLength(5);
261
+ expect(contributorRoles).toHaveLength(5);
262
+
263
+ expect(names[0]).toHaveValue("An Author");
264
+ expect(contributorRoles[0]).toHaveValue("Author");
265
+ expect(names[1]).toHaveValue("A Narrator");
266
+ expect(contributorRoles[1]).toHaveValue("Narrator");
267
+ expect(names[2]).toHaveValue("Another Narrator");
268
+ expect(contributorRoles[2]).toHaveValue("Narrator");
269
+ expect(names[3]).toHaveValue("New Author");
270
+ expect(contributorRoles[3]).toHaveValue("Author");
271
+ expect(names[4]).toHaveValue("");
272
+ expect(contributorRoles[4]).toHaveValue("Author");
273
+ });
274
+
275
+ it("calls editBook on submit", async () => {
276
+ const user = userEvent.setup();
277
+ const editBook = jest.fn().mockResolvedValue(undefined);
278
+ renderForm({ editBook });
279
+
280
+ await user.click(screen.getByRole("button", { name: "Submit" }));
281
+
282
+ expect(editBook).toHaveBeenCalledTimes(1);
283
+ const [url, data] = editBook.mock.calls[0];
284
+ expect(url).toBe("href");
285
+ expect(data.get("title")).toBe(bookData.title);
286
+ expect(data.get("subtitle")).toBe(bookData.subtitle);
287
+
288
+ // The last contributor field is the empty one for adding a new contributor.
289
+ // If the user had filled it in without clicking "Add", it would still be
290
+ // submitted.
291
+ expect(data.getAll("contributor-name")).toStrictEqual([
292
+ "An Author",
293
+ "A Narrator",
294
+ "Another Narrator",
295
+ "",
296
+ ]);
297
+ expect(data.getAll("contributor-role")).toStrictEqual([
298
+ "Author",
299
+ "Narrator",
300
+ "Narrator",
301
+ "Author",
302
+ ]);
303
+
304
+ expect(data.get("series")).toBe(bookData.series);
305
+ expect(data.get("series_position")).toBe(String(bookData.seriesPosition));
306
+ expect(data.get("medium")).toBe("Audio");
307
+ expect(data.get("language")).toBe("English");
308
+ expect(data.get("publisher")).toBe(bookData.publisher);
309
+ expect(data.get("imprint")).toBe(bookData.imprint);
310
+ expect(data.get("issued")).toBe(bookData.issued);
311
+ expect(data.get("rating")).toBe("4");
312
+ expect(data.get("summary")).toContain(bookData.summary);
313
+ });
314
+
315
+ it("refreshes book after editing", async () => {
316
+ const user = userEvent.setup();
317
+ const editBook = jest.fn().mockResolvedValue(undefined);
318
+ const refresh = jest.fn().mockResolvedValue(undefined);
319
+ renderForm({ editBook, refresh });
320
+
321
+ await user.click(screen.getByRole("button", { name: "Submit" }));
322
+
323
+ await waitFor(() => expect(refresh).toHaveBeenCalledTimes(1));
324
+ });
325
+
326
+ it("only adds updated summary content if it's not empty", async () => {
327
+ const user = userEvent.setup();
328
+ const editBook = jest.fn().mockResolvedValue(undefined);
329
+ const refresh = jest.fn().mockResolvedValue(undefined);
330
+ // Since we are adding an empty string, there should be no `summary` value
331
+ // in the FormData object that gets passed to the `editBook` function.
332
+ renderForm({
333
+ editBook,
334
+ refresh,
335
+ summary: "<p></p>",
336
+ editLink: { href: "test-url", rel: "something" },
337
+ });
338
+
339
+ await user.click(screen.getByRole("button", { name: "Submit" }));
340
+
341
+ await waitFor(() => expect(editBook).toHaveBeenCalledTimes(1));
342
+ expect(editBook.mock.calls[0][1].get("summary")).toBeNull();
343
+ });
344
+
345
+ it("disables all inputs", () => {
346
+ const { container } = renderForm({ disabled: true });
347
+
348
+ container
349
+ .querySelectorAll("input, select")
350
+ .forEach((input) => expect(input).toBeDisabled());
351
+ });
352
+ });
@@ -7,7 +7,6 @@ import {
7
7
  PER_LIBRARY_UNSUPPRESS_REL,
8
8
  } from "../../../src/features/book/bookEditorSlice";
9
9
  import { BookDetailsEditor } from "../../../src/components/BookDetailsEditor";
10
- import { expect } from "chai";
11
10
  import { store } from "../../../src/store";
12
11
  import * as fetchMock from "fetch-mock-jest";
13
12
 
@@ -93,8 +92,8 @@ describe("BookDetails", () => {
93
92
  const hideButton = queryByRole("button", { name: "Hide" });
94
93
  const restoreButton = queryByRole("button", { name: "Restore" });
95
94
 
96
- expect(hideButton).to.be.null;
97
- expect(restoreButton).to.be.null;
95
+ expect(hideButton).toBeNull();
96
+ expect(restoreButton).toBeNull();
98
97
  });
99
98
 
100
99
  it("don't show restore button if not a library's admin", () => {
@@ -110,8 +109,8 @@ describe("BookDetails", () => {
110
109
  const hideButton = queryByRole("button", { name: "Hide" });
111
110
  const restoreButton = queryByRole("button", { name: "Restore" });
112
111
 
113
- expect(hideButton).to.be.null;
114
- expect(restoreButton).to.be.null;
112
+ expect(hideButton).toBeNull();
113
+ expect(restoreButton).toBeNull();
115
114
  });
116
115
 
117
116
  it("uses modal for suppress book confirmation", async () => {
@@ -149,8 +148,8 @@ describe("BookDetails", () => {
149
148
  await user.click(cancelButton);
150
149
  confirmButton = queryByRole("button", { name: "Suppress Availability" });
151
150
  cancelButton = queryByRole("button", { name: "Cancel" });
152
- expect(confirmButton).to.be.null;
153
- expect(cancelButton).to.be.null;
151
+ expect(confirmButton).toBeNull();
152
+ expect(cancelButton).toBeNull();
154
153
 
155
154
  // Clicking `Hide` again should show the modal again.
156
155
  await user.click(hideButton);
@@ -163,15 +162,15 @@ describe("BookDetails", () => {
163
162
  getByRole("button", { name: "Dismiss" });
164
163
 
165
164
  // Check that the API was invoked.
166
- expect(suppressBook.mock.calls.length).to.equal(1);
167
- expect(suppressBook.mock.calls[0][0]).to.equal("/suppress/href");
165
+ expect(suppressBook.mock.calls.length).toBe(1);
166
+ expect(suppressBook.mock.calls[0][0]).toBe("/suppress/href");
168
167
  const fetchCalls = fetchMock.calls();
169
- expect(fetchCalls.length).to.equal(1);
168
+ expect(fetchCalls.length).toBe(1);
170
169
  const fetchCall = fetchCalls[0];
171
170
  const fetchOptions = fetchCalls[0][1];
172
- expect(fetchCall[0]).to.equal("/suppress/href");
173
- expect(fetchOptions["headers"]["X-CSRF-Token"]).to.contain("token");
174
- expect(fetchOptions["method"]).to.equal("POST");
171
+ expect(fetchCall[0]).toBe("/suppress/href");
172
+ expect(fetchOptions["headers"]["X-CSRF-Token"]).toContain("token");
173
+ expect(fetchOptions["method"]).toBe("POST");
175
174
  });
176
175
  it("uses modal for unsuppress book confirmation", async () => {
177
176
  // Configure standard constructors so that RTK Query works in tests with FetchMockJest
@@ -213,8 +212,8 @@ describe("BookDetails", () => {
213
212
  await user.click(cancelButton);
214
213
  confirmButton = queryByRole("button", { name: "Restore Availability" });
215
214
  cancelButton = queryByRole("button", { name: "Cancel" });
216
- expect(confirmButton).to.be.null;
217
- expect(cancelButton).to.be.null;
215
+ expect(confirmButton).toBeNull();
216
+ expect(cancelButton).toBeNull();
218
217
 
219
218
  // Clicking `Restore` again should show the modal again.
220
219
  await user.click(restoreButton);
@@ -227,14 +226,14 @@ describe("BookDetails", () => {
227
226
  getByRole("button", { name: "Dismiss" });
228
227
 
229
228
  // Check that the API was invoked.
230
- expect(unsuppressBook.mock.calls.length).to.equal(1);
231
- expect(unsuppressBook.mock.calls[0][0]).to.equal("/unsuppress/href");
229
+ expect(unsuppressBook.mock.calls.length).toBe(1);
230
+ expect(unsuppressBook.mock.calls[0][0]).toBe("/unsuppress/href");
232
231
  const fetchCalls = fetchMock.calls();
233
- expect(fetchCalls.length).to.equal(1);
232
+ expect(fetchCalls.length).toBe(1);
234
233
  const fetchCall = fetchCalls[0];
235
234
  const fetchOptions = fetchCalls[0][1];
236
- expect(fetchCall[0]).to.equal("/unsuppress/href");
237
- expect(fetchOptions["headers"]["X-CSRF-Token"]).to.contain("token");
238
- expect(fetchOptions["method"]).to.equal("DELETE");
235
+ expect(fetchCall[0]).toBe("/unsuppress/href");
236
+ expect(fetchOptions["headers"]["X-CSRF-Token"]).toContain("token");
237
+ expect(fetchOptions["method"]).toBe("DELETE");
239
238
  });
240
239
  });
@@ -0,0 +1,158 @@
1
+ import * as React from "react";
2
+ import { screen } from "@testing-library/react";
3
+
4
+ import { renderWithProviders } from "../testUtils/withProviders";
5
+ import title from "../../../src/utils/title";
6
+
7
+ // CatalogPage does `require("@thepalaceproject/web-opds-client")` and mounts the OPDS
8
+ // Web Client, a heavy third-party render target that fetches on mount. Replace it
9
+ // with a marker that echoes the props CatalogPage hands it and the legacy
10
+ // `tab`/`library` child context CatalogPage supplies (read via `contextTypes`), so
11
+ // we can assert the wiring without the real catalog.
12
+ jest.mock("@thepalaceproject/web-opds-client", () => {
13
+ const React = require("react");
14
+ const PropTypes = require("prop-types");
15
+ const OPDSCatalogMock = (props, context) => (
16
+ <div
17
+ data-testid="opds-catalog"
18
+ data-collection-url={props.collectionUrl}
19
+ data-book-url={props.bookUrl}
20
+ data-tab={context.tab}
21
+ data-library={context.library && context.library()}
22
+ data-header-name={props.Header && props.Header.name}
23
+ data-book-details-container-name={
24
+ props.BookDetailsContainer && props.BookDetailsContainer.name
25
+ }
26
+ data-has-compute-breadcrumbs={String(!!props.computeBreadcrumbs)}
27
+ data-title-book={
28
+ props.pageTitleTemplate && props.pageTitleTemplate("Collection", "Book")
29
+ }
30
+ data-title-collection={
31
+ props.pageTitleTemplate && props.pageTitleTemplate("Collection", null)
32
+ }
33
+ />
34
+ );
35
+ OPDSCatalogMock.contextTypes = {
36
+ tab: PropTypes.string,
37
+ library: PropTypes.func,
38
+ };
39
+ return OPDSCatalogMock;
40
+ });
41
+
42
+ // When there is no library, CatalogPage renders WelcomePage, which mounts the real
43
+ // Header — that needs react-router v3 context and fetches on mount. Mock Header to a
44
+ // marker so the WelcomePage path stays self-contained.
45
+ jest.mock("../../../src/components/Header", () => ({
46
+ __esModule: true,
47
+ default: function Header() {
48
+ return <div data-testid="header" />;
49
+ },
50
+ }));
51
+
52
+ import CatalogPage from "../../../src/components/CatalogPage";
53
+ import Header from "../../../src/components/Header";
54
+ import BookDetailsContainer from "../../../src/components/BookDetailsContainer";
55
+
56
+ // The unit env has no `global.jsdom`, so compute the expected URLs from the real
57
+ // origin.
58
+ const origin = window.location.origin;
59
+
60
+ describe("CatalogPage", () => {
61
+ const baseParams = {
62
+ collectionUrl: "library/collectionurl",
63
+ bookUrl: "library/bookurl",
64
+ tab: "tab",
65
+ };
66
+
67
+ it("renders the OPDS catalog with expanded collection and book URLs", () => {
68
+ renderWithProviders(<CatalogPage params={baseParams} />);
69
+
70
+ const catalog = screen.getByTestId("opds-catalog");
71
+ expect(catalog).toHaveAttribute(
72
+ "data-collection-url",
73
+ `${origin}/library/collectionurl`
74
+ );
75
+ expect(catalog).toHaveAttribute(
76
+ "data-book-url",
77
+ `${origin}/library/works/bookurl`
78
+ );
79
+ });
80
+
81
+ it("forwards the admin header, book details container, breadcrumbs, and page-title template", () => {
82
+ renderWithProviders(<CatalogPage params={baseParams} />);
83
+
84
+ const catalog = screen.getByTestId("opds-catalog");
85
+ expect(catalog).toHaveAttribute("data-header-name", Header.name);
86
+ expect(catalog).toHaveAttribute(
87
+ "data-book-details-container-name",
88
+ BookDetailsContainer.name
89
+ );
90
+ expect(catalog).toHaveAttribute("data-has-compute-breadcrumbs", "true");
91
+ // pageTitleTemplate prefers the book title over the collection title.
92
+ expect(catalog).toHaveAttribute("data-title-book", title("Book"));
93
+ expect(catalog).toHaveAttribute(
94
+ "data-title-collection",
95
+ title("Collection")
96
+ );
97
+ });
98
+
99
+ it("expands a collection URL that already contains a query string", () => {
100
+ renderWithProviders(
101
+ <CatalogPage
102
+ params={{
103
+ ...baseParams,
104
+ collectionUrl: "library/collectionurl?samplequery=test",
105
+ }}
106
+ />
107
+ );
108
+
109
+ expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
110
+ "data-collection-url",
111
+ `${origin}/library/collectionurl?samplequery=test`
112
+ );
113
+ });
114
+
115
+ it("puts the tab into the child context", () => {
116
+ renderWithProviders(<CatalogPage params={baseParams} />);
117
+ expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
118
+ "data-tab",
119
+ "tab"
120
+ );
121
+ });
122
+
123
+ it("derives the child-context library from the collection URL", () => {
124
+ renderWithProviders(<CatalogPage params={baseParams} />);
125
+ expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
126
+ "data-library",
127
+ "library"
128
+ );
129
+ });
130
+
131
+ it("derives the child-context library from the book URL when there is no collection URL", () => {
132
+ renderWithProviders(
133
+ <CatalogPage params={{ ...baseParams, collectionUrl: null }} />
134
+ );
135
+ expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
136
+ "data-library",
137
+ "library"
138
+ );
139
+ });
140
+
141
+ it("renders the welcome page when there is no library", () => {
142
+ renderWithProviders(
143
+ <CatalogPage params={{ collectionUrl: null, bookUrl: null, tab: null }} />
144
+ );
145
+
146
+ expect(screen.queryByTestId("opds-catalog")).not.toBeInTheDocument();
147
+ expect(
148
+ screen.getByText(/Welcome to the Circulation Admin Interface!/)
149
+ ).toBeInTheDocument();
150
+ });
151
+
152
+ it("renders the footer", () => {
153
+ const { container } = renderWithProviders(
154
+ <CatalogPage params={baseParams} />
155
+ );
156
+ expect(container.querySelector("footer")).toBeInTheDocument();
157
+ });
158
+ });
@@ -1,7 +1,11 @@
1
1
  import * as React from "react";
2
2
  import { screen, within } from "@testing-library/react";
3
- import { CatalogServices } from "../../../src/components/CatalogServices";
3
+ import CatalogServicesConnected, {
4
+ CatalogServices,
5
+ } from "../../../src/components/CatalogServices";
4
6
  import renderWithContext from "../testUtils/renderWithContext";
7
+ import { renderWithProviders } from "../testUtils/withProviders";
8
+ import buildStore from "../../../src/store";
5
9
  import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
6
10
 
7
11
  describe("CatalogServices", () => {
@@ -61,3 +65,36 @@ describe("CatalogServices", () => {
61
65
  );
62
66
  });
63
67
  });
68
+
69
+ describe("CatalogServices - connected wiring", () => {
70
+ afterEach(() => jest.restoreAllMocks());
71
+
72
+ it("wires mapStateToProps/mapDispatchToProps and renders the fetched list", async () => {
73
+ jest.spyOn(globalThis, "fetch").mockImplementation(
74
+ async () =>
75
+ new Response(
76
+ JSON.stringify({
77
+ catalog_services: [
78
+ { id: 2, protocol: "test protocol", name: "Test Catalog" },
79
+ ],
80
+ protocols: [{ name: "test protocol", label: "TP", settings: [] }],
81
+ }),
82
+ { status: 200, headers: { "Content-Type": "application/json" } }
83
+ )
84
+ );
85
+ const store = buildStore();
86
+
87
+ renderWithProviders(<CatalogServicesConnected csrfToken="token" />, {
88
+ reduxProviderProps: { store },
89
+ appConfigSettings: { roles: [{ role: "system" }] },
90
+ });
91
+
92
+ // The edit link only appears once the on-mount fetch resolves and
93
+ // mapStateToProps feeds the fetched service back in as props.
94
+ const editLink = await screen.findByRole("link", { name: /edit/i });
95
+ expect(editLink).toHaveAttribute(
96
+ "href",
97
+ "/admin/web/config/catalogServices/edit/2"
98
+ );
99
+ });
100
+ });