@thepalaceproject/circulation-admin 1.43.0-post.1 → 1.43.0-post.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +16 -7
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +56 -9
- package/package.json +6 -16
- 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/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/Autocomplete.test.tsx +67 -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/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/CatalogServices.test.tsx +63 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
- package/tests/jest/components/Classifications.test.tsx +166 -0
- package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
- package/tests/jest/components/ColorPicker.test.tsx +58 -0
- package/tests/jest/components/ComplaintForm.test.tsx +171 -0
- package/tests/jest/components/Complaints.test.tsx +259 -0
- package/tests/jest/components/ConfigPage.test.tsx +58 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomListsForBook.test.tsx +315 -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/EditableConfigList.test.tsx +8 -2
- 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/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/LaneEditor.test.tsx +2 -6
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -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 +59 -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/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/SelfTestsCategory.test.tsx +200 -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 -8
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -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/index.test.tsx +50 -0
- package/tests/jest/legacy-setup.ts +13 -0
- package/tests/jest/legacyJsdomEnvironment.js +49 -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/components/AdvancedSearchBuilder.test.tsx +0 -38
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, within } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import CustomListEditor, {
|
|
6
|
+
CustomListEditorProps,
|
|
7
|
+
} from "../../../src/components/CustomListEditor";
|
|
8
|
+
|
|
9
|
+
// Each list entry renders a web-opds-client CatalogLink whose `collectionUrl` is
|
|
10
|
+
// the list's opdsFeedUrl. Mock it to a marker that echoes that URL, so the
|
|
11
|
+
// crawlable-feed derivation can be asserted without router/pathFor context.
|
|
12
|
+
jest.mock(
|
|
13
|
+
"@thepalaceproject/web-opds-client/lib/components/CatalogLink",
|
|
14
|
+
() => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: (props: any) => (
|
|
17
|
+
<a
|
|
18
|
+
href={props.bookUrl}
|
|
19
|
+
data-testid="catalog-link"
|
|
20
|
+
data-collection-url={props.collectionUrl}
|
|
21
|
+
>
|
|
22
|
+
{props.children}
|
|
23
|
+
</a>
|
|
24
|
+
),
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe("CustomListEditor", () => {
|
|
29
|
+
// CustomListEntriesEditor calls Element.scrollTo on mount, which JSDOM does
|
|
30
|
+
// not implement, so stub it.
|
|
31
|
+
Element.prototype.scrollTo = () => {};
|
|
32
|
+
|
|
33
|
+
const searchResults = {
|
|
34
|
+
id: "id",
|
|
35
|
+
url: "url",
|
|
36
|
+
title: "title",
|
|
37
|
+
lanes: [],
|
|
38
|
+
books: [],
|
|
39
|
+
navigationLinks: [],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const entries = {
|
|
43
|
+
baseline: [],
|
|
44
|
+
baselineTotalCount: 0,
|
|
45
|
+
added: {},
|
|
46
|
+
removed: {},
|
|
47
|
+
current: [],
|
|
48
|
+
currentTotalCount: 0,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const entryPoints = ["Book", "Audio"];
|
|
52
|
+
|
|
53
|
+
const library = {
|
|
54
|
+
uuid: "uuid",
|
|
55
|
+
name: "name",
|
|
56
|
+
short_name: "library",
|
|
57
|
+
settings: {
|
|
58
|
+
large_collections: ["eng", "fre", "spa"],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const languages = {
|
|
63
|
+
eng: ["English"],
|
|
64
|
+
spa: ["Spanish", "Castilian"],
|
|
65
|
+
fre: ["French"],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const properties = {
|
|
69
|
+
name: "Listy McList",
|
|
70
|
+
collections: [2],
|
|
71
|
+
autoUpdate: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const searchParams = {
|
|
75
|
+
entryPoint: "All",
|
|
76
|
+
terms: "",
|
|
77
|
+
sort: null,
|
|
78
|
+
language: "all",
|
|
79
|
+
advanced: {
|
|
80
|
+
include: {
|
|
81
|
+
query: null,
|
|
82
|
+
selectedQueryId: null,
|
|
83
|
+
clearFilters: null,
|
|
84
|
+
},
|
|
85
|
+
exclude: {
|
|
86
|
+
query: null,
|
|
87
|
+
selectedQueryId: null,
|
|
88
|
+
clearFilters: null,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const makeProps = (
|
|
94
|
+
overrides: Partial<CustomListEditorProps> = {}
|
|
95
|
+
): CustomListEditorProps => ({
|
|
96
|
+
entries,
|
|
97
|
+
entryPoints,
|
|
98
|
+
isFetchingMoreCustomListEntries: false,
|
|
99
|
+
isFetchingSearchResults: false,
|
|
100
|
+
isFetchingMoreSearchResults: false,
|
|
101
|
+
isLoaded: false,
|
|
102
|
+
isModified: true,
|
|
103
|
+
isOwner: true,
|
|
104
|
+
isValid: true,
|
|
105
|
+
languages,
|
|
106
|
+
library: library as CustomListEditorProps["library"],
|
|
107
|
+
listId: "1",
|
|
108
|
+
properties: properties as CustomListEditorProps["properties"],
|
|
109
|
+
searchParams: searchParams as CustomListEditorProps["searchParams"],
|
|
110
|
+
searchResults: searchResults as CustomListEditorProps["searchResults"],
|
|
111
|
+
loadMoreEntries: jest.fn(),
|
|
112
|
+
loadMoreSearchResults: jest.fn(),
|
|
113
|
+
reset: jest.fn(),
|
|
114
|
+
save: jest.fn().mockResolvedValue(undefined),
|
|
115
|
+
search: jest.fn(),
|
|
116
|
+
share: jest.fn(),
|
|
117
|
+
updateProperty: jest.fn(),
|
|
118
|
+
updateSearchParam: jest.fn(),
|
|
119
|
+
...overrides,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const renderEditor = (overrides: Partial<CustomListEditorProps> = {}) => {
|
|
123
|
+
const props = makeProps(overrides);
|
|
124
|
+
const result = render(<CustomListEditor {...props} />);
|
|
125
|
+
const rerender = (nextOverrides: Partial<CustomListEditorProps>) =>
|
|
126
|
+
result.rerender(<CustomListEditor {...makeProps(nextOverrides)} />);
|
|
127
|
+
return { ...result, rerender, props };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
it("shows the list title with an edit button", () => {
|
|
131
|
+
renderEditor();
|
|
132
|
+
|
|
133
|
+
// The editable title (TextWithEditMode) shows the name and an edit button.
|
|
134
|
+
expect(screen.getByText("Listy McList")).toBeInTheDocument();
|
|
135
|
+
expect(
|
|
136
|
+
screen.getByRole("button", { name: /Edit list title/ })
|
|
137
|
+
).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("shows the list title without an edit button when isOwner is false", () => {
|
|
141
|
+
renderEditor({ isOwner: false });
|
|
142
|
+
|
|
143
|
+
const title = screen.getByRole("heading", { level: 3 });
|
|
144
|
+
expect(title).toHaveTextContent("Listy McList");
|
|
145
|
+
expect(
|
|
146
|
+
screen.queryByRole("button", { name: /Edit list title/ })
|
|
147
|
+
).not.toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("shows the list id", () => {
|
|
151
|
+
renderEditor();
|
|
152
|
+
|
|
153
|
+
const listId = screen.getByRole("heading", { level: 4, name: /ID-1/ });
|
|
154
|
+
expect(listId).toHaveTextContent("1");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("shows an entries editor with list entries and search results", () => {
|
|
158
|
+
renderEditor();
|
|
159
|
+
|
|
160
|
+
// The entries editor renders both the search results and list-entries columns.
|
|
161
|
+
expect(
|
|
162
|
+
screen.getByRole("heading", { name: "Search Results" })
|
|
163
|
+
).toBeInTheDocument();
|
|
164
|
+
expect(
|
|
165
|
+
screen.getByRole("heading", {
|
|
166
|
+
name: /List Entries:.*No books in this list/,
|
|
167
|
+
})
|
|
168
|
+
).toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("gives each entry of a saved list a crawlable OPDS feed URL", () => {
|
|
172
|
+
// opdsFeedUrl is `${short_name}/lists/${savedName}/crawlable`, but only when
|
|
173
|
+
// the list has both a listId and a savedName (CustomListEditor.tsx:124). It
|
|
174
|
+
// reaches the DOM as the collectionUrl of each list entry's catalog link.
|
|
175
|
+
renderEditor({
|
|
176
|
+
savedName: "saved-list",
|
|
177
|
+
entries: {
|
|
178
|
+
...entries,
|
|
179
|
+
current: [{ id: "book1", title: "A Book", url: "/works/book1" }],
|
|
180
|
+
currentTotalCount: 1,
|
|
181
|
+
} as CustomListEditorProps["entries"],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(screen.getByTestId("catalog-link")).toHaveAttribute(
|
|
185
|
+
"data-collection-url",
|
|
186
|
+
"library/lists/saved-list/crawlable"
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("disables entry point options when isOwner is false", () => {
|
|
191
|
+
const { container } = renderEditor({ isOwner: false });
|
|
192
|
+
|
|
193
|
+
const entryPointsSection =
|
|
194
|
+
container.querySelector<HTMLElement>(".entry-points");
|
|
195
|
+
within(entryPointsSection)
|
|
196
|
+
.getAllByRole("radio")
|
|
197
|
+
.forEach((radio) => expect(radio).toBeDisabled());
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("shows entry point options", () => {
|
|
201
|
+
const { container } = renderEditor();
|
|
202
|
+
|
|
203
|
+
const entryPointsSection =
|
|
204
|
+
container.querySelector<HTMLElement>(".entry-points");
|
|
205
|
+
|
|
206
|
+
const all = within(entryPointsSection).getByRole("radio", { name: "All" });
|
|
207
|
+
expect(all).toHaveAttribute("value", "All");
|
|
208
|
+
expect(all).toBeChecked();
|
|
209
|
+
|
|
210
|
+
const book = within(entryPointsSection).getByRole("radio", {
|
|
211
|
+
name: "Book",
|
|
212
|
+
});
|
|
213
|
+
expect(book).toHaveAttribute("value", "Book");
|
|
214
|
+
expect(book).not.toBeChecked();
|
|
215
|
+
|
|
216
|
+
const audio = within(entryPointsSection).getByRole("radio", {
|
|
217
|
+
name: "Audio",
|
|
218
|
+
});
|
|
219
|
+
expect(audio).toHaveAttribute("value", "Audio");
|
|
220
|
+
expect(audio).not.toBeChecked();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("enables the save button when the list has changes and is valid", () => {
|
|
224
|
+
renderEditor();
|
|
225
|
+
|
|
226
|
+
expect(
|
|
227
|
+
screen.getByRole("button", { name: "Save this list" })
|
|
228
|
+
).toBeEnabled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("disables the save button when the list does not have changes", () => {
|
|
232
|
+
renderEditor({ isModified: false });
|
|
233
|
+
|
|
234
|
+
expect(
|
|
235
|
+
screen.getByRole("button", { name: "Save this list" })
|
|
236
|
+
).toBeDisabled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("disables the save button when the list is invalid", () => {
|
|
240
|
+
renderEditor({ isValid: false });
|
|
241
|
+
|
|
242
|
+
expect(
|
|
243
|
+
screen.getByRole("button", { name: "Save this list" })
|
|
244
|
+
).toBeDisabled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("calls save when the save button is clicked", async () => {
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
const save = jest.fn().mockResolvedValue(undefined);
|
|
250
|
+
renderEditor({ save });
|
|
251
|
+
|
|
252
|
+
await user.click(screen.getByRole("button", { name: "Save this list" }));
|
|
253
|
+
|
|
254
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("disables the cancel button when the list does not have changes", () => {
|
|
258
|
+
renderEditor({ isModified: false });
|
|
259
|
+
|
|
260
|
+
expect(
|
|
261
|
+
screen.getByRole("button", { name: "Cancel changes" })
|
|
262
|
+
).toBeDisabled();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("calls reset when the cancel button is clicked", async () => {
|
|
266
|
+
const user = userEvent.setup();
|
|
267
|
+
const reset = jest.fn();
|
|
268
|
+
renderEditor({ reset });
|
|
269
|
+
|
|
270
|
+
await user.click(screen.getByRole("button", { name: "Cancel changes" }));
|
|
271
|
+
|
|
272
|
+
expect(reset).toHaveBeenCalledTimes(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("does not render save/cancel buttons when isOwner is false", () => {
|
|
276
|
+
renderEditor({ isOwner: false });
|
|
277
|
+
|
|
278
|
+
expect(
|
|
279
|
+
screen.queryByRole("button", { name: "Save this list" })
|
|
280
|
+
).not.toBeInTheDocument();
|
|
281
|
+
expect(
|
|
282
|
+
screen.queryByRole("button", { name: "Cancel changes" })
|
|
283
|
+
).not.toBeInTheDocument();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("renders a CustomListSearch component", () => {
|
|
287
|
+
const { container } = renderEditor();
|
|
288
|
+
|
|
289
|
+
// CustomListSearch renders its search panel (a collapsible Panel whose
|
|
290
|
+
// header is a button) with the entry point options.
|
|
291
|
+
expect(
|
|
292
|
+
screen.getByRole("button", { name: "Search for titles" })
|
|
293
|
+
).toBeInTheDocument();
|
|
294
|
+
expect(container.querySelector(".entry-points")).toBeInTheDocument();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("updates the autoUpdate property from the search component", async () => {
|
|
298
|
+
const user = userEvent.setup();
|
|
299
|
+
// The auto-update controls only render when auto-update is enabled and the
|
|
300
|
+
// list is new (listId null), which is when the radios are interactive.
|
|
301
|
+
const updateProperty = jest.fn();
|
|
302
|
+
renderEditor({
|
|
303
|
+
updateProperty,
|
|
304
|
+
isAutoUpdateEnabled: true,
|
|
305
|
+
listId: null,
|
|
306
|
+
properties: { ...properties, autoUpdate: false },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await user.click(
|
|
310
|
+
screen.getByRole("radio", { name: "Automatically update this list" })
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
expect(updateProperty).toHaveBeenCalledTimes(1);
|
|
314
|
+
expect(updateProperty).toHaveBeenCalledWith("autoUpdate", true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("calls search when listId changes", () => {
|
|
318
|
+
const search = jest.fn();
|
|
319
|
+
const { rerender } = renderEditor({ search });
|
|
320
|
+
|
|
321
|
+
expect(search).toHaveBeenCalledTimes(1);
|
|
322
|
+
|
|
323
|
+
rerender({ search, listId: "2" });
|
|
324
|
+
|
|
325
|
+
expect(search).toHaveBeenCalledTimes(2);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("calls search when isLoaded changes", () => {
|
|
329
|
+
const search = jest.fn();
|
|
330
|
+
const { rerender } = renderEditor({ search });
|
|
331
|
+
|
|
332
|
+
expect(search).toHaveBeenCalledTimes(1);
|
|
333
|
+
|
|
334
|
+
rerender({ search, isLoaded: true });
|
|
335
|
+
|
|
336
|
+
expect(search).toHaveBeenCalledTimes(2);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("calls share when the share button is clicked", async () => {
|
|
340
|
+
const user = userEvent.setup();
|
|
341
|
+
const share = jest.fn();
|
|
342
|
+
const { container } = renderEditor({ share });
|
|
343
|
+
|
|
344
|
+
const shareButton = container.querySelector<HTMLButtonElement>(
|
|
345
|
+
".sharing-info button"
|
|
346
|
+
);
|
|
347
|
+
expect(shareButton).toBeInTheDocument();
|
|
348
|
+
|
|
349
|
+
await user.click(shareButton);
|
|
350
|
+
|
|
351
|
+
expect(share).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("does not render a share button when listId is undefined", () => {
|
|
355
|
+
const { container } = renderEditor({ listId: undefined });
|
|
356
|
+
|
|
357
|
+
expect(container.querySelector(".sharing-info")).not.toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("does not render a share button when isOwner is false", () => {
|
|
361
|
+
const { container } = renderEditor({ isOwner: false });
|
|
362
|
+
|
|
363
|
+
expect(
|
|
364
|
+
container.querySelector(".sharing-info button")
|
|
365
|
+
).not.toBeInTheDocument();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
// CustomLists is a connected component that fetches lists, collections, libraries,
|
|
18
|
+
// and languages over the network on mount. That is incidental to what
|
|
19
|
+
// CustomListPage composes — the page's job is to hand the editor the params and
|
|
20
|
+
// context it needs — so mock it to a marker that echoes the props it received.
|
|
21
|
+
jest.mock("../../../src/components/CustomLists", () => ({
|
|
22
|
+
__esModule: true,
|
|
23
|
+
default: (props: any) => (
|
|
24
|
+
<div
|
|
25
|
+
data-testid="custom-lists"
|
|
26
|
+
data-library={props.library}
|
|
27
|
+
data-edit-or-create={props.editOrCreate}
|
|
28
|
+
data-identifier={props.identifier}
|
|
29
|
+
data-csrf-token={props.csrfToken}
|
|
30
|
+
data-has-store={String(!!props.store)}
|
|
31
|
+
/>
|
|
32
|
+
),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
import CustomListPage from "../../../src/components/CustomListPage";
|
|
36
|
+
|
|
37
|
+
describe("CustomListPage", () => {
|
|
38
|
+
const params = {
|
|
39
|
+
library: "library",
|
|
40
|
+
editOrCreate: "edit",
|
|
41
|
+
identifier: "identifier",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
it("shows custom lists with info from params", () => {
|
|
45
|
+
renderWithProviders(<CustomListPage params={params} />, {
|
|
46
|
+
appConfigSettings: { csrfToken: "token" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const customLists = screen.getByTestId("custom-lists");
|
|
50
|
+
expect(customLists).toHaveAttribute("data-library", "library");
|
|
51
|
+
expect(customLists).toHaveAttribute("data-edit-or-create", "edit");
|
|
52
|
+
expect(customLists).toHaveAttribute("data-identifier", "identifier");
|
|
53
|
+
expect(customLists).toHaveAttribute("data-csrf-token", "token");
|
|
54
|
+
// The redux store is forwarded from context; assert it was passed through.
|
|
55
|
+
expect(customLists).toHaveAttribute("data-has-store", "true");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,275 @@
|
|
|
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 CustomListSearch from "../../../src/components/CustomListSearch";
|
|
5
|
+
|
|
6
|
+
describe("CustomListSearch", () => {
|
|
7
|
+
const library = {
|
|
8
|
+
uuid: "uuid",
|
|
9
|
+
name: "name",
|
|
10
|
+
short_name: "short_name",
|
|
11
|
+
settings: {
|
|
12
|
+
large_collections: ["eng", "fre", "spa"],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const entryPoints = ["All", "Book", "Audio"];
|
|
17
|
+
|
|
18
|
+
const searchParams = {
|
|
19
|
+
entryPoint: "all",
|
|
20
|
+
terms: "foo bar",
|
|
21
|
+
sort: "title",
|
|
22
|
+
language: "English",
|
|
23
|
+
advanced: {
|
|
24
|
+
include: {
|
|
25
|
+
query: null,
|
|
26
|
+
selectedQueryId: null,
|
|
27
|
+
clearFilters: null,
|
|
28
|
+
},
|
|
29
|
+
exclude: {
|
|
30
|
+
query: null,
|
|
31
|
+
selectedQueryId: null,
|
|
32
|
+
clearFilters: null,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const languages = {
|
|
38
|
+
eng: ["English"],
|
|
39
|
+
spa: ["Spanish", "Castilian"],
|
|
40
|
+
fre: ["French"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const renderSearch = (
|
|
44
|
+
overrides: Partial<React.ComponentProps<typeof CustomListSearch>> = {}
|
|
45
|
+
) =>
|
|
46
|
+
render(
|
|
47
|
+
<CustomListSearch
|
|
48
|
+
entryPoints={entryPoints}
|
|
49
|
+
isOwner={true}
|
|
50
|
+
languages={languages}
|
|
51
|
+
library={library}
|
|
52
|
+
listId="123"
|
|
53
|
+
search={jest.fn()}
|
|
54
|
+
searchParams={searchParams}
|
|
55
|
+
updateAutoUpdate={jest.fn()}
|
|
56
|
+
updateSearchParam={jest.fn()}
|
|
57
|
+
{...overrides}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
it("renders a radio button for each entry point", () => {
|
|
62
|
+
const { container } = renderSearch();
|
|
63
|
+
const entryPointsSection =
|
|
64
|
+
container.querySelector<HTMLElement>(".entry-points");
|
|
65
|
+
|
|
66
|
+
const radios = within(entryPointsSection).getAllByRole("radio");
|
|
67
|
+
expect(radios).toHaveLength(3);
|
|
68
|
+
|
|
69
|
+
const all = within(entryPointsSection).getByRole("radio", { name: "All" });
|
|
70
|
+
expect(all).toHaveAttribute("name", "entry-points-selection");
|
|
71
|
+
expect(all).toHaveAttribute("value", "All");
|
|
72
|
+
expect(all).toBeEnabled();
|
|
73
|
+
expect(all).toBeChecked();
|
|
74
|
+
|
|
75
|
+
const book = within(entryPointsSection).getByRole("radio", {
|
|
76
|
+
name: "Book",
|
|
77
|
+
});
|
|
78
|
+
expect(book).toHaveAttribute("name", "entry-points-selection");
|
|
79
|
+
expect(book).toHaveAttribute("value", "Book");
|
|
80
|
+
expect(book).toBeEnabled();
|
|
81
|
+
expect(book).not.toBeChecked();
|
|
82
|
+
|
|
83
|
+
const audio = within(entryPointsSection).getByRole("radio", {
|
|
84
|
+
name: "Audio",
|
|
85
|
+
});
|
|
86
|
+
expect(audio).toHaveAttribute("name", "entry-points-selection");
|
|
87
|
+
expect(audio).toHaveAttribute("value", "Audio");
|
|
88
|
+
expect(audio).toBeEnabled();
|
|
89
|
+
expect(audio).not.toBeChecked();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("disables the entry point radio buttons when isOwner is false", () => {
|
|
93
|
+
const { container } = renderSearch({ isOwner: false });
|
|
94
|
+
const entryPointsSection =
|
|
95
|
+
container.querySelector<HTMLElement>(".entry-points");
|
|
96
|
+
|
|
97
|
+
within(entryPointsSection)
|
|
98
|
+
.getAllByRole("radio")
|
|
99
|
+
.forEach((radio) => expect(radio).toBeDisabled());
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("calls updateSearchParam when an entry point radio button is changed", async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
const updateSearchParam = jest.fn();
|
|
105
|
+
const { container } = renderSearch({ updateSearchParam });
|
|
106
|
+
const entryPointsSection =
|
|
107
|
+
container.querySelector<HTMLElement>(".entry-points");
|
|
108
|
+
|
|
109
|
+
await user.click(
|
|
110
|
+
within(entryPointsSection).getByRole("radio", { name: "Audio" })
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(updateSearchParam).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(updateSearchParam).toHaveBeenCalledWith("entryPoint", "Audio");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("renders a radio button for each sort option", () => {
|
|
118
|
+
const { container } = renderSearch();
|
|
119
|
+
const sortSection = container.querySelector<HTMLElement>(".search-options");
|
|
120
|
+
|
|
121
|
+
const radios = within(sortSection).getAllByRole("radio");
|
|
122
|
+
expect(radios).toHaveLength(3);
|
|
123
|
+
|
|
124
|
+
const relevance = within(sortSection).getByRole("radio", {
|
|
125
|
+
name: "Relevance",
|
|
126
|
+
});
|
|
127
|
+
expect(relevance).toHaveAttribute("name", "sort-selection");
|
|
128
|
+
expect(relevance).toHaveAttribute("value", "");
|
|
129
|
+
expect(relevance).toBeEnabled();
|
|
130
|
+
expect(relevance).not.toBeChecked();
|
|
131
|
+
|
|
132
|
+
const title = within(sortSection).getByRole("radio", { name: "Title" });
|
|
133
|
+
expect(title).toHaveAttribute("name", "sort-selection");
|
|
134
|
+
expect(title).toHaveAttribute("value", "title");
|
|
135
|
+
expect(title).toBeEnabled();
|
|
136
|
+
expect(title).toBeChecked();
|
|
137
|
+
|
|
138
|
+
const author = within(sortSection).getByRole("radio", { name: "Author" });
|
|
139
|
+
expect(author).toHaveAttribute("name", "sort-selection");
|
|
140
|
+
expect(author).toHaveAttribute("value", "author");
|
|
141
|
+
expect(author).toBeEnabled();
|
|
142
|
+
expect(author).not.toBeChecked();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("disables the sort option radio buttons when isOwner is false", () => {
|
|
146
|
+
const { container } = renderSearch({ isOwner: false });
|
|
147
|
+
const sortSection = container.querySelector<HTMLElement>(".search-options");
|
|
148
|
+
|
|
149
|
+
within(sortSection)
|
|
150
|
+
.getAllByRole("radio")
|
|
151
|
+
.forEach((radio) => expect(radio).toBeDisabled());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("calls updateSearchParam when a sort radio button is changed", async () => {
|
|
155
|
+
const user = userEvent.setup();
|
|
156
|
+
const updateSearchParam = jest.fn();
|
|
157
|
+
const { container } = renderSearch({ updateSearchParam });
|
|
158
|
+
const sortSection = container.querySelector<HTMLElement>(".search-options");
|
|
159
|
+
|
|
160
|
+
await user.click(
|
|
161
|
+
within(sortSection).getByRole("radio", { name: "Author" })
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(updateSearchParam).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(updateSearchParam).toHaveBeenCalledWith("sort", "author");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("calls addAdvSearchQuery and search when mounted if there is a startingTitle", () => {
|
|
169
|
+
const addAdvSearchQuery = jest.fn();
|
|
170
|
+
const search = jest.fn();
|
|
171
|
+
|
|
172
|
+
renderSearch({ startingTitle: "test", addAdvSearchQuery, search });
|
|
173
|
+
|
|
174
|
+
expect(addAdvSearchQuery).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(addAdvSearchQuery).toHaveBeenCalledWith("include", {
|
|
176
|
+
key: "title",
|
|
177
|
+
op: "eq",
|
|
178
|
+
value: "test",
|
|
179
|
+
});
|
|
180
|
+
expect(search).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("when showAutoUpdate is false", () => {
|
|
184
|
+
it("does not render radio buttons for auto update on and off", () => {
|
|
185
|
+
const { container } = renderSearch();
|
|
186
|
+
|
|
187
|
+
expect(container.querySelector(".auto-update")).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("when showAutoUpdate is true", () => {
|
|
192
|
+
it("renders radio buttons for auto update on and off", () => {
|
|
193
|
+
const { container } = renderSearch({
|
|
194
|
+
showAutoUpdate: true,
|
|
195
|
+
listId: null,
|
|
196
|
+
});
|
|
197
|
+
const autoSection = container.querySelector<HTMLElement>(".auto-update");
|
|
198
|
+
|
|
199
|
+
const radios = within(autoSection).getAllByRole("radio");
|
|
200
|
+
expect(radios).toHaveLength(2);
|
|
201
|
+
|
|
202
|
+
const on = within(autoSection).getByRole("radio", {
|
|
203
|
+
name: "Automatically update this list",
|
|
204
|
+
});
|
|
205
|
+
expect(on).toHaveAttribute("name", "auto-update");
|
|
206
|
+
expect(on).toBeEnabled();
|
|
207
|
+
|
|
208
|
+
const off = within(autoSection).getByRole("radio", {
|
|
209
|
+
name: "Manually select titles",
|
|
210
|
+
});
|
|
211
|
+
expect(off).toHaveAttribute("name", "auto-update");
|
|
212
|
+
expect(off).toBeEnabled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("disables the radio buttons for auto update when isOwner is false", () => {
|
|
216
|
+
const { container } = renderSearch({
|
|
217
|
+
showAutoUpdate: true,
|
|
218
|
+
isOwner: false,
|
|
219
|
+
listId: null,
|
|
220
|
+
});
|
|
221
|
+
const autoSection = container.querySelector<HTMLElement>(".auto-update");
|
|
222
|
+
|
|
223
|
+
within(autoSection)
|
|
224
|
+
.getAllByRole("radio")
|
|
225
|
+
.forEach((radio) => expect(radio).toBeDisabled());
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("disables the radio buttons for auto update when listId is not null", () => {
|
|
229
|
+
// isOwner: true keeps readOnly false, so listId is the only thing that can
|
|
230
|
+
// disable the radios — otherwise readOnly alone would, masking this path.
|
|
231
|
+
const { container } = renderSearch({
|
|
232
|
+
showAutoUpdate: true,
|
|
233
|
+
isOwner: true,
|
|
234
|
+
listId: "123",
|
|
235
|
+
});
|
|
236
|
+
const autoSection = container.querySelector<HTMLElement>(".auto-update");
|
|
237
|
+
|
|
238
|
+
within(autoSection)
|
|
239
|
+
.getAllByRole("radio")
|
|
240
|
+
.forEach((radio) => expect(radio).toBeDisabled());
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("calls updateAutoUpdate when an auto update radio button is changed", async () => {
|
|
244
|
+
const user = userEvent.setup();
|
|
245
|
+
// Clicking the currently-unchecked "on" radio reports true.
|
|
246
|
+
const updateAutoUpdateOn = jest.fn();
|
|
247
|
+
const { unmount } = renderSearch({
|
|
248
|
+
showAutoUpdate: true,
|
|
249
|
+
listId: null,
|
|
250
|
+
autoUpdate: false,
|
|
251
|
+
updateAutoUpdate: updateAutoUpdateOn,
|
|
252
|
+
});
|
|
253
|
+
await user.click(
|
|
254
|
+
screen.getByRole("radio", { name: "Automatically update this list" })
|
|
255
|
+
);
|
|
256
|
+
expect(updateAutoUpdateOn).toHaveBeenCalledTimes(1);
|
|
257
|
+
expect(updateAutoUpdateOn).toHaveBeenCalledWith(true);
|
|
258
|
+
unmount();
|
|
259
|
+
|
|
260
|
+
// Clicking the currently-unchecked "off" radio reports false.
|
|
261
|
+
const updateAutoUpdateOff = jest.fn();
|
|
262
|
+
renderSearch({
|
|
263
|
+
showAutoUpdate: true,
|
|
264
|
+
listId: null,
|
|
265
|
+
autoUpdate: true,
|
|
266
|
+
updateAutoUpdate: updateAutoUpdateOff,
|
|
267
|
+
});
|
|
268
|
+
await user.click(
|
|
269
|
+
screen.getByRole("radio", { name: "Manually select titles" })
|
|
270
|
+
);
|
|
271
|
+
expect(updateAutoUpdateOff).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect(updateAutoUpdateOff).toHaveBeenCalledWith(false);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|