@thepalaceproject/circulation-admin 1.43.0-post.1 → 1.43.0-post.11

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 (94) hide show
  1. package/CLAUDE.md +16 -7
  2. package/dist/circulation-admin.js +1 -1
  3. package/eslint.config.mjs +9 -1
  4. package/jest.config.js +56 -9
  5. package/package.json +6 -16
  6. package/tests/jest/actions.test.ts +962 -0
  7. package/tests/jest/components/AccountPage.test.tsx +41 -0
  8. package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
  9. package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
  10. package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
  11. package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
  12. package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
  13. package/tests/jest/components/Announcement.test.tsx +76 -0
  14. package/tests/jest/components/Autocomplete.test.tsx +67 -0
  15. package/tests/jest/components/BookDetails.test.tsx +133 -0
  16. package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
  17. package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
  18. package/tests/jest/components/BookEditForm.test.tsx +352 -0
  19. package/tests/jest/components/CatalogServices.test.tsx +63 -0
  20. package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
  21. package/tests/jest/components/Classifications.test.tsx +166 -0
  22. package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
  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/ConfigPage.test.tsx +58 -0
  27. package/tests/jest/components/ContextProvider.test.tsx +138 -0
  28. package/tests/jest/components/CustomListEditor.test.tsx +367 -0
  29. package/tests/jest/components/CustomListPage.test.tsx +57 -0
  30. package/tests/jest/components/CustomListSearch.test.tsx +275 -0
  31. package/tests/jest/components/CustomListsForBook.test.tsx +315 -0
  32. package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
  33. package/tests/jest/components/DashboardPage.test.tsx +76 -0
  34. package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
  35. package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
  36. package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
  37. package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
  38. package/tests/jest/components/EditableConfigList.test.tsx +8 -2
  39. package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
  40. package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
  41. package/tests/jest/components/ErrorMessage.test.tsx +97 -0
  42. package/tests/jest/components/Footer.test.tsx +141 -0
  43. package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
  44. package/tests/jest/components/LaneEditor.test.tsx +2 -6
  45. package/tests/jest/components/LanePage.test.tsx +68 -0
  46. package/tests/jest/components/LanguageField.test.tsx +40 -0
  47. package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
  48. package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
  49. package/tests/jest/components/LoadButton.test.tsx +49 -0
  50. package/tests/jest/components/ManagePatrons.test.tsx +73 -0
  51. package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
  52. package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
  53. package/tests/jest/components/MetadataServices.test.tsx +59 -0
  54. package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
  55. package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
  56. package/tests/jest/components/PatronInfo.test.tsx +54 -0
  57. package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
  58. package/tests/jest/components/SaveButton.test.tsx +47 -0
  59. package/tests/jest/components/SelfTestResult.test.tsx +164 -0
  60. package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
  61. package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
  62. package/tests/jest/components/SetupPage.test.tsx +41 -0
  63. package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
  64. package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
  65. package/tests/jest/components/Stats.test.tsx +4 -8
  66. package/tests/jest/components/TabContainer.test.tsx +68 -0
  67. package/tests/jest/components/Timestamp.test.tsx +74 -0
  68. package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
  69. package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
  70. package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
  71. package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
  72. package/tests/jest/components/WelcomePage.test.tsx +43 -0
  73. package/tests/jest/components/WithEditButton.test.tsx +55 -0
  74. package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
  75. package/tests/jest/editorAdapter.test.ts +245 -0
  76. package/tests/jest/index.test.tsx +50 -0
  77. package/tests/jest/legacy-setup.ts +13 -0
  78. package/tests/jest/legacyJsdomEnvironment.js +49 -0
  79. package/tests/jest/models/Admin.test.ts +206 -0
  80. package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
  81. package/tests/jest/reducers/classifications.test.ts +161 -0
  82. package/tests/jest/reducers/collections.test.ts +96 -0
  83. package/tests/jest/reducers/complaints.test.ts +166 -0
  84. package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
  85. package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
  86. package/tests/jest/reducers/customListDetails.test.ts +120 -0
  87. package/tests/jest/reducers/customListEditor.test.ts +2823 -0
  88. package/tests/jest/reducers/managePatrons.test.ts +71 -0
  89. package/tests/jest/testUtils/formDataShim.test.ts +135 -0
  90. package/tests/jest/testUtils/formDataShim.ts +112 -0
  91. package/tests/jest/utils/sharedFunctions.test.ts +121 -0
  92. package/tsconfig.json +3 -4
  93. package/testReporter.js +0 -29
  94. package/tests/jest/components/AdvancedSearchBuilder.test.tsx +0 -38
@@ -0,0 +1,275 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ import { renderWithProviders } from "../testUtils/withProviders";
5
+ import buildStore from "../../../src/store";
6
+ import {
7
+ PER_LIBRARY_SUPPRESS_REL,
8
+ PER_LIBRARY_UNSUPPRESS_REL,
9
+ } from "../../../src/features/book/bookEditorSlice";
10
+
11
+ // BookEditForm is a heavy child; the legacy enzyme tests only shallow-rendered
12
+ // it and asserted the props it received. Mock it to a marker that surfaces those
13
+ // props as data attributes so the same facts can be asserted against real DOM.
14
+ jest.mock("../../../src/components/BookEditForm", () => ({
15
+ __esModule: true,
16
+ default: (props: any) => (
17
+ <div
18
+ data-testid="book-edit-form"
19
+ data-title={props.title}
20
+ data-roles={JSON.stringify(props.roles ?? null)}
21
+ data-media={JSON.stringify(props.media ?? null)}
22
+ data-languages={JSON.stringify(props.languages ?? null)}
23
+ />
24
+ ),
25
+ }));
26
+
27
+ import BookDetailsEditor, {
28
+ BookDetailsEditor as UnconnectedBookDetailsEditor,
29
+ } from "../../../src/components/BookDetailsEditor";
30
+
31
+ // The connected component derives bookData from OPDS parsing of a fetch, which is
32
+ // impractical to synthesize; render the unconnected class with injected props for
33
+ // the render-behavior tests (as the legacy enzyme tests did), and cover the
34
+ // connect wiring with the dedicated connected test below.
35
+ const renderEditor = (overrides: Record<string, unknown> = {}) => {
36
+ const props = {
37
+ bookUrl: "url",
38
+ csrfToken: "token",
39
+ canSuppress: true,
40
+ bookAdminUrl: "admin url",
41
+ bookData: undefined,
42
+ roles: undefined,
43
+ media: undefined,
44
+ languages: undefined,
45
+ isFetching: false,
46
+ fetchError: undefined,
47
+ editError: undefined,
48
+ refreshCatalog: jest.fn().mockResolvedValue(undefined),
49
+ fetchBookData: jest.fn(),
50
+ fetchRoles: jest.fn(),
51
+ fetchMedia: jest.fn(),
52
+ fetchLanguages: jest.fn(),
53
+ postBookData: jest.fn(),
54
+ suppressBook: jest.fn(),
55
+ unsuppressBook: jest.fn(),
56
+ ...overrides,
57
+ };
58
+ const result = render(<UnconnectedBookDetailsEditor {...(props as any)} />);
59
+ return { props, ...result };
60
+ };
61
+
62
+ describe("BookDetailsEditor", () => {
63
+ afterEach(() => {
64
+ jest.restoreAllMocks();
65
+ });
66
+
67
+ it("loads admin book url, roles, languages, and media on mount", () => {
68
+ const { props } = renderEditor({ bookUrl: "works/1234" });
69
+
70
+ expect(props.fetchBookData).toHaveBeenCalledTimes(1);
71
+ expect(props.fetchBookData).toHaveBeenCalledWith("admin/works/1234");
72
+ expect(props.fetchRoles).toHaveBeenCalledTimes(1);
73
+ expect(props.fetchMedia).toHaveBeenCalledTimes(1);
74
+ expect(props.fetchLanguages).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ it("loads admin book url when given a new book url", () => {
78
+ const { props, rerender } = renderEditor({ bookUrl: "works/1234" });
79
+ rerender(
80
+ <UnconnectedBookDetailsEditor {...(props as any)} bookUrl="works/5555" />
81
+ );
82
+
83
+ expect(props.fetchBookData).toHaveBeenCalledTimes(2);
84
+ expect((props.fetchBookData as jest.Mock).mock.calls[1][0]).toBe(
85
+ "admin/works/5555"
86
+ );
87
+ });
88
+
89
+ it("shows the title", () => {
90
+ renderEditor({ bookData: { id: "id", title: "title" } });
91
+ expect(
92
+ screen.getByRole("heading", { level: 2, name: "title" })
93
+ ).toBeInTheDocument();
94
+ });
95
+
96
+ it("shows a Hide button for a per-library suppress link", () => {
97
+ renderEditor({
98
+ bookData: {
99
+ id: "id",
100
+ title: "title",
101
+ suppressPerLibraryLink: { href: "href", rel: PER_LIBRARY_SUPPRESS_REL },
102
+ },
103
+ });
104
+ expect(screen.getByRole("button", { name: "Hide" })).toBeInTheDocument();
105
+ });
106
+
107
+ it("shows a Restore button in the visibility banner for a per-library unsuppress link", () => {
108
+ const { container } = renderEditor({
109
+ bookData: {
110
+ id: "id",
111
+ title: "title",
112
+ unsuppressPerLibraryLink: {
113
+ href: "href",
114
+ rel: PER_LIBRARY_UNSUPPRESS_REL,
115
+ },
116
+ visibilityStatus: "manually-suppressed",
117
+ },
118
+ });
119
+ expect(container.querySelector(".visibility-status")).toBeInTheDocument();
120
+ expect(screen.getByRole("button", { name: "Restore" })).toBeInTheDocument();
121
+ });
122
+
123
+ it("shows the restore banner when an unsuppress link exists without a visibilityStatus", () => {
124
+ const { container } = renderEditor({
125
+ bookData: {
126
+ id: "id",
127
+ title: "title",
128
+ unsuppressPerLibraryLink: {
129
+ href: "href",
130
+ rel: PER_LIBRARY_UNSUPPRESS_REL,
131
+ },
132
+ },
133
+ });
134
+ expect(
135
+ container.querySelector(".visibility-status-manually-suppressed")
136
+ ).toBeInTheDocument();
137
+ expect(screen.getByRole("button", { name: "Restore" })).toBeInTheDocument();
138
+ });
139
+
140
+ it("shows the manually-suppressed banner without a restore button when there is no unsuppress link", () => {
141
+ const { container } = renderEditor({
142
+ bookData: {
143
+ id: "id",
144
+ title: "title",
145
+ visibilityStatus: "manually-suppressed",
146
+ },
147
+ });
148
+ const banner = container.querySelector(
149
+ ".visibility-status-manually-suppressed"
150
+ );
151
+ expect(banner).toBeInTheDocument();
152
+ expect(banner.textContent).toContain(
153
+ "manually hidden by a library staff member"
154
+ );
155
+ expect(
156
+ screen.queryByRole("button", { name: "Restore" })
157
+ ).not.toBeInTheDocument();
158
+ });
159
+
160
+ it("shows the policy-filtered banner without a restore button", () => {
161
+ const { container } = renderEditor({
162
+ bookData: {
163
+ id: "id",
164
+ title: "title",
165
+ visibilityStatus: "policy-filtered",
166
+ },
167
+ });
168
+ const banner = container.querySelector(
169
+ ".visibility-status-policy-filtered"
170
+ );
171
+ expect(banner).toBeInTheDocument();
172
+ expect(banner.textContent).toContain("library content filtering");
173
+ expect(
174
+ screen.queryByRole("button", { name: "Restore" })
175
+ ).not.toBeInTheDocument();
176
+ });
177
+
178
+ it("shows a Refresh Metadata button for a refresh link", () => {
179
+ renderEditor({
180
+ bookData: {
181
+ id: "id",
182
+ title: "title",
183
+ refreshLink: {
184
+ href: "href",
185
+ rel: "http://librarysimplified/terms/rel/refresh",
186
+ },
187
+ },
188
+ });
189
+ expect(
190
+ screen.getByRole("button", { name: "Refresh Metadata" })
191
+ ).toBeInTheDocument();
192
+ });
193
+
194
+ it("shows a fetch error message and no edit form", () => {
195
+ renderEditor({
196
+ bookData: { id: "id", title: "title" },
197
+ fetchError: { status: 500, response: "response", url: "" },
198
+ });
199
+ expect(screen.getByText(/Error: response/)).toBeInTheDocument();
200
+ expect(screen.queryByTestId("book-edit-form")).not.toBeInTheDocument();
201
+ });
202
+
203
+ it("shows an edit error message together with the edit form", () => {
204
+ renderEditor({
205
+ bookData: {
206
+ id: "id",
207
+ title: "title",
208
+ editLink: {
209
+ href: "href",
210
+ rel: "http://librarysimplified.org/terms/rel/edit",
211
+ },
212
+ },
213
+ editError: { status: 500, response: "response", url: "" },
214
+ });
215
+ expect(screen.getByTestId("book-edit-form")).toBeInTheDocument();
216
+ expect(screen.getByText(/Error: response/)).toBeInTheDocument();
217
+ });
218
+
219
+ it("shows the book edit form with roles, media, and languages", () => {
220
+ const roles = { aut: "Author", nar: "Narrator" };
221
+ const media = {
222
+ "http://schema.org/AudioObject": "Audio",
223
+ "http://schema.org/Book": "Book",
224
+ };
225
+ const languages = { eng: ["English"], spa: ["Spanish", "Castilian"] };
226
+ renderEditor({
227
+ bookData: {
228
+ id: "id",
229
+ title: "title",
230
+ editLink: {
231
+ href: "href",
232
+ rel: "http://librarysimplified.org/terms/rel/edit",
233
+ },
234
+ },
235
+ roles,
236
+ media,
237
+ languages,
238
+ });
239
+
240
+ const form = screen.getByTestId("book-edit-form");
241
+ expect(form).toHaveAttribute("data-title", "title");
242
+ expect(JSON.parse(form.getAttribute("data-roles"))).toStrictEqual(roles);
243
+ expect(JSON.parse(form.getAttribute("data-media"))).toStrictEqual(media);
244
+ expect(JSON.parse(form.getAttribute("data-languages"))).toStrictEqual(
245
+ languages
246
+ );
247
+ });
248
+
249
+ it("wires up the connected default export (mapStateToProps / mapDispatchToProps)", async () => {
250
+ // Stub fetch so mounting the connected panel does not hit the network. The
251
+ // roles/media/languages endpoints return JSON; the book endpoint returns a
252
+ // non-OPDS body so getBookData rejects (handled) and surfaces a fetch error.
253
+ jest
254
+ .spyOn(globalThis, "fetch")
255
+ .mockImplementation(async () => new Response("{}", { status: 200 }));
256
+
257
+ const store = buildStore();
258
+ renderWithProviders(
259
+ <BookDetailsEditor
260
+ store={store}
261
+ bookUrl="works/1234"
262
+ csrfToken="token"
263
+ canSuppress={true}
264
+ refreshCatalog={jest.fn().mockResolvedValue(undefined)}
265
+ />,
266
+ { reduxProviderProps: { store } }
267
+ );
268
+
269
+ // getBookData's OPDS parse fails on the non-OPDS body, so mapStateToProps
270
+ // feeds the resulting fetch error back in and the panel renders an error.
271
+ expect(
272
+ await screen.findByText(/Failed to parse OPDS data/)
273
+ ).toBeInTheDocument();
274
+ });
275
+ });
@@ -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
+ });
@@ -0,0 +1,63 @@
1
+ import * as React from "react";
2
+ import { screen, within } from "@testing-library/react";
3
+ import { CatalogServices } from "../../../src/components/CatalogServices";
4
+ import renderWithContext from "../testUtils/renderWithContext";
5
+ import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
6
+
7
+ describe("CatalogServices", () => {
8
+ const data = {
9
+ catalog_services: [
10
+ {
11
+ id: 2,
12
+ protocol: "test protocol",
13
+ settings: {
14
+ test_setting: "test setting",
15
+ },
16
+ libraries: [
17
+ {
18
+ short_name: "nypl",
19
+ test_library_setting: "test library setting",
20
+ },
21
+ ],
22
+ name: "nypl catalog",
23
+ },
24
+ ],
25
+ protocols: [
26
+ {
27
+ name: "test protocol",
28
+ label: "test protocol label",
29
+ settings: [],
30
+ },
31
+ ],
32
+ allLibraries: [
33
+ {
34
+ short_name: "nypl",
35
+ },
36
+ ],
37
+ };
38
+
39
+ const config = { csrfToken: "token", featureFlags: defaultFeatureFlags };
40
+
41
+ it("shows catalog service list", () => {
42
+ renderWithContext(
43
+ <CatalogServices
44
+ data={data}
45
+ fetchData={jest.fn()}
46
+ editItem={jest.fn().mockResolvedValue(undefined)}
47
+ csrfToken="token"
48
+ isFetching={false}
49
+ />,
50
+ config
51
+ );
52
+
53
+ const items = screen.getAllByRole("listitem");
54
+ expect(items).toHaveLength(1);
55
+ expect(items[0]).toHaveTextContent("nypl catalog: test protocol label");
56
+
57
+ const editLink = within(items[0]).getByRole("link");
58
+ expect(editLink).toHaveAttribute(
59
+ "href",
60
+ "/admin/web/config/catalogServices/edit/2"
61
+ );
62
+ });
63
+ });