@thepalaceproject/circulation-admin 1.43.0 → 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.
- package/CLAUDE.md +28 -7
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +22 -8
- package/package.json +6 -20
- package/tests/jest/actions.test.ts +962 -0
- package/tests/jest/components/AccountPage.test.tsx +41 -0
- package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +93 -20
- package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
- package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
- package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
- package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
- package/tests/jest/components/Announcement.test.tsx +76 -0
- package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
- package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
- package/tests/jest/components/Autocomplete.test.tsx +67 -0
- package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
- package/tests/jest/components/BookDetails.test.tsx +133 -0
- package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
- package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/BookEditor.test.tsx +20 -21
- package/tests/jest/components/CatalogPage.test.tsx +158 -0
- package/tests/jest/components/CatalogServices.test.tsx +100 -0
- package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
- package/tests/jest/components/Classifications.test.tsx +166 -0
- package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
- package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
- package/tests/jest/components/Collections.test.tsx +335 -7
- 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/ConfigPage.test.tsx +58 -0
- package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/Contributors.test.tsx +157 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
- package/tests/jest/components/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomLists.test.tsx +621 -4
- package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
- package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
- package/tests/jest/components/DashboardPage.test.tsx +76 -0
- package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
- package/tests/jest/components/EditableConfigList.test.tsx +606 -9
- package/tests/jest/components/EditableInput.test.tsx +250 -1
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
- package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
- package/tests/jest/components/ErrorMessage.test.tsx +97 -0
- package/tests/jest/components/Footer.test.tsx +141 -0
- package/tests/jest/components/GenreForm.test.tsx +152 -0
- package/tests/jest/components/Header.test.tsx +421 -0
- package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
- package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/JsonField.test.tsx +20 -0
- package/tests/jest/components/Lane.test.tsx +132 -10
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +377 -13
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/Libraries.test.tsx +48 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/LoadButton.test.tsx +49 -0
- package/tests/jest/components/ManagePatrons.test.tsx +73 -0
- package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
- package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
- package/tests/jest/components/MetadataServices.test.tsx +95 -0
- package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
- package/tests/jest/components/PatronInfo.test.tsx +54 -0
- package/tests/jest/components/ProtocolFormField.test.tsx +479 -7
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SaveButton.test.tsx +47 -0
- package/tests/jest/components/SelfTestResult.test.tsx +164 -0
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
- package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
- package/tests/jest/components/SetupPage.test.tsx +41 -0
- package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +4 -15
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -0
- package/tests/jest/components/ToolTip.test.tsx +46 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
- package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
- package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
- package/tests/jest/components/WelcomePage.test.tsx +43 -0
- package/tests/jest/components/WithEditButton.test.tsx +55 -0
- package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
- package/tests/jest/editorAdapter.test.ts +245 -0
- package/tests/jest/features/book.test.ts +75 -80
- package/tests/jest/fixtures/classificationsData.ts +45 -0
- package/tests/jest/fixtures/genreData.ts +811 -0
- package/tests/jest/index.test.tsx +50 -0
- package/tests/jest/models/Admin.test.ts +206 -0
- package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
- package/tests/jest/reducers/classifications.test.ts +161 -0
- package/tests/jest/reducers/collections.test.ts +96 -0
- package/tests/jest/reducers/complaints.test.ts +166 -0
- package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
- package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
- package/tests/jest/reducers/customListDetails.test.ts +120 -0
- package/tests/jest/reducers/customListEditor.test.ts +2823 -0
- package/tests/jest/reducers/managePatrons.test.ts +71 -0
- package/tests/jest/testUtils/formDataShim.test.ts +135 -0
- package/tests/jest/testUtils/formDataShim.ts +112 -0
- package/tests/jest/utils/sharedFunctions.test.ts +121 -0
- package/tsconfig.json +3 -4
- package/testReporter.js +0 -29
- package/tests/jest/README.md +0 -6
|
@@ -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
|
+
});
|