@thepalaceproject/circulation-admin 1.43.0-post.10 → 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.
- package/package.json +1 -1
- package/tests/jest/components/Announcement.test.tsx +76 -0
- package/tests/jest/components/Autocomplete.test.tsx +67 -0
- package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/Classifications.test.tsx +166 -0
- package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
- package/tests/jest/components/ColorPicker.test.tsx +58 -0
- package/tests/jest/components/ComplaintForm.test.tsx +171 -0
- package/tests/jest/components/Complaints.test.tsx +259 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListsForBook.test.tsx +315 -0
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
|
@@ -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,166 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
|
+
import buildStore from "../../../src/store";
|
|
7
|
+
import genreData from "../../../src/components/__tests__/genreData";
|
|
8
|
+
import classificationsData from "../../../src/components/__tests__/classificationsData";
|
|
9
|
+
// Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
|
|
10
|
+
// are exercised. The genre tree and classifications are fetched on mount and fed
|
|
11
|
+
// back in through the Redux store; `book` and `refreshCatalog` are own props.
|
|
12
|
+
import Classifications, {
|
|
13
|
+
Classifications as UnconnectedClassifications,
|
|
14
|
+
} from "../../../src/components/Classifications";
|
|
15
|
+
|
|
16
|
+
// A `targetAgeRange` is required: ClassificationsForm reads it in bookChanged on
|
|
17
|
+
// re-render (the legacy enzyme test used shallow, so the child never re-rendered).
|
|
18
|
+
const bookData = {
|
|
19
|
+
title: "title",
|
|
20
|
+
fiction: true,
|
|
21
|
+
categories: ["Space Opera"],
|
|
22
|
+
targetAgeRange: [null, null],
|
|
23
|
+
};
|
|
24
|
+
const bookUrl = "works/1234";
|
|
25
|
+
const classificationsUrl = "admin/works/1234/classifications";
|
|
26
|
+
|
|
27
|
+
const jsonResponse = (body: unknown) =>
|
|
28
|
+
new Response(JSON.stringify(body), {
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Resolves each fetch with the appropriate body based on the request URL/method.
|
|
34
|
+
const stubFetch = () =>
|
|
35
|
+
jest
|
|
36
|
+
.spyOn(globalThis, "fetch")
|
|
37
|
+
.mockImplementation(async (url, opts?: RequestInit) => {
|
|
38
|
+
const u = String(url);
|
|
39
|
+
if (opts?.method === "POST") {
|
|
40
|
+
return new Response("success", { status: 200 });
|
|
41
|
+
}
|
|
42
|
+
if (u === "/admin/genres") {
|
|
43
|
+
return jsonResponse(genreData);
|
|
44
|
+
}
|
|
45
|
+
if (u.endsWith("/classifications")) {
|
|
46
|
+
return jsonResponse({ classifications: classificationsData });
|
|
47
|
+
}
|
|
48
|
+
// getBookData (OPDS) on refresh — non-OPDS body so it settles quietly.
|
|
49
|
+
return new Response("{}", { status: 200 });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const renderConnected = (
|
|
53
|
+
overrides: Partial<{ refreshCatalog: () => Promise<any> }> = {}
|
|
54
|
+
) => {
|
|
55
|
+
const store = buildStore();
|
|
56
|
+
const refreshCatalog =
|
|
57
|
+
overrides.refreshCatalog ?? jest.fn().mockResolvedValue(undefined);
|
|
58
|
+
const result = renderWithProviders(
|
|
59
|
+
<Classifications
|
|
60
|
+
store={store}
|
|
61
|
+
csrfToken="token"
|
|
62
|
+
bookUrl={bookUrl}
|
|
63
|
+
book={bookData as any}
|
|
64
|
+
refreshCatalog={refreshCatalog}
|
|
65
|
+
/>,
|
|
66
|
+
{ reduxProviderProps: { store } }
|
|
67
|
+
);
|
|
68
|
+
return { ...result, refreshCatalog };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe("Classifications", () => {
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
jest.restoreAllMocks();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("shows the book title", async () => {
|
|
77
|
+
stubFetch();
|
|
78
|
+
renderConnected();
|
|
79
|
+
expect(
|
|
80
|
+
await screen.findByRole("heading", { level: 2, name: "title" })
|
|
81
|
+
).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("shows the updating indicator while fetching", () => {
|
|
85
|
+
// A never-resolving fetch keeps the panel in its fetching state.
|
|
86
|
+
jest
|
|
87
|
+
.spyOn(globalThis, "fetch")
|
|
88
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
89
|
+
const { container } = renderConnected();
|
|
90
|
+
expect(container.querySelector(".updating-loader")).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("shows the classifications form and table once the data loads, and hides the updating indicator", async () => {
|
|
94
|
+
stubFetch();
|
|
95
|
+
const { container } = renderConnected();
|
|
96
|
+
|
|
97
|
+
// The form renders once the genre tree loads.
|
|
98
|
+
expect(
|
|
99
|
+
await screen.findByRole("button", { name: "Save" })
|
|
100
|
+
).toBeInTheDocument();
|
|
101
|
+
// The table renders once the classifications load.
|
|
102
|
+
expect(
|
|
103
|
+
screen.getByRole("heading", { name: "Related Classifications" })
|
|
104
|
+
).toBeInTheDocument();
|
|
105
|
+
// A sample classification from the fixture shows in the table.
|
|
106
|
+
expect(screen.getByText("FICTION / Ghost")).toBeInTheDocument();
|
|
107
|
+
// Fetching is done, so the updating indicator is gone.
|
|
108
|
+
expect(container.querySelector(".updating-loader")).not.toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("shows a fetch error", () => {
|
|
112
|
+
// A network failure on mount would reject the (uncaught) fetch promise and
|
|
113
|
+
// crash the worker, so drive the error through the prop that connect derives.
|
|
114
|
+
render(
|
|
115
|
+
<UnconnectedClassifications
|
|
116
|
+
store={buildStore() as any}
|
|
117
|
+
csrfToken="token"
|
|
118
|
+
bookUrl={bookUrl}
|
|
119
|
+
book={bookData as any}
|
|
120
|
+
refreshCatalog={jest.fn().mockResolvedValue(undefined)}
|
|
121
|
+
bookAdminUrl="book admin url"
|
|
122
|
+
fetchError={{ status: 500, url: "url", response: "response" } as any}
|
|
123
|
+
fetchGenreTree={jest.fn()}
|
|
124
|
+
fetchClassifications={jest.fn()}
|
|
125
|
+
fetchBook={jest.fn()}
|
|
126
|
+
editClassifications={jest.fn().mockResolvedValue(undefined)}
|
|
127
|
+
genreTree={null}
|
|
128
|
+
classifications={null}
|
|
129
|
+
isFetching={false}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
expect(screen.getByText(/Error: response/)).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("fetches the genre tree and classifications on mount", async () => {
|
|
136
|
+
const fetchSpy = stubFetch();
|
|
137
|
+
renderConnected();
|
|
138
|
+
await screen.findByRole("button", { name: "Save" });
|
|
139
|
+
|
|
140
|
+
const urls = fetchSpy.mock.calls.map(([u]) => String(u));
|
|
141
|
+
expect(urls).toContain("/admin/genres");
|
|
142
|
+
expect(urls).toContain(classificationsUrl);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("refreshes the book, classifications, and catalog after editing classifications", async () => {
|
|
146
|
+
const user = userEvent.setup();
|
|
147
|
+
const fetchSpy = stubFetch();
|
|
148
|
+
const { container, refreshCatalog } = renderConnected();
|
|
149
|
+
|
|
150
|
+
await screen.findByRole("button", { name: "Save" });
|
|
151
|
+
|
|
152
|
+
// The form requires an audience selection before it will submit.
|
|
153
|
+
await user.selectOptions(
|
|
154
|
+
container.querySelector('select[name="audience"]') as HTMLSelectElement,
|
|
155
|
+
"Adult"
|
|
156
|
+
);
|
|
157
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
158
|
+
|
|
159
|
+
// Editing POSTs the edit_classifications URL, then refreshes the catalog.
|
|
160
|
+
await waitFor(() => expect(refreshCatalog).toHaveBeenCalledTimes(1));
|
|
161
|
+
const postCall = fetchSpy.mock.calls.find(
|
|
162
|
+
([, opts]) => (opts as RequestInit)?.method === "POST"
|
|
163
|
+
);
|
|
164
|
+
expect(String(postCall[0])).toBe("admin/works/1234/edit_classifications");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -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 "../../../src/components/__tests__/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
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import ColorPicker from "../../../src/components/ColorPicker";
|
|
6
|
+
|
|
7
|
+
describe("ColorPicker", () => {
|
|
8
|
+
const setting = {
|
|
9
|
+
key: "key",
|
|
10
|
+
label: "label",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it("should show the compact picker", () => {
|
|
14
|
+
const { container } = render(
|
|
15
|
+
<ColorPicker setting={setting} value="#123456" />
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// The react-color CompactPicker renders a `.compact-picker` container whose
|
|
19
|
+
// hex field reflects the color it was given.
|
|
20
|
+
expect(container.querySelector(".compact-picker")).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByLabelText("hex")).toHaveValue("#123456");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should have a hidden input with the value", () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<ColorPicker setting={setting} value="#123456" />
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const input = container.querySelector<HTMLInputElement>(
|
|
30
|
+
'input[type="hidden"]'
|
|
31
|
+
);
|
|
32
|
+
expect(input).toHaveAttribute("name", "key");
|
|
33
|
+
expect(input).toHaveValue("#123456");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should change the value", async () => {
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
// getValue() is the imperative API ProtocolFormField calls via a ref in
|
|
39
|
+
// production, so exercise it through a real ref here.
|
|
40
|
+
const ref = React.createRef<ColorPicker>();
|
|
41
|
+
const { container } = render(
|
|
42
|
+
<ColorPicker ref={ref} setting={setting} value="#123456" />
|
|
43
|
+
);
|
|
44
|
+
const hiddenInput = container.querySelector<HTMLInputElement>(
|
|
45
|
+
'input[type="hidden"]'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(ref.current.getValue()).toBe("#123456");
|
|
49
|
+
expect(hiddenInput).toHaveValue("#123456");
|
|
50
|
+
|
|
51
|
+
// Clicking a palette swatch fires the picker's onChangeComplete, which
|
|
52
|
+
// updates the component's value (react-color debounces that callback).
|
|
53
|
+
await user.click(screen.getByTitle("#FE9200"));
|
|
54
|
+
|
|
55
|
+
await waitFor(() => expect(ref.current.getValue()).toBe("#fe9200"));
|
|
56
|
+
expect(hiddenInput).toHaveValue("#fe9200");
|
|
57
|
+
});
|
|
58
|
+
});
|