@thepalaceproject/circulation-admin 1.43.0-post.2 → 1.43.0-post.20
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 +26 -12
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/jest.config.js +17 -18
- package/package.json +4 -22
- 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/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 +443 -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/.nycrc.json +0 -8
- package/testReporter.js +0 -29
- package/tests/jest/README.md +0 -6
|
@@ -0,0 +1,353 @@
|
|
|
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
|
+
|
|
8
|
+
// CustomListsForBook renders a react-router v3 <Link> for which
|
|
9
|
+
// renderWithProviders supplies no router context. Mock it to a marker that
|
|
10
|
+
// exposes the `to` pathname and state so the destination can be asserted.
|
|
11
|
+
jest.mock("react-router", () => ({
|
|
12
|
+
...jest.requireActual("react-router"),
|
|
13
|
+
Link: (props: any) => (
|
|
14
|
+
<div
|
|
15
|
+
data-testid="Link"
|
|
16
|
+
data-pathname={
|
|
17
|
+
typeof props.to === "string" ? props.to : props.to?.pathname
|
|
18
|
+
}
|
|
19
|
+
data-book-title={
|
|
20
|
+
typeof props.to === "object" ? props.to?.state?.bookTitle : undefined
|
|
21
|
+
}
|
|
22
|
+
>
|
|
23
|
+
{props.children}
|
|
24
|
+
</div>
|
|
25
|
+
),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
|
|
29
|
+
// are exercised. The book's lists and all available lists are fetched on mount
|
|
30
|
+
// and fed back in through the Redux store; book/bookUrl/library/refreshCatalog
|
|
31
|
+
// are own props.
|
|
32
|
+
import CustomListsForBook, {
|
|
33
|
+
CustomListsForBook as UnconnectedCustomListsForBook,
|
|
34
|
+
} from "../../../src/components/CustomListsForBook";
|
|
35
|
+
|
|
36
|
+
const allCustomLists = [
|
|
37
|
+
{ id: "1", name: "list 1", is_owner: true, is_shared: false },
|
|
38
|
+
{ id: "2", name: "list 2", is_owner: true, is_shared: false },
|
|
39
|
+
{ id: "3", name: "list 3", is_owner: true, is_shared: false },
|
|
40
|
+
];
|
|
41
|
+
const listsForBook = [
|
|
42
|
+
{ id: "2", name: "list 2", is_owner: true, is_shared: false },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const jsonResponse = (body: unknown) =>
|
|
46
|
+
new Response(JSON.stringify(body), {
|
|
47
|
+
status: 200,
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Serves the "all custom lists" endpoint and the per-book "/lists" endpoint from
|
|
52
|
+
// the supplied fixtures; answers POSTs (edit) with success.
|
|
53
|
+
const stubFetch = (allLists: unknown[], forBook: unknown[]) =>
|
|
54
|
+
jest
|
|
55
|
+
.spyOn(globalThis, "fetch")
|
|
56
|
+
.mockImplementation(async (url, opts?: RequestInit) => {
|
|
57
|
+
if (opts?.method === "POST") {
|
|
58
|
+
return new Response("success", { status: 200 });
|
|
59
|
+
}
|
|
60
|
+
const u = String(url);
|
|
61
|
+
if (u.includes("custom_lists")) {
|
|
62
|
+
return jsonResponse({ custom_lists: allLists });
|
|
63
|
+
}
|
|
64
|
+
return jsonResponse({ custom_lists: forBook });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const renderConnected = () => {
|
|
68
|
+
const store = buildStore();
|
|
69
|
+
const refreshCatalog = jest.fn().mockResolvedValue(undefined);
|
|
70
|
+
const result = renderWithProviders(
|
|
71
|
+
<CustomListsForBook
|
|
72
|
+
csrfToken="token"
|
|
73
|
+
book={{ id: "id", title: "test title" } as any}
|
|
74
|
+
bookUrl="works/book url"
|
|
75
|
+
library="library"
|
|
76
|
+
refreshCatalog={refreshCatalog}
|
|
77
|
+
/>,
|
|
78
|
+
{ reduxProviderProps: { store } }
|
|
79
|
+
);
|
|
80
|
+
return { ...result, refreshCatalog };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
describe("CustomListsForBook", () => {
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
jest.restoreAllMocks();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("shows the book title and the book's current lists", async () => {
|
|
89
|
+
stubFetch(allCustomLists, listsForBook);
|
|
90
|
+
const { container } = renderConnected();
|
|
91
|
+
|
|
92
|
+
expect(
|
|
93
|
+
await screen.findByRole("heading", { level: 2, name: "test title" })
|
|
94
|
+
).toBeInTheDocument();
|
|
95
|
+
expect(
|
|
96
|
+
screen.getByRole("heading", { name: "Current Lists" })
|
|
97
|
+
).toBeInTheDocument();
|
|
98
|
+
|
|
99
|
+
const link = await screen.findByRole("link", { name: "list 2" });
|
|
100
|
+
expect(link).toHaveAttribute("href", "/admin/web/lists/library/edit/2");
|
|
101
|
+
// Exactly one current list is shown.
|
|
102
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("builds the current-list URL from the list's id, not its name", async () => {
|
|
106
|
+
// A list named "new" whose id is "42" proves the URL uses the id.
|
|
107
|
+
const newList = { id: "42", name: "new", is_owner: true, is_shared: false };
|
|
108
|
+
stubFetch([...allCustomLists, newList], [newList]);
|
|
109
|
+
renderConnected();
|
|
110
|
+
|
|
111
|
+
const link = await screen.findByRole("link", { name: "new" });
|
|
112
|
+
expect(link).toHaveAttribute("href", "/admin/web/lists/library/edit/42");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("shows a menu of the lists the book is not yet on", async () => {
|
|
116
|
+
stubFetch(allCustomLists, listsForBook);
|
|
117
|
+
const { container } = renderConnected();
|
|
118
|
+
|
|
119
|
+
await screen.findByRole("link", { name: "list 2" });
|
|
120
|
+
|
|
121
|
+
const select = container.querySelector("select");
|
|
122
|
+
expect(select).not.toBeNull();
|
|
123
|
+
const optionValues = Array.from(select.querySelectorAll("option")).map(
|
|
124
|
+
(o) => o.getAttribute("value")
|
|
125
|
+
);
|
|
126
|
+
// "list 2" is excluded because the book is already on it.
|
|
127
|
+
expect(optionValues).toStrictEqual(["list 1", "list 3"]);
|
|
128
|
+
expect(screen.getByText(/Select an existing list/)).toBeInTheDocument();
|
|
129
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("shows placeholder text when the book is not on any list", async () => {
|
|
133
|
+
stubFetch(allCustomLists, []);
|
|
134
|
+
const { container } = renderConnected();
|
|
135
|
+
|
|
136
|
+
expect(
|
|
137
|
+
await screen.findByText("This book is not currently on any lists.")
|
|
138
|
+
).toBeInTheDocument();
|
|
139
|
+
// With no current lists there is no "Current Lists" heading.
|
|
140
|
+
expect(
|
|
141
|
+
screen.queryByRole("heading", { name: "Current Lists" })
|
|
142
|
+
).not.toBeInTheDocument();
|
|
143
|
+
expect(container.querySelector(".with-remove-button")).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("hides the menu once the book is on every available list", async () => {
|
|
147
|
+
stubFetch(allCustomLists, allCustomLists);
|
|
148
|
+
const { container } = renderConnected();
|
|
149
|
+
|
|
150
|
+
expect(
|
|
151
|
+
await screen.findByText(
|
|
152
|
+
"This book has been added to all the available lists."
|
|
153
|
+
)
|
|
154
|
+
).toBeInTheDocument();
|
|
155
|
+
expect(container.querySelector("select")).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("shows a message instead of the input list when no lists exist", async () => {
|
|
159
|
+
stubFetch([], []);
|
|
160
|
+
renderConnected();
|
|
161
|
+
|
|
162
|
+
expect(
|
|
163
|
+
await screen.findByText("There are no available lists.")
|
|
164
|
+
).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("shows a link to the list creator that carries the book title", async () => {
|
|
168
|
+
stubFetch(allCustomLists, listsForBook);
|
|
169
|
+
renderConnected();
|
|
170
|
+
|
|
171
|
+
await screen.findByRole("link", { name: "list 2" });
|
|
172
|
+
|
|
173
|
+
const creatorLink = screen.getByTestId("Link");
|
|
174
|
+
expect(creatorLink).toHaveTextContent("Create a new list");
|
|
175
|
+
expect(creatorLink).toHaveAttribute(
|
|
176
|
+
"data-pathname",
|
|
177
|
+
"/admin/web/lists/library/create"
|
|
178
|
+
);
|
|
179
|
+
expect(creatorLink).toHaveAttribute("data-book-title", "test title");
|
|
180
|
+
expect(
|
|
181
|
+
screen.getByText(
|
|
182
|
+
"(The book title will be automatically copied and searched on the list creator page.)"
|
|
183
|
+
)
|
|
184
|
+
).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("fetches the book's lists and all custom lists on mount, without refreshing the catalog", async () => {
|
|
188
|
+
const fetchSpy = stubFetch(allCustomLists, listsForBook);
|
|
189
|
+
const { refreshCatalog } = renderConnected();
|
|
190
|
+
|
|
191
|
+
await screen.findByRole("link", { name: "list 2" });
|
|
192
|
+
|
|
193
|
+
const urls = fetchSpy.mock.calls.map(([u]) => String(u));
|
|
194
|
+
expect(urls).toContain("admin/works/book url/lists");
|
|
195
|
+
expect(urls.some((u) => u.includes("custom_lists"))).toBe(true);
|
|
196
|
+
expect(refreshCatalog).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("does not re-fetch all custom lists on mount when they are already loaded", () => {
|
|
200
|
+
// The mount guard (CustomListsForBook.tsx:158) fetches the full list of
|
|
201
|
+
// custom lists only when they are not already present; the book's own lists
|
|
202
|
+
// are still fetched. Drive the already-loaded case through the unconnected
|
|
203
|
+
// class, since the connected mount always starts from an empty store.
|
|
204
|
+
const fetchAllCustomLists = jest.fn();
|
|
205
|
+
const fetchCustomListsForBook = jest.fn();
|
|
206
|
+
render(
|
|
207
|
+
<UnconnectedCustomListsForBook
|
|
208
|
+
csrfToken="token"
|
|
209
|
+
book={{ id: "id", title: "test title" } as any}
|
|
210
|
+
bookUrl="works/book url"
|
|
211
|
+
library="library"
|
|
212
|
+
refreshCatalog={jest.fn().mockResolvedValue(undefined)}
|
|
213
|
+
allCustomLists={allCustomLists as any}
|
|
214
|
+
customListsForBook={listsForBook as any}
|
|
215
|
+
fetchAllCustomLists={fetchAllCustomLists}
|
|
216
|
+
fetchCustomListsForBook={fetchCustomListsForBook}
|
|
217
|
+
editCustomListsForBook={jest.fn().mockResolvedValue(undefined)}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(fetchCustomListsForBook).toHaveBeenCalledTimes(1);
|
|
222
|
+
expect(fetchAllCustomLists).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("adds a list and posts the updated set of lists", async () => {
|
|
226
|
+
const user = userEvent.setup();
|
|
227
|
+
const fetchSpy = stubFetch(allCustomLists, listsForBook);
|
|
228
|
+
const { container, refreshCatalog } = renderConnected();
|
|
229
|
+
|
|
230
|
+
await screen.findByRole("link", { name: "list 2" });
|
|
231
|
+
|
|
232
|
+
await user.selectOptions(
|
|
233
|
+
container.querySelector("select") as HTMLSelectElement,
|
|
234
|
+
"list 1"
|
|
235
|
+
);
|
|
236
|
+
await user.click(screen.getByRole("button", { name: "Add" }));
|
|
237
|
+
|
|
238
|
+
await waitFor(() => expect(refreshCatalog).toHaveBeenCalled());
|
|
239
|
+
|
|
240
|
+
const postCall = fetchSpy.mock.calls.find(
|
|
241
|
+
([, opts]) => (opts as RequestInit)?.method === "POST"
|
|
242
|
+
);
|
|
243
|
+
expect(String(postCall[0])).toBe("admin/works/book url/lists");
|
|
244
|
+
const lists = JSON.parse(
|
|
245
|
+
((postCall[1] as RequestInit).body as FormData).get("lists") as string
|
|
246
|
+
);
|
|
247
|
+
expect(lists).toStrictEqual([
|
|
248
|
+
{ id: "2", name: "list 2", is_owner: true, is_shared: false },
|
|
249
|
+
{ id: "1", name: "list 1", is_owner: true, is_shared: false },
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("removes a list and posts the emptied set of lists", async () => {
|
|
254
|
+
const user = userEvent.setup();
|
|
255
|
+
const fetchSpy = stubFetch(allCustomLists, listsForBook);
|
|
256
|
+
const { refreshCatalog } = renderConnected();
|
|
257
|
+
|
|
258
|
+
await screen.findByRole("link", { name: "list 2" });
|
|
259
|
+
|
|
260
|
+
await user.click(screen.getByRole("button", { name: "Delete" }));
|
|
261
|
+
|
|
262
|
+
await waitFor(() => expect(refreshCatalog).toHaveBeenCalled());
|
|
263
|
+
|
|
264
|
+
const postCall = fetchSpy.mock.calls.find(
|
|
265
|
+
([, opts]) => (opts as RequestInit)?.method === "POST"
|
|
266
|
+
);
|
|
267
|
+
expect(String(postCall[0])).toBe("admin/works/book url/lists");
|
|
268
|
+
const lists = JSON.parse(
|
|
269
|
+
((postCall[1] as RequestInit).body as FormData).get("lists") as string
|
|
270
|
+
);
|
|
271
|
+
expect(lists).toStrictEqual([]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("shows an error message", () => {
|
|
275
|
+
// A mount-time fetch failure would reject the (uncaught) fetch promise and
|
|
276
|
+
// crash the worker, so drive the error through the prop connect derives.
|
|
277
|
+
render(
|
|
278
|
+
<UnconnectedCustomListsForBook
|
|
279
|
+
csrfToken="token"
|
|
280
|
+
book={{ id: "id", title: "test title" } as any}
|
|
281
|
+
bookUrl="works/book url"
|
|
282
|
+
library="library"
|
|
283
|
+
refreshCatalog={jest.fn().mockResolvedValue(undefined)}
|
|
284
|
+
allCustomLists={allCustomLists as any}
|
|
285
|
+
customListsForBook={listsForBook as any}
|
|
286
|
+
fetchAllCustomLists={jest.fn()}
|
|
287
|
+
fetchCustomListsForBook={jest.fn()}
|
|
288
|
+
editCustomListsForBook={jest.fn().mockResolvedValue(undefined)}
|
|
289
|
+
fetchError={{ status: 400, response: "boom", url: "" } as any}
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
expect(screen.getByText(/Error: boom/)).toBeInTheDocument();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("disables the remove and add controls while fetching", () => {
|
|
296
|
+
// isFetching is a connect-derived prop; inject it via the unconnected class.
|
|
297
|
+
render(
|
|
298
|
+
<UnconnectedCustomListsForBook
|
|
299
|
+
csrfToken="token"
|
|
300
|
+
book={{ id: "id", title: "test title" } as any}
|
|
301
|
+
bookUrl="works/book url"
|
|
302
|
+
library="library"
|
|
303
|
+
refreshCatalog={jest.fn().mockResolvedValue(undefined)}
|
|
304
|
+
allCustomLists={allCustomLists as any}
|
|
305
|
+
customListsForBook={listsForBook as any}
|
|
306
|
+
fetchAllCustomLists={jest.fn()}
|
|
307
|
+
fetchCustomListsForBook={jest.fn()}
|
|
308
|
+
editCustomListsForBook={jest.fn().mockResolvedValue(undefined)}
|
|
309
|
+
isFetching={true}
|
|
310
|
+
/>
|
|
311
|
+
);
|
|
312
|
+
expect(screen.getByRole("button", { name: "Delete" })).toBeDisabled();
|
|
313
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("shows the new book's lists when it is rerendered for another book", () => {
|
|
317
|
+
const commonProps = {
|
|
318
|
+
csrfToken: "token",
|
|
319
|
+
library: "library",
|
|
320
|
+
refreshCatalog: jest.fn().mockResolvedValue(undefined),
|
|
321
|
+
allCustomLists: allCustomLists as any,
|
|
322
|
+
fetchAllCustomLists: jest.fn(),
|
|
323
|
+
fetchCustomListsForBook: jest.fn(),
|
|
324
|
+
editCustomListsForBook: jest.fn().mockResolvedValue(undefined),
|
|
325
|
+
};
|
|
326
|
+
const { rerender } = render(
|
|
327
|
+
<UnconnectedCustomListsForBook
|
|
328
|
+
{...commonProps}
|
|
329
|
+
book={{ id: "a", title: "Book A" } as any}
|
|
330
|
+
bookUrl="works/book-a"
|
|
331
|
+
customListsForBook={listsForBook as any}
|
|
332
|
+
/>
|
|
333
|
+
);
|
|
334
|
+
// list 2 (the book's only list) is in the current column, which renders
|
|
335
|
+
// each list as a link; the add-menu renders options instead.
|
|
336
|
+
expect(screen.getByRole("link", { name: "list 2" })).toBeInTheDocument();
|
|
337
|
+
|
|
338
|
+
// The current column renders straight from customListsForBook, so a new
|
|
339
|
+
// book's lists replace the old ones on rerender.
|
|
340
|
+
rerender(
|
|
341
|
+
<UnconnectedCustomListsForBook
|
|
342
|
+
{...commonProps}
|
|
343
|
+
book={{ id: "b", title: "Book B" } as any}
|
|
344
|
+
bookUrl="works/book-b"
|
|
345
|
+
customListsForBook={[allCustomLists[0]] as any}
|
|
346
|
+
/>
|
|
347
|
+
);
|
|
348
|
+
expect(screen.getByRole("link", { name: "list 1" })).toBeInTheDocument();
|
|
349
|
+
expect(
|
|
350
|
+
screen.queryByRole("link", { name: "list 2" })
|
|
351
|
+
).not.toBeInTheDocument();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, within } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import CustomListsSidebar from "../../../src/components/CustomListsSidebar";
|
|
5
|
+
|
|
6
|
+
// Render react-router's Link as a marker anchor so we can assert its target
|
|
7
|
+
// (`data-to`), text, and className without needing a Router in the test.
|
|
8
|
+
jest.mock("react-router", () => ({
|
|
9
|
+
...jest.requireActual("react-router"),
|
|
10
|
+
Link: (props) => (
|
|
11
|
+
<a
|
|
12
|
+
data-testid="Link"
|
|
13
|
+
data-to={props.to}
|
|
14
|
+
href={props.to}
|
|
15
|
+
className={props.className}
|
|
16
|
+
>
|
|
17
|
+
{props.children}
|
|
18
|
+
</a>
|
|
19
|
+
),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe("CustomListsSidebar", () => {
|
|
23
|
+
const makeLists = () => [
|
|
24
|
+
{
|
|
25
|
+
id: 1,
|
|
26
|
+
name: "First List",
|
|
27
|
+
entry_count: 5,
|
|
28
|
+
is_owner: true,
|
|
29
|
+
is_shared: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 2,
|
|
33
|
+
name: "Second List",
|
|
34
|
+
entry_count: 10,
|
|
35
|
+
is_owner: true,
|
|
36
|
+
is_shared: false,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const baseProps = () => ({
|
|
41
|
+
filter: "owned",
|
|
42
|
+
lists: makeLists(),
|
|
43
|
+
library: "library_name",
|
|
44
|
+
identifier: "123",
|
|
45
|
+
isLibraryManager: true,
|
|
46
|
+
deleteCustomList: jest.fn(),
|
|
47
|
+
changeSort: jest.fn(),
|
|
48
|
+
sortOrder: "asc",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders a sidebar with a header and a create button", () => {
|
|
52
|
+
const { container } = render(<CustomListsSidebar {...baseProps()} />);
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
container.querySelector(".custom-lists-sidebar")
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
|
|
58
|
+
"List Manager"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const createButton = screen.getAllByTestId("Link")[0];
|
|
62
|
+
expect(createButton).toHaveTextContent("Create New List");
|
|
63
|
+
expect(createButton).toHaveAttribute(
|
|
64
|
+
"data-to",
|
|
65
|
+
"/admin/web/lists/library_name/create"
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders filter select", () => {
|
|
70
|
+
const { container } = render(<CustomListsSidebar {...baseProps()} />);
|
|
71
|
+
|
|
72
|
+
const select = container.querySelector<HTMLSelectElement>(
|
|
73
|
+
'select[name="filter"]'
|
|
74
|
+
);
|
|
75
|
+
expect(select).toHaveValue("owned");
|
|
76
|
+
|
|
77
|
+
const options = within(select).getAllByRole("option");
|
|
78
|
+
expect(options).toHaveLength(4);
|
|
79
|
+
expect(options[0]).toHaveValue("");
|
|
80
|
+
expect(options[1]).toHaveValue("owned");
|
|
81
|
+
expect(options[2]).toHaveValue("shared-out");
|
|
82
|
+
expect(options[3]).toHaveValue("shared-in");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("renders sort select", () => {
|
|
86
|
+
const { container } = render(<CustomListsSidebar {...baseProps()} />);
|
|
87
|
+
|
|
88
|
+
const select = container.querySelector<HTMLSelectElement>(
|
|
89
|
+
'select[name="sort"]'
|
|
90
|
+
);
|
|
91
|
+
expect(select).toHaveValue("asc");
|
|
92
|
+
|
|
93
|
+
const options = within(select).getAllByRole("option");
|
|
94
|
+
expect(options).toHaveLength(2);
|
|
95
|
+
expect(options[0]).toHaveValue("asc");
|
|
96
|
+
expect(options[1]).toHaveValue("desc");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders a list of custom list info items", async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
101
|
+
const deleteCustomList = jest.fn();
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<CustomListsSidebar
|
|
104
|
+
{...baseProps()}
|
|
105
|
+
deleteCustomList={deleteCustomList}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const listItems = container.querySelectorAll("ul > li");
|
|
110
|
+
expect(listItems).toHaveLength(2);
|
|
111
|
+
const [firstList, secondList] = Array.from(listItems);
|
|
112
|
+
|
|
113
|
+
const firstInfo = firstList.querySelectorAll(".custom-list-info > div");
|
|
114
|
+
expect(firstInfo[0]).toHaveTextContent("First List");
|
|
115
|
+
expect(firstInfo[1]).toHaveTextContent("Books in list: 5");
|
|
116
|
+
expect(firstInfo[2]).toHaveTextContent("ID-1");
|
|
117
|
+
|
|
118
|
+
const firstButtons = firstList.querySelector<HTMLElement>(
|
|
119
|
+
".custom-list-buttons"
|
|
120
|
+
);
|
|
121
|
+
const firstEdit = within(firstButtons).getByTestId("Link");
|
|
122
|
+
expect(firstEdit).toHaveTextContent("Edit");
|
|
123
|
+
expect(firstEdit).toHaveAttribute(
|
|
124
|
+
"data-to",
|
|
125
|
+
"/admin/web/lists/library_name/edit/1"
|
|
126
|
+
);
|
|
127
|
+
const firstDelete = within(firstButtons).getByRole("button");
|
|
128
|
+
expect(firstDelete).toHaveTextContent("Delete");
|
|
129
|
+
await user.click(firstDelete);
|
|
130
|
+
expect(deleteCustomList).toHaveBeenCalledTimes(1);
|
|
131
|
+
|
|
132
|
+
const secondInfo = secondList.querySelectorAll(".custom-list-info > div");
|
|
133
|
+
expect(secondInfo[0]).toHaveTextContent("Second List");
|
|
134
|
+
expect(secondInfo[1]).toHaveTextContent("Books in list: 10");
|
|
135
|
+
expect(secondInfo[2]).toHaveTextContent("ID-2");
|
|
136
|
+
|
|
137
|
+
const secondButtons = secondList.querySelector<HTMLElement>(
|
|
138
|
+
".custom-list-buttons"
|
|
139
|
+
);
|
|
140
|
+
const secondEdit = within(secondButtons).getByTestId("Link");
|
|
141
|
+
expect(secondEdit).toHaveTextContent("Edit");
|
|
142
|
+
expect(secondEdit).toHaveAttribute(
|
|
143
|
+
"data-to",
|
|
144
|
+
"/admin/web/lists/library_name/edit/2"
|
|
145
|
+
);
|
|
146
|
+
const secondDelete = within(secondButtons).getByRole("button");
|
|
147
|
+
expect(secondDelete).toHaveTextContent("Delete");
|
|
148
|
+
await user.click(secondDelete);
|
|
149
|
+
expect(deleteCustomList).toHaveBeenCalledTimes(2);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("disables the edit button if the list is already being edited", () => {
|
|
153
|
+
const props = baseProps();
|
|
154
|
+
const { container, rerender } = render(<CustomListsSidebar {...props} />);
|
|
155
|
+
|
|
156
|
+
const firstButtons = () =>
|
|
157
|
+
container.querySelectorAll<HTMLElement>(".custom-list-buttons")[0];
|
|
158
|
+
|
|
159
|
+
const editLink = within(firstButtons()).getByTestId("Link");
|
|
160
|
+
expect(editLink).not.toHaveClass("disabled");
|
|
161
|
+
expect(editLink).toHaveTextContent("EditPencil Icon");
|
|
162
|
+
|
|
163
|
+
rerender(<CustomListsSidebar {...props} identifier="1" />);
|
|
164
|
+
|
|
165
|
+
const editingButton = within(firstButtons()).getByRole("button", {
|
|
166
|
+
name: "Editing",
|
|
167
|
+
});
|
|
168
|
+
expect(editingButton).toBeDisabled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("renders a view button instead of an edit button if a list is not owned", () => {
|
|
172
|
+
const { container } = render(
|
|
173
|
+
<CustomListsSidebar
|
|
174
|
+
{...baseProps()}
|
|
175
|
+
lists={[
|
|
176
|
+
{
|
|
177
|
+
id: 1,
|
|
178
|
+
name: "First List",
|
|
179
|
+
entry_count: 5,
|
|
180
|
+
is_owner: false,
|
|
181
|
+
is_shared: false,
|
|
182
|
+
},
|
|
183
|
+
]}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const firstButtons = container.querySelector<HTMLElement>(
|
|
188
|
+
".custom-list-buttons"
|
|
189
|
+
);
|
|
190
|
+
const viewLink = within(firstButtons).getByTestId("Link");
|
|
191
|
+
expect(viewLink).not.toHaveClass("disabled");
|
|
192
|
+
expect(viewLink).toHaveTextContent("ViewVisible Icon");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("displays the delete button only to library managers", () => {
|
|
196
|
+
const props = baseProps();
|
|
197
|
+
const { container, rerender } = render(<CustomListsSidebar {...props} />);
|
|
198
|
+
|
|
199
|
+
expect(
|
|
200
|
+
container.querySelectorAll(".custom-list-buttons button")
|
|
201
|
+
).toHaveLength(2);
|
|
202
|
+
|
|
203
|
+
rerender(<CustomListsSidebar {...props} isLibraryManager={false} />);
|
|
204
|
+
|
|
205
|
+
expect(
|
|
206
|
+
container.querySelectorAll(".custom-list-buttons button")
|
|
207
|
+
).toHaveLength(0);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not render a delete button if a list is not owned", () => {
|
|
211
|
+
const { container } = render(
|
|
212
|
+
<CustomListsSidebar
|
|
213
|
+
{...baseProps()}
|
|
214
|
+
lists={[
|
|
215
|
+
{
|
|
216
|
+
id: 1,
|
|
217
|
+
name: "First List",
|
|
218
|
+
entry_count: 5,
|
|
219
|
+
is_owner: false,
|
|
220
|
+
is_shared: false,
|
|
221
|
+
},
|
|
222
|
+
]}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(
|
|
227
|
+
container.querySelectorAll(".custom-list-buttons button")
|
|
228
|
+
).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does not render a delete button if a list is shared", () => {
|
|
232
|
+
const { container } = render(
|
|
233
|
+
<CustomListsSidebar
|
|
234
|
+
{...baseProps()}
|
|
235
|
+
lists={[
|
|
236
|
+
{
|
|
237
|
+
id: 1,
|
|
238
|
+
name: "First List",
|
|
239
|
+
entry_count: 5,
|
|
240
|
+
is_owner: true,
|
|
241
|
+
is_shared: true,
|
|
242
|
+
},
|
|
243
|
+
]}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(
|
|
248
|
+
container.querySelectorAll(".custom-list-buttons button")
|
|
249
|
+
).toHaveLength(0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
|
|
6
|
+
// Header and Footer are page chrome; Header pulls in the react-router v3 context
|
|
7
|
+
// that `renderWithProviders` does not supply. Mock them to markers.
|
|
8
|
+
jest.mock("../../../src/components/Header", () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
default: () => <div data-testid="header" />,
|
|
11
|
+
}));
|
|
12
|
+
jest.mock("../../../src/components/Footer", () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: () => <div data-testid="footer" />,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Stats fetches statistics via RTK Query on mount; DashboardPage's job is just
|
|
18
|
+
// composition, so mock Stats to a marker that echoes the `library` prop it is
|
|
19
|
+
// handed rather than wiring up fetch mocking.
|
|
20
|
+
jest.mock("../../../src/components/Stats", () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
default: (props: any) => (
|
|
23
|
+
<div data-testid="stats" data-library={props.library} />
|
|
24
|
+
),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// CirculationEventsDownload gates its output on a feature flag (off by default) and
|
|
28
|
+
// keeps the `library` prop inside a collapsed form, so it is not DOM-observable when
|
|
29
|
+
// rendered for real. Mock it to a marker that echoes `library` so the prop-flow the
|
|
30
|
+
// original test checked stays observable.
|
|
31
|
+
jest.mock("../../../src/components/CirculationEventsDownload", () => ({
|
|
32
|
+
__esModule: true,
|
|
33
|
+
default: (props: any) => (
|
|
34
|
+
<div
|
|
35
|
+
data-testid="circulation-events-download"
|
|
36
|
+
data-library={props.library}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
import DashboardPage from "../../../src/components/DashboardPage";
|
|
42
|
+
|
|
43
|
+
describe("DashboardPage", () => {
|
|
44
|
+
it("shows Header", () => {
|
|
45
|
+
renderWithProviders(<DashboardPage params={{}} />);
|
|
46
|
+
expect(screen.getByTestId("header")).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("shows CirculationEventsDownload", () => {
|
|
50
|
+
const { rerender } = renderWithProviders(<DashboardPage params={{}} />);
|
|
51
|
+
|
|
52
|
+
let events = screen.getByTestId("circulation-events-download");
|
|
53
|
+
expect(events).toBeInTheDocument();
|
|
54
|
+
expect(events).not.toHaveAttribute("data-library");
|
|
55
|
+
|
|
56
|
+
rerender(<DashboardPage params={{ library: "NYPL" }} />);
|
|
57
|
+
events = screen.getByTestId("circulation-events-download");
|
|
58
|
+
expect(events).toHaveAttribute("data-library", "NYPL");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows Stats", () => {
|
|
62
|
+
const { rerender } = renderWithProviders(<DashboardPage params={{}} />);
|
|
63
|
+
|
|
64
|
+
let stats = screen.getByTestId("stats");
|
|
65
|
+
expect(stats).not.toHaveAttribute("data-library");
|
|
66
|
+
|
|
67
|
+
rerender(<DashboardPage params={{ library: "NYPL" }} />);
|
|
68
|
+
stats = screen.getByTestId("stats");
|
|
69
|
+
expect(stats).toHaveAttribute("data-library", "NYPL");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("shows Footer", () => {
|
|
73
|
+
renderWithProviders(<DashboardPage params={{}} />);
|
|
74
|
+
expect(screen.getByTestId("footer")).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -212,11 +212,7 @@ describe("DebugAuthentication", () => {
|
|
|
212
212
|
)
|
|
213
213
|
);
|
|
214
214
|
|
|
215
|
-
const {
|
|
216
|
-
getByLabelText,
|
|
217
|
-
queryByLabelText,
|
|
218
|
-
rerender,
|
|
219
|
-
} = renderWithProviders(
|
|
215
|
+
const { getByLabelText, queryByLabelText, rerender } = renderWithProviders(
|
|
220
216
|
<DebugAuthentication library={LIBRARY} csrfToken="test-csrf-token" />,
|
|
221
217
|
{ queryClient }
|
|
222
218
|
);
|