@thepalaceproject/circulation-admin 1.43.0 → 1.44.0-post.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,259 @@
|
|
|
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 ConnectedComplaints, {
|
|
6
|
+
Complaints,
|
|
7
|
+
} from "../../../src/components/Complaints";
|
|
8
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
9
|
+
|
|
10
|
+
const bookData = {
|
|
11
|
+
id: "id",
|
|
12
|
+
title: "test title",
|
|
13
|
+
issuesLink: {
|
|
14
|
+
href: "issues url",
|
|
15
|
+
rel: "issues",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const complaintsData = {
|
|
20
|
+
"http://librarysimplified.org/terms/problem/test-type": 5,
|
|
21
|
+
"http://librarysimplified.org/terms/problem/other-type": 3,
|
|
22
|
+
"http://librarysimplified.org/terms/problem/last-type": 1,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// The unconnected `Complaints` takes its data + dispatch functions as props, so
|
|
26
|
+
// a bare `render` (no store) is enough for the rendering/behavior assertions.
|
|
27
|
+
// One separate test below renders the connected default export through the real
|
|
28
|
+
// providers to cover the connect() wiring (mapStateToProps/mapDispatchToProps).
|
|
29
|
+
const renderComplaints = (
|
|
30
|
+
props: Partial<React.ComponentProps<typeof Complaints>> = {}
|
|
31
|
+
) =>
|
|
32
|
+
render(
|
|
33
|
+
<Complaints
|
|
34
|
+
csrfToken="token"
|
|
35
|
+
bookUrl="book url"
|
|
36
|
+
book={bookData}
|
|
37
|
+
complaints={complaintsData}
|
|
38
|
+
fetchComplaints={jest.fn()}
|
|
39
|
+
postComplaint={jest.fn()}
|
|
40
|
+
refreshCatalog={jest.fn()}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
describe("Complaints", () => {
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
jest.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("rendering", () => {
|
|
51
|
+
it("shows the book title", () => {
|
|
52
|
+
renderComplaints();
|
|
53
|
+
expect(
|
|
54
|
+
screen.getByRole("heading", { name: "test title" })
|
|
55
|
+
).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("shows complaints with their counts", () => {
|
|
59
|
+
const { container } = renderComplaints();
|
|
60
|
+
const types = Array.from(
|
|
61
|
+
container.querySelectorAll(".complaint-type")
|
|
62
|
+
).map((el) => el.textContent);
|
|
63
|
+
const counts = Array.from(
|
|
64
|
+
container.querySelectorAll(".complaint-count")
|
|
65
|
+
).map((el) => parseInt(el.textContent || "", 10));
|
|
66
|
+
expect(types).toStrictEqual(["Test type", "Other type", "Last type"]);
|
|
67
|
+
expect(counts).toStrictEqual([5, 3, 1]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("shows simplified complaint types", () => {
|
|
71
|
+
const { container } = renderComplaints();
|
|
72
|
+
const types = Array.from(
|
|
73
|
+
container.querySelectorAll(".complaint-type")
|
|
74
|
+
).map((el) => el.textContent);
|
|
75
|
+
expect(types).toStrictEqual(["Test type", "Other type", "Last type"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("shows an unrecognized complaint type unchanged", () => {
|
|
79
|
+
// A type that does not match the /terms/problem/ pattern is rendered as-is.
|
|
80
|
+
const { container } = renderComplaints({
|
|
81
|
+
complaints: { "custom-complaint": 2 },
|
|
82
|
+
});
|
|
83
|
+
const types = Array.from(
|
|
84
|
+
container.querySelectorAll(".complaint-type")
|
|
85
|
+
).map((el) => el.textContent);
|
|
86
|
+
expect(types).toStrictEqual(["custom-complaint"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("shows an enabled resolve button for each complaint type", () => {
|
|
90
|
+
renderComplaints();
|
|
91
|
+
const buttons = screen.getAllByRole("button", { name: "Resolve" });
|
|
92
|
+
expect(buttons).toHaveLength(Object.keys(complaintsData).length);
|
|
93
|
+
buttons.forEach((button) => expect(button).not.toBeDisabled());
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("shows an enabled complaint form when the book has an issues link", () => {
|
|
97
|
+
const { container } = renderComplaints();
|
|
98
|
+
// The ComplaintForm is only rendered when `book.issuesLink` exists, so its
|
|
99
|
+
// presence (with an enabled select) reflects that the form was wired up.
|
|
100
|
+
expect(screen.getByText("Add Complaint")).toBeInTheDocument();
|
|
101
|
+
const select = container.querySelector("select") as HTMLSelectElement;
|
|
102
|
+
expect(select).toBeInTheDocument();
|
|
103
|
+
expect(select).not.toBeDisabled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("shows a fetch error and re-fetches when trying again", async () => {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
const fetchComplaints = jest.fn();
|
|
109
|
+
const fetchError = { status: 401, response: "test", url: "test url" };
|
|
110
|
+
const { container } = render(
|
|
111
|
+
<Complaints
|
|
112
|
+
csrfToken="token"
|
|
113
|
+
bookUrl="book url"
|
|
114
|
+
book={{ id: "id", title: "test book" }}
|
|
115
|
+
fetchError={fetchError}
|
|
116
|
+
fetchComplaints={fetchComplaints}
|
|
117
|
+
postComplaint={jest.fn()}
|
|
118
|
+
refreshCatalog={jest.fn()}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// The error alert is rendered from the fetchError prop.
|
|
123
|
+
expect(container.querySelector(".alert-danger")).toBeInTheDocument();
|
|
124
|
+
// fetchComplaints is called once on mount (bookUrl is set).
|
|
125
|
+
expect(fetchComplaints).toHaveBeenCalledTimes(1);
|
|
126
|
+
|
|
127
|
+
await user.click(screen.getByRole("button", { name: "Try again" }));
|
|
128
|
+
|
|
129
|
+
expect(fetchComplaints).toHaveBeenCalledTimes(2);
|
|
130
|
+
expect(fetchComplaints.mock.calls[1][0]).toBe("book url/complaints");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("behavior", () => {
|
|
135
|
+
const singleComplaint = {
|
|
136
|
+
"http://librarysimplified.org/terms/problem/test-type": 2,
|
|
137
|
+
};
|
|
138
|
+
const bookUrl = "http://example.com/works/fakeid";
|
|
139
|
+
|
|
140
|
+
it("fetches complaints on mount", () => {
|
|
141
|
+
const fetchComplaints = jest.fn();
|
|
142
|
+
render(
|
|
143
|
+
<Complaints
|
|
144
|
+
csrfToken="token"
|
|
145
|
+
book={{ id: "id", title: "test book" }}
|
|
146
|
+
bookUrl={bookUrl}
|
|
147
|
+
complaints={singleComplaint}
|
|
148
|
+
fetchComplaints={fetchComplaints}
|
|
149
|
+
refreshCatalog={jest.fn()}
|
|
150
|
+
resolveComplaints={jest.fn().mockResolvedValue(undefined)}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(fetchComplaints).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect(fetchComplaints.mock.calls[0][0]).toBe(
|
|
156
|
+
"http://example.com/admin/works/fakeid/complaints"
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("resolves a complaint type when its resolve button is clicked", async () => {
|
|
161
|
+
const user = userEvent.setup();
|
|
162
|
+
jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
163
|
+
const resolveComplaints = jest.fn().mockResolvedValue(undefined);
|
|
164
|
+
render(
|
|
165
|
+
<Complaints
|
|
166
|
+
csrfToken="token"
|
|
167
|
+
book={{ id: "id", title: "test book" }}
|
|
168
|
+
bookUrl={bookUrl}
|
|
169
|
+
complaints={singleComplaint}
|
|
170
|
+
fetchComplaints={jest.fn()}
|
|
171
|
+
refreshCatalog={jest.fn()}
|
|
172
|
+
resolveComplaints={resolveComplaints}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
await user.click(screen.getByRole("button", { name: "Resolve" }));
|
|
177
|
+
|
|
178
|
+
expect(resolveComplaints).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(resolveComplaints.mock.calls[0][0]).toBe(
|
|
180
|
+
"http://example.com/admin/works/fakeid/resolve_complaints"
|
|
181
|
+
);
|
|
182
|
+
// The FormData sent along carries the complaint type being resolved.
|
|
183
|
+
const submittedData = resolveComplaints.mock.calls[0][1];
|
|
184
|
+
expect(submittedData.get("type")).toBe(
|
|
185
|
+
"http://librarysimplified.org/terms/problem/test-type"
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("re-fetches complaints and refreshes the catalog after resolving", async () => {
|
|
190
|
+
const user = userEvent.setup();
|
|
191
|
+
jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
192
|
+
const fetchComplaints = jest.fn();
|
|
193
|
+
const refreshCatalog = jest.fn();
|
|
194
|
+
const resolveComplaints = jest.fn().mockResolvedValue(undefined);
|
|
195
|
+
render(
|
|
196
|
+
<Complaints
|
|
197
|
+
csrfToken="token"
|
|
198
|
+
book={{ id: "id", title: "test book" }}
|
|
199
|
+
bookUrl={bookUrl}
|
|
200
|
+
complaints={singleComplaint}
|
|
201
|
+
fetchComplaints={fetchComplaints}
|
|
202
|
+
refreshCatalog={refreshCatalog}
|
|
203
|
+
resolveComplaints={resolveComplaints}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
await user.click(screen.getByRole("button", { name: "Resolve" }));
|
|
208
|
+
|
|
209
|
+
await waitFor(() => expect(refreshCatalog).toHaveBeenCalledTimes(1));
|
|
210
|
+
expect(resolveComplaints).toHaveBeenCalledTimes(1);
|
|
211
|
+
// Once on mount, and again as part of the post-resolve refresh.
|
|
212
|
+
expect(fetchComplaints).toHaveBeenCalledTimes(2);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("connect() wiring", () => {
|
|
217
|
+
it("renders the connected default export, mapping state and dispatch", async () => {
|
|
218
|
+
const fetchSpy = jest.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
219
|
+
new Response(JSON.stringify({ complaints: {} }), {
|
|
220
|
+
status: 200,
|
|
221
|
+
headers: { "Content-Type": "application/json" },
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const { container } = renderWithProviders(
|
|
226
|
+
<ConnectedComplaints
|
|
227
|
+
bookUrl="http://example.com/works/fakeid"
|
|
228
|
+
book={{ id: "id", title: "connected book" }}
|
|
229
|
+
csrfToken="token"
|
|
230
|
+
refreshCatalog={jest.fn().mockResolvedValue(undefined)}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Own prop flows through the connected component.
|
|
235
|
+
expect(
|
|
236
|
+
screen.getByRole("heading", { name: "connected book" })
|
|
237
|
+
).toBeInTheDocument();
|
|
238
|
+
|
|
239
|
+
// mapDispatchToProps.fetchComplaints ran on mount and hit the network.
|
|
240
|
+
await waitFor(() => expect(fetchSpy).toHaveBeenCalled());
|
|
241
|
+
expect(fetchSpy.mock.calls[0][0]).toBe(
|
|
242
|
+
"http://example.com/admin/works/fakeid/complaints"
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Let the request/load cycle settle (isFetching true -> false), which also
|
|
246
|
+
// flushes the pending fetch so it cannot reject during teardown.
|
|
247
|
+
await waitFor(() =>
|
|
248
|
+
expect(
|
|
249
|
+
container.querySelector(".updating-loader")
|
|
250
|
+
).not.toBeInTheDocument()
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// mapStateToProps: no complaints in the store -> "None found." is shown.
|
|
254
|
+
expect(screen.getByText("None found.")).toBeInTheDocument();
|
|
255
|
+
|
|
256
|
+
jest.restoreAllMocks();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
// ConfigTabContainer mounts the connected configuration sections (Libraries,
|
|
18
|
+
// Collections, Individual Admins, ...) which fetch over the network on mount. That
|
|
19
|
+
// is incidental to what ConfigPage composes — the page's job is to hand the tab
|
|
20
|
+
// container the params and context it needs — so mock it to a marker that echoes
|
|
21
|
+
// the props it received.
|
|
22
|
+
jest.mock("../../../src/components/ConfigTabContainer", () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
default: (props: any) => (
|
|
25
|
+
<div
|
|
26
|
+
data-testid="config-tab-container"
|
|
27
|
+
data-tab={props.tab}
|
|
28
|
+
data-edit-or-create={props.editOrCreate}
|
|
29
|
+
data-identifier={props.identifier}
|
|
30
|
+
data-csrf-token={props.csrfToken}
|
|
31
|
+
data-has-store={String(!!props.store)}
|
|
32
|
+
/>
|
|
33
|
+
),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
import ConfigPage from "../../../src/components/ConfigPage";
|
|
37
|
+
|
|
38
|
+
describe("ConfigPage", () => {
|
|
39
|
+
const params = {
|
|
40
|
+
tab: "libraries",
|
|
41
|
+
editOrCreate: "edit",
|
|
42
|
+
identifier: "identifier",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
it("shows a tab container with tab from params", () => {
|
|
46
|
+
renderWithProviders(<ConfigPage params={params} />, {
|
|
47
|
+
appConfigSettings: { csrfToken: "token" },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const tabContainer = screen.getByTestId("config-tab-container");
|
|
51
|
+
expect(tabContainer).toHaveAttribute("data-tab", "libraries");
|
|
52
|
+
expect(tabContainer).toHaveAttribute("data-edit-or-create", "edit");
|
|
53
|
+
expect(tabContainer).toHaveAttribute("data-identifier", "identifier");
|
|
54
|
+
expect(tabContainer).toHaveAttribute("data-csrf-token", "token");
|
|
55
|
+
// The redux store is forwarded from context; assert it was passed through.
|
|
56
|
+
expect(tabContainer).toHaveAttribute("data-has-store", "true");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import buildStore from "../../../src/store";
|
|
7
|
+
import Admin from "../../../src/models/Admin";
|
|
8
|
+
|
|
9
|
+
// Every configuration tab panel is a connected component that fetches on mount.
|
|
10
|
+
// This container's behavior is the tab SET and tab SWITCHING, not the panels'
|
|
11
|
+
// internals, so mock each panel to a marker echoing the props the container is
|
|
12
|
+
// responsible for threading into it (csrfToken / editOrCreate / identifier).
|
|
13
|
+
const panelMock = (testId: string) => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: (props: any) => (
|
|
16
|
+
<div
|
|
17
|
+
data-testid={testId}
|
|
18
|
+
data-csrf-token={props.csrfToken}
|
|
19
|
+
data-edit-or-create={props.editOrCreate}
|
|
20
|
+
data-identifier={props.identifier}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
jest.mock("../../../src/components/Libraries", () => panelMock("libraries"));
|
|
26
|
+
jest.mock("../../../src/components/Collections", () =>
|
|
27
|
+
panelMock("collections")
|
|
28
|
+
);
|
|
29
|
+
jest.mock("../../../src/components/IndividualAdmins", () =>
|
|
30
|
+
panelMock("individual-admins")
|
|
31
|
+
);
|
|
32
|
+
jest.mock("../../../src/components/PatronAuthServices", () =>
|
|
33
|
+
panelMock("patron-auth-services")
|
|
34
|
+
);
|
|
35
|
+
jest.mock("../../../src/components/MetadataServices", () =>
|
|
36
|
+
panelMock("metadata-services")
|
|
37
|
+
);
|
|
38
|
+
jest.mock("../../../src/components/CatalogServices", () =>
|
|
39
|
+
panelMock("catalog-services")
|
|
40
|
+
);
|
|
41
|
+
jest.mock("../../../src/components/DiscoveryServices", () =>
|
|
42
|
+
panelMock("discovery-services")
|
|
43
|
+
);
|
|
44
|
+
jest.mock("../../../src/components/SitewideAnnouncements", () =>
|
|
45
|
+
panelMock("sitewide-announcements")
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
import ConfigTabContainer from "../../../src/components/ConfigTabContainer";
|
|
49
|
+
|
|
50
|
+
// ConfigTabContainer reads `router`, `pathFor`, and `admin` from the legacy React
|
|
51
|
+
// context the app supplies in production. Wrap it in a small provider that hands
|
|
52
|
+
// it those.
|
|
53
|
+
class LegacyContextProvider extends React.Component<{ context: any }> {
|
|
54
|
+
static childContextTypes = {
|
|
55
|
+
router: PropTypes.object,
|
|
56
|
+
pathFor: PropTypes.func,
|
|
57
|
+
admin: PropTypes.object,
|
|
58
|
+
};
|
|
59
|
+
getChildContext() {
|
|
60
|
+
return this.props.context;
|
|
61
|
+
}
|
|
62
|
+
render() {
|
|
63
|
+
return this.props.children;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("ConfigTabContainer", () => {
|
|
68
|
+
const systemAdmin = new Admin([{ role: "system" }]);
|
|
69
|
+
const libraryManagerA = new Admin([{ role: "manager", library: "a" }]);
|
|
70
|
+
const sitewideLibrarian = new Admin([{ role: "librarian-all" }]);
|
|
71
|
+
|
|
72
|
+
const renderWithAdmin = (admin: Admin, push: jest.Mock = jest.fn()) => {
|
|
73
|
+
const context = { router: { push }, pathFor: jest.fn(), admin };
|
|
74
|
+
const result = render(
|
|
75
|
+
<LegacyContextProvider context={context}>
|
|
76
|
+
<ConfigTabContainer
|
|
77
|
+
tab={null}
|
|
78
|
+
csrfToken="token"
|
|
79
|
+
store={buildStore()}
|
|
80
|
+
editOrCreate="edit"
|
|
81
|
+
identifier="identifier"
|
|
82
|
+
/>
|
|
83
|
+
</LegacyContextProvider>
|
|
84
|
+
);
|
|
85
|
+
return { ...result, push };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const expectPanelProps = (testId: string) => {
|
|
89
|
+
const panel = screen.getByTestId(testId);
|
|
90
|
+
expect(panel).toHaveAttribute("data-csrf-token", "token");
|
|
91
|
+
expect(panel).toHaveAttribute("data-edit-or-create", "edit");
|
|
92
|
+
expect(panel).toHaveAttribute("data-identifier", "identifier");
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
describe("for system admin", () => {
|
|
96
|
+
it("shows tabs", () => {
|
|
97
|
+
renderWithAdmin(systemAdmin);
|
|
98
|
+
for (const name of [
|
|
99
|
+
"Libraries",
|
|
100
|
+
"Admins",
|
|
101
|
+
"Collections",
|
|
102
|
+
"Patron Authentication",
|
|
103
|
+
"Metadata",
|
|
104
|
+
"External Catalogs",
|
|
105
|
+
"Discovery",
|
|
106
|
+
"Sitewide Announcements",
|
|
107
|
+
]) {
|
|
108
|
+
expect(screen.getByRole("link", { name })).toBeInTheDocument();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("shows components", () => {
|
|
113
|
+
renderWithAdmin(systemAdmin);
|
|
114
|
+
for (const testId of [
|
|
115
|
+
"libraries",
|
|
116
|
+
"individual-admins",
|
|
117
|
+
"collections",
|
|
118
|
+
"patron-auth-services",
|
|
119
|
+
"metadata-services",
|
|
120
|
+
"catalog-services",
|
|
121
|
+
"discovery-services",
|
|
122
|
+
"sitewide-announcements",
|
|
123
|
+
]) {
|
|
124
|
+
expectPanelProps(testId);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("uses router to navigate when tab is clicked", async () => {
|
|
129
|
+
const { push } = renderWithAdmin(systemAdmin);
|
|
130
|
+
await userEvent.click(screen.getByRole("link", { name: "Collections" }));
|
|
131
|
+
expect(push).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(push).toHaveBeenCalledWith("/admin/web/config/collections");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("for library manager", () => {
|
|
137
|
+
it("shows tabs", () => {
|
|
138
|
+
renderWithAdmin(libraryManagerA);
|
|
139
|
+
expect(
|
|
140
|
+
screen.getByRole("link", { name: "Libraries" })
|
|
141
|
+
).toBeInTheDocument();
|
|
142
|
+
expect(screen.getByRole("link", { name: "Admins" })).toBeInTheDocument();
|
|
143
|
+
for (const name of [
|
|
144
|
+
"Collections",
|
|
145
|
+
"Patron Authentication",
|
|
146
|
+
"Metadata",
|
|
147
|
+
"External Catalogs",
|
|
148
|
+
]) {
|
|
149
|
+
expect(screen.queryByRole("link", { name })).not.toBeInTheDocument();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("shows components", () => {
|
|
154
|
+
renderWithAdmin(libraryManagerA);
|
|
155
|
+
expectPanelProps("libraries");
|
|
156
|
+
expectPanelProps("individual-admins");
|
|
157
|
+
for (const testId of [
|
|
158
|
+
"collections",
|
|
159
|
+
"patron-auth-services",
|
|
160
|
+
"metadata-services",
|
|
161
|
+
"catalog-services",
|
|
162
|
+
"discovery-services",
|
|
163
|
+
"sitewide-announcements",
|
|
164
|
+
]) {
|
|
165
|
+
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("for librarian", () => {
|
|
171
|
+
it("shows tabs", () => {
|
|
172
|
+
const { container } = renderWithAdmin(sitewideLibrarian);
|
|
173
|
+
const links = container.querySelectorAll("ul.nav-tabs a");
|
|
174
|
+
expect(links).toHaveLength(1);
|
|
175
|
+
expect(
|
|
176
|
+
screen.getByRole("link", { name: "Libraries" })
|
|
177
|
+
).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("shows components", () => {
|
|
181
|
+
renderWithAdmin(sitewideLibrarian);
|
|
182
|
+
expectPanelProps("libraries");
|
|
183
|
+
for (const testId of [
|
|
184
|
+
"individual-admins",
|
|
185
|
+
"collections",
|
|
186
|
+
"patron-auth-services",
|
|
187
|
+
"metadata-services",
|
|
188
|
+
"catalog-services",
|
|
189
|
+
"discovery-services",
|
|
190
|
+
"sitewide-announcements",
|
|
191
|
+
]) {
|
|
192
|
+
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import ContextProvider from "../../../src/components/ContextProvider";
|
|
6
|
+
import Admin from "../../../src/models/Admin";
|
|
7
|
+
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
|
|
8
|
+
import { ConfigurationSettings } from "../../../src/interfaces";
|
|
9
|
+
|
|
10
|
+
// Reads everything ContextProvider puts on the legacy React context — the real
|
|
11
|
+
// contract the app's pages consume — so we can assert on it without reaching for
|
|
12
|
+
// the component instance.
|
|
13
|
+
let captured: any;
|
|
14
|
+
class Capture extends React.Component {
|
|
15
|
+
static contextTypes = {
|
|
16
|
+
editorStore: PropTypes.object,
|
|
17
|
+
csrfToken: PropTypes.string,
|
|
18
|
+
settingUp: PropTypes.bool,
|
|
19
|
+
admin: PropTypes.object,
|
|
20
|
+
featureFlags: PropTypes.object,
|
|
21
|
+
pathFor: PropTypes.func,
|
|
22
|
+
};
|
|
23
|
+
render() {
|
|
24
|
+
captured = this.context;
|
|
25
|
+
return <div>child rendered</div>;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const renderWithConfig = (config: Partial<ConfigurationSettings>) =>
|
|
30
|
+
render(
|
|
31
|
+
<ContextProvider config={config as ConfigurationSettings}>
|
|
32
|
+
<Capture />
|
|
33
|
+
</ContextProvider>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
describe("ContextProvider", () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
captured = undefined;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("provides child context", () => {
|
|
42
|
+
renderWithConfig({
|
|
43
|
+
csrfToken: "token",
|
|
44
|
+
featureFlags: defaultFeatureFlags,
|
|
45
|
+
roles: [{ role: "system" }],
|
|
46
|
+
email: "email",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(captured.editorStore.getState().editor).toBeTruthy();
|
|
50
|
+
expect(captured.editorStore.getState().catalog).toBeTruthy();
|
|
51
|
+
expect(captured.csrfToken).toBe("token");
|
|
52
|
+
expect(captured.settingUp).toBeFalsy();
|
|
53
|
+
expect(captured.admin instanceof Admin).toBe(true);
|
|
54
|
+
expect(captured.admin.isSystemAdmin()).toBe(true);
|
|
55
|
+
expect(captured.admin.email).toBe("email");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("saves the enableAutoList feature flag to the store", () => {
|
|
59
|
+
renderWithConfig({
|
|
60
|
+
csrfToken: "token",
|
|
61
|
+
featureFlags: { enableAutoList: true },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
captured.editorStore.getState().editor.customListEditor
|
|
66
|
+
.isAutoUpdateEnabled
|
|
67
|
+
).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders its child", () => {
|
|
71
|
+
renderWithConfig({ csrfToken: "token", featureFlags: {} });
|
|
72
|
+
expect(screen.getByText("child rendered")).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("passes pathFor down through context", () => {
|
|
76
|
+
renderWithConfig({ csrfToken: "token", featureFlags: {} });
|
|
77
|
+
expect(typeof captured.pathFor).toBe("function");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("pathFor", () => {
|
|
81
|
+
// prepareCollectionUrl / prepareBookUrl strip only `document.location.origin`,
|
|
82
|
+
// so using the real jsdom origin as the host yields the same encoded output the
|
|
83
|
+
// legacy test asserted (it forced the origin to example.com via jsdom.reconfigure,
|
|
84
|
+
// which the unit test environment does not provide). These calls also exercise
|
|
85
|
+
// prepareCollectionUrl and prepareBookUrl transitively.
|
|
86
|
+
const host = window.location.origin;
|
|
87
|
+
const collectionUrl = `${host}/groups/eng/Adult%20Fiction`;
|
|
88
|
+
const bookUrl = `${host}/library/works/Axis%20360/Axis%20360%20ID/0016201449`;
|
|
89
|
+
const preparedCollection = "groups%2Feng%2FAdult%2520Fiction";
|
|
90
|
+
const preparedBook =
|
|
91
|
+
"library%2FAxis%2520360%2FAxis%2520360%2520ID%2F0016201449";
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
renderWithConfig({ csrfToken: "token", featureFlags: {} });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns a path with collection, book, and tab", () => {
|
|
98
|
+
expect(captured.pathFor(collectionUrl, bookUrl, "tab")).toBe(
|
|
99
|
+
`/admin/web/collection/${preparedCollection}/book/${preparedBook}/tab/tab`
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns a path with collection and book", () => {
|
|
104
|
+
expect(captured.pathFor(collectionUrl, bookUrl, null)).toBe(
|
|
105
|
+
`/admin/web/collection/${preparedCollection}/book/${preparedBook}`
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns a path with only a collection", () => {
|
|
110
|
+
expect(captured.pathFor(collectionUrl, null, null)).toBe(
|
|
111
|
+
`/admin/web/collection/${preparedCollection}`
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns a path with only a book", () => {
|
|
116
|
+
expect(captured.pathFor(null, bookUrl, null)).toBe(
|
|
117
|
+
`/admin/web/book/${preparedBook}`
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns a path with a book and tab", () => {
|
|
122
|
+
expect(captured.pathFor(null, bookUrl, "tab")).toBe(
|
|
123
|
+
`/admin/web/book/${preparedBook}/tab/tab`
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("returns the base path with no collection, book, or tab", () => {
|
|
128
|
+
expect(captured.pathFor(null, null, null)).toBe("/admin/web");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("leaves a book url unchanged when it does not match the works pattern", () => {
|
|
132
|
+
// prepareBookUrl returns the url as-is when the /works/ regexp misses.
|
|
133
|
+
expect(captured.pathFor(null, "not-a-works-url", null)).toBe(
|
|
134
|
+
"/admin/web/book/not-a-works-url"
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|