@thepalaceproject/circulation-admin 1.43.0-post.11 → 1.43.0-post.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
- package/tests/jest/components/CatalogPage.test.tsx +158 -0
- package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
- package/tests/jest/components/Collections.test.tsx +335 -7
- package/tests/jest/components/Contributors.test.tsx +157 -0
- package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
- package/tests/jest/components/CustomLists.test.tsx +621 -4
- package/tests/jest/components/EditableConfigList.test.tsx +598 -7
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/GenreForm.test.tsx +152 -0
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/Lane.test.tsx +130 -7
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +367 -2
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/ProtocolFormField.test.tsx +443 -7
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
package/package.json
CHANGED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { installFormDataShim } from "../testUtils/formDataShim";
|
|
3
|
+
import { act, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
7
|
+
import buildStore from "../../../src/store";
|
|
8
|
+
// Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
|
|
9
|
+
// are exercised: `preview`, `rightsStatuses`, `isFetching`, the fetch errors, and
|
|
10
|
+
// `bookAdminUrl` are all fed in through the Redux store, while `book`, `bookUrl`,
|
|
11
|
+
// `csrfToken`, and `refreshCatalog` are own props.
|
|
12
|
+
import BookCoverEditor from "../../../src/components/BookCoverEditor";
|
|
13
|
+
import { BookData, RightsStatusData } from "../../../src/interfaces";
|
|
14
|
+
// The reusable-components `Form` builds `new FormData(formElement)` on
|
|
15
|
+
// submit, which the unit jsdom env's undici FormData rejects; install the
|
|
16
|
+
// shared shim that reads the form's successful controls.
|
|
17
|
+
installFormDataShim();
|
|
18
|
+
const rightsStatuses: RightsStatusData = {
|
|
19
|
+
"http://creativecommons.org/licenses/by/4.0/": {
|
|
20
|
+
allows_derivatives: true,
|
|
21
|
+
name: "Creative Commons Attribution (CC BY)",
|
|
22
|
+
open_access: true,
|
|
23
|
+
},
|
|
24
|
+
"http://librarysimplified.org/terms/rights-status/in-copyright": {
|
|
25
|
+
allows_derivatives: false,
|
|
26
|
+
name: "In Copyright",
|
|
27
|
+
open_access: false,
|
|
28
|
+
},
|
|
29
|
+
"https://creativecommons.org/licenses/by-nd/4.0": {
|
|
30
|
+
allows_derivatives: false,
|
|
31
|
+
name: "Creative Commons Attribution-NoDerivs (CC BY-ND)",
|
|
32
|
+
open_access: true,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const bookData: BookData = {
|
|
37
|
+
id: "id",
|
|
38
|
+
title: "title",
|
|
39
|
+
coverUrl: "/cover",
|
|
40
|
+
changeCoverLink: {
|
|
41
|
+
href: "/change_cover",
|
|
42
|
+
rel: "http://librarysimplified.org/terms/rel/change_cover",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe("BookCoverEditor", () => {
|
|
47
|
+
let fetchSpy: jest.SpyInstance;
|
|
48
|
+
|
|
49
|
+
// The connected component fetches the rights statuses on mount and posts the
|
|
50
|
+
// cover preview / edited cover. Serve those endpoints and answer anything else
|
|
51
|
+
// (e.g. the OPDS book re-fetch) benignly so nothing rejects after teardown.
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
fetchSpy = jest.spyOn(globalThis, "fetch").mockImplementation((async (
|
|
54
|
+
url: string
|
|
55
|
+
) => {
|
|
56
|
+
const u = String(url);
|
|
57
|
+
if (u.includes("rights_status")) {
|
|
58
|
+
return new Response(JSON.stringify(rightsStatuses), {
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (u.includes("preview_book_cover")) {
|
|
64
|
+
return new Response("image data", { status: 200 });
|
|
65
|
+
}
|
|
66
|
+
return new Response("", { status: 200 });
|
|
67
|
+
}) as any);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
jest.restoreAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Each test gets a fresh store so state does not leak. `preSeed` runs before
|
|
75
|
+
// mount; note the component clears the preview on mount, so preview-slice state
|
|
76
|
+
// must be dispatched after render instead.
|
|
77
|
+
const renderEditor = (
|
|
78
|
+
preSeed?: (store: ReturnType<typeof buildStore>) => void
|
|
79
|
+
) => {
|
|
80
|
+
const store = buildStore();
|
|
81
|
+
preSeed?.(store);
|
|
82
|
+
const refreshCatalog = jest.fn().mockResolvedValue(undefined);
|
|
83
|
+
const utils = renderWithProviders(
|
|
84
|
+
<BookCoverEditor
|
|
85
|
+
csrfToken="token"
|
|
86
|
+
bookUrl="/book"
|
|
87
|
+
book={bookData}
|
|
88
|
+
refreshCatalog={refreshCatalog}
|
|
89
|
+
/>,
|
|
90
|
+
{ reduxProviderProps: { store } }
|
|
91
|
+
);
|
|
92
|
+
return { store, refreshCatalog, ...utils };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// The rights inputs only appear once the on-mount fetch resolves; awaiting them
|
|
96
|
+
// both settles that fetch and confirms the rights statuses were fetched.
|
|
97
|
+
const awaitMounted = () =>
|
|
98
|
+
screen.findByText("License") as Promise<HTMLElement>;
|
|
99
|
+
|
|
100
|
+
describe("rendering", () => {
|
|
101
|
+
it("shows the book title", async () => {
|
|
102
|
+
renderEditor();
|
|
103
|
+
await awaitMounted();
|
|
104
|
+
expect(
|
|
105
|
+
screen.getByRole("heading", { level: 2, name: "title" })
|
|
106
|
+
).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("shows the current cover", async () => {
|
|
110
|
+
const { container } = renderEditor();
|
|
111
|
+
await awaitMounted();
|
|
112
|
+
const cover = container.querySelector(".current-cover");
|
|
113
|
+
expect(cover).toBeInTheDocument();
|
|
114
|
+
expect(cover).toHaveAttribute("src", bookData.coverUrl);
|
|
115
|
+
expect(cover).toHaveAttribute("alt", "Current book cover");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("shows the cover URL and cover file inputs", async () => {
|
|
119
|
+
const { container } = renderEditor();
|
|
120
|
+
await awaitMounted();
|
|
121
|
+
|
|
122
|
+
expect(screen.getByText("URL for cover image")).toBeInTheDocument();
|
|
123
|
+
expect(
|
|
124
|
+
container.querySelector('input[name="cover_url"]')
|
|
125
|
+
).toBeInTheDocument();
|
|
126
|
+
|
|
127
|
+
expect(screen.getByText("Or upload cover image")).toBeInTheDocument();
|
|
128
|
+
const fileInput = container.querySelector('input[name="cover_file"]');
|
|
129
|
+
expect(fileInput).toHaveAttribute("type", "file");
|
|
130
|
+
expect(fileInput).toHaveAttribute("accept", "image/*");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("shows the title position select", async () => {
|
|
134
|
+
const { container } = renderEditor();
|
|
135
|
+
await awaitMounted();
|
|
136
|
+
|
|
137
|
+
expect(screen.getByText("Title and Author Position")).toBeInTheDocument();
|
|
138
|
+
const select = container.querySelector('select[name="title_position"]');
|
|
139
|
+
expect(select).toBeInTheDocument();
|
|
140
|
+
const options = select.querySelectorAll("option");
|
|
141
|
+
expect(Array.from(options).map((o) => o.getAttribute("value"))).toEqual([
|
|
142
|
+
"none",
|
|
143
|
+
"top",
|
|
144
|
+
"center",
|
|
145
|
+
"bottom",
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("shows the rights inputs", async () => {
|
|
150
|
+
const { container } = renderEditor();
|
|
151
|
+
await awaitMounted();
|
|
152
|
+
|
|
153
|
+
const rightsSelect = container.querySelector(
|
|
154
|
+
'select[name="rights_status"]'
|
|
155
|
+
);
|
|
156
|
+
expect(rightsSelect).toBeInTheDocument();
|
|
157
|
+
const options = Array.from(rightsSelect.querySelectorAll("option"));
|
|
158
|
+
expect(options).toHaveLength(3);
|
|
159
|
+
expect(options[0].getAttribute("value")).toBe(
|
|
160
|
+
"http://creativecommons.org/licenses/by/4.0/"
|
|
161
|
+
);
|
|
162
|
+
expect(options[0].textContent).toBe(
|
|
163
|
+
"Creative Commons Attribution (CC BY)"
|
|
164
|
+
);
|
|
165
|
+
expect(options[1].getAttribute("value")).toBe(
|
|
166
|
+
"http://librarysimplified.org/terms/rights-status/in-copyright"
|
|
167
|
+
);
|
|
168
|
+
expect(options[1].textContent).toBe("In Copyright");
|
|
169
|
+
expect(options[2].getAttribute("value")).toBe(
|
|
170
|
+
"http://librarysimplified.org/terms/rights-status/unknown"
|
|
171
|
+
);
|
|
172
|
+
expect(options[2].textContent).toBe("Other");
|
|
173
|
+
|
|
174
|
+
expect(screen.getByText("Explanation of rights")).toBeInTheDocument();
|
|
175
|
+
expect(
|
|
176
|
+
container.querySelector('textarea[name="rights_explanation"]')
|
|
177
|
+
).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("shows the updating loader while fetching", async () => {
|
|
181
|
+
const { container } = renderEditor((store) =>
|
|
182
|
+
store.dispatch({ type: "BOOK_COVER_REQUEST" })
|
|
183
|
+
);
|
|
184
|
+
await awaitMounted();
|
|
185
|
+
|
|
186
|
+
// Only the top loader (driven by `isFetching`) shows; the preview loader
|
|
187
|
+
// (driven by `isFetchingPreview`) does not.
|
|
188
|
+
const loaders = container.querySelectorAll(".updating-loader");
|
|
189
|
+
expect(loaders).toHaveLength(1);
|
|
190
|
+
expect(loaders[0]).toHaveTextContent("Updating");
|
|
191
|
+
expect(loaders[0]).not.toHaveTextContent("Updating Preview");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("shows the preview updating message", async () => {
|
|
195
|
+
const { store, container } = renderEditor();
|
|
196
|
+
await awaitMounted();
|
|
197
|
+
expect(container.querySelectorAll(".updating-loader")).toHaveLength(0);
|
|
198
|
+
|
|
199
|
+
act(() => {
|
|
200
|
+
store.dispatch({ type: "PREVIEW_BOOK_COVER_REQUEST" });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const loaders = container.querySelectorAll(".updating-loader");
|
|
204
|
+
expect(loaders).toHaveLength(1);
|
|
205
|
+
expect(loaders[0]).toHaveTextContent("Updating Preview");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("shows the preview once it is loaded", async () => {
|
|
209
|
+
const { store, container } = renderEditor();
|
|
210
|
+
await awaitMounted();
|
|
211
|
+
expect(container.querySelector(".preview-cover")).toBeNull();
|
|
212
|
+
|
|
213
|
+
act(() => {
|
|
214
|
+
store.dispatch({ type: "PREVIEW_BOOK_COVER_LOAD", data: "image data" });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const preview = container.querySelector(".preview-cover");
|
|
218
|
+
expect(preview).toBeInTheDocument();
|
|
219
|
+
expect(preview).toHaveAttribute("src", "image data");
|
|
220
|
+
expect(preview).toHaveAttribute("alt", "Preview of new cover");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("shows a fetch error", async () => {
|
|
224
|
+
const { store } = renderEditor();
|
|
225
|
+
await awaitMounted();
|
|
226
|
+
expect(screen.queryByText(/Error:/)).not.toBeInTheDocument();
|
|
227
|
+
|
|
228
|
+
act(() => {
|
|
229
|
+
store.dispatch({
|
|
230
|
+
type: "BOOK_COVER_FAILURE",
|
|
231
|
+
error: { status: 500, url: "url", response: "fetch failed" },
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(screen.getByText("Error: fetch failed")).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("shows a preview fetch error", async () => {
|
|
239
|
+
const { store } = renderEditor();
|
|
240
|
+
await awaitMounted();
|
|
241
|
+
expect(screen.queryByText(/Error:/)).not.toBeInTheDocument();
|
|
242
|
+
|
|
243
|
+
act(() => {
|
|
244
|
+
store.dispatch({
|
|
245
|
+
type: "PREVIEW_BOOK_COVER_FAILURE",
|
|
246
|
+
error: { status: 500, url: "url", response: "preview failed" },
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(screen.getByText("Error: preview failed")).toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("enables the save button only once there is a preview", async () => {
|
|
254
|
+
const { store } = renderEditor();
|
|
255
|
+
await awaitMounted();
|
|
256
|
+
|
|
257
|
+
const save = screen.getByRole("button", { name: "Save this cover" });
|
|
258
|
+
expect(save).toBeDisabled();
|
|
259
|
+
|
|
260
|
+
act(() => {
|
|
261
|
+
store.dispatch({ type: "PREVIEW_BOOK_COVER_LOAD", data: "image data" });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(
|
|
265
|
+
screen.getByRole("button", { name: "Save this cover" })
|
|
266
|
+
).toBeEnabled();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("mount behavior", () => {
|
|
271
|
+
it("fetches the rights statuses on mount", async () => {
|
|
272
|
+
renderEditor();
|
|
273
|
+
// The rights inputs only render after the statuses are fetched.
|
|
274
|
+
expect(await screen.findByText("License")).toBeInTheDocument();
|
|
275
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
276
|
+
"/admin/rights_status",
|
|
277
|
+
expect.anything()
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("clears any stale preview on mount", async () => {
|
|
282
|
+
const { store, container } = renderEditor((store) =>
|
|
283
|
+
store.dispatch({ type: "PREVIEW_BOOK_COVER_LOAD", data: "stale" })
|
|
284
|
+
);
|
|
285
|
+
await awaitMounted();
|
|
286
|
+
|
|
287
|
+
// The stale preview was cleared during mount, so it never renders.
|
|
288
|
+
expect(container.querySelector(".preview-cover")).toBeNull();
|
|
289
|
+
expect(store.getState().editor.bookCoverPreview.data).toBeNull();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("behavior", () => {
|
|
294
|
+
it("previews the cover when the inputs change and the button is clicked", async () => {
|
|
295
|
+
const user = userEvent.setup();
|
|
296
|
+
const { container } = renderEditor();
|
|
297
|
+
await awaitMounted();
|
|
298
|
+
|
|
299
|
+
await user.type(
|
|
300
|
+
container.querySelector('input[name="cover_url"]'),
|
|
301
|
+
"http://example.com"
|
|
302
|
+
);
|
|
303
|
+
await user.click(screen.getByRole("button", { name: "Preview" }));
|
|
304
|
+
|
|
305
|
+
// The preview arrives from the store and is displayed.
|
|
306
|
+
const preview = await screen.findByAltText("Preview of new cover");
|
|
307
|
+
expect(preview).toHaveAttribute("src", "image data");
|
|
308
|
+
|
|
309
|
+
// The preview was posted with the entered cover URL and title position.
|
|
310
|
+
const previewCall = fetchSpy.mock.calls.find(([u]) =>
|
|
311
|
+
String(u).includes("preview_book_cover")
|
|
312
|
+
);
|
|
313
|
+
expect(previewCall).toBeTruthy();
|
|
314
|
+
expect(previewCall[1].method).toBe("POST");
|
|
315
|
+
expect(previewCall[1].body.get("cover_url")).toBe("http://example.com");
|
|
316
|
+
expect(previewCall[1].body.get("title_position")).toBe("none");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("clears the preview when the inputs are emptied", async () => {
|
|
320
|
+
const user = userEvent.setup();
|
|
321
|
+
const { store } = renderEditor();
|
|
322
|
+
await awaitMounted();
|
|
323
|
+
// Seed a preview so we can observe it being cleared.
|
|
324
|
+
act(() => {
|
|
325
|
+
store.dispatch({ type: "PREVIEW_BOOK_COVER_LOAD", data: "image data" });
|
|
326
|
+
});
|
|
327
|
+
expect(screen.getByAltText("Preview of new cover")).toBeInTheDocument();
|
|
328
|
+
|
|
329
|
+
// With no cover URL or file, submitting the preview form clears it.
|
|
330
|
+
await user.click(screen.getByRole("button", { name: "Preview" }));
|
|
331
|
+
|
|
332
|
+
await waitFor(() =>
|
|
333
|
+
expect(
|
|
334
|
+
screen.queryByAltText("Preview of new cover")
|
|
335
|
+
).not.toBeInTheDocument()
|
|
336
|
+
);
|
|
337
|
+
// No preview request was posted, since there was nothing to preview.
|
|
338
|
+
expect(
|
|
339
|
+
fetchSpy.mock.calls.some(([u]) =>
|
|
340
|
+
String(u).includes("preview_book_cover")
|
|
341
|
+
)
|
|
342
|
+
).toBe(false);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("saves the cover with the entered values", async () => {
|
|
346
|
+
const user = userEvent.setup();
|
|
347
|
+
const { store, refreshCatalog } = renderEditor();
|
|
348
|
+
await awaitMounted();
|
|
349
|
+
|
|
350
|
+
// A preview must exist for the save button to be enabled.
|
|
351
|
+
act(() => {
|
|
352
|
+
store.dispatch({ type: "PREVIEW_BOOK_COVER_LOAD", data: "image data" });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await user.type(
|
|
356
|
+
document.querySelector('input[name="cover_url"]') as HTMLElement,
|
|
357
|
+
"http://example.com"
|
|
358
|
+
);
|
|
359
|
+
await user.selectOptions(
|
|
360
|
+
document.querySelector('select[name="title_position"]') as HTMLElement,
|
|
361
|
+
"center"
|
|
362
|
+
);
|
|
363
|
+
await user.selectOptions(
|
|
364
|
+
document.querySelector('select[name="rights_status"]') as HTMLElement,
|
|
365
|
+
"http://creativecommons.org/licenses/by/4.0/"
|
|
366
|
+
);
|
|
367
|
+
await user.type(
|
|
368
|
+
document.querySelector(
|
|
369
|
+
'textarea[name="rights_explanation"]'
|
|
370
|
+
) as HTMLElement,
|
|
371
|
+
"explanation"
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
await user.click(screen.getByRole("button", { name: "Save this cover" }));
|
|
375
|
+
|
|
376
|
+
// The edited cover is posted to the exact change-cover link with the form
|
|
377
|
+
// values, and on success the catalog is refreshed.
|
|
378
|
+
const editCall = fetchSpy.mock.calls.find(
|
|
379
|
+
([u]) => String(u) === bookData.changeCoverLink.href
|
|
380
|
+
);
|
|
381
|
+
expect(editCall).toBeTruthy();
|
|
382
|
+
expect(editCall[1].method).toBe("POST");
|
|
383
|
+
expect(editCall[1].body.get("cover_url")).toBe("http://example.com");
|
|
384
|
+
expect(editCall[1].body.get("title_position")).toBe("center");
|
|
385
|
+
expect(editCall[1].body.get("rights_status")).toBe(
|
|
386
|
+
"http://creativecommons.org/licenses/by/4.0/"
|
|
387
|
+
);
|
|
388
|
+
expect(editCall[1].body.get("rights_explanation")).toBe("explanation");
|
|
389
|
+
|
|
390
|
+
await waitFor(() => expect(refreshCatalog).toHaveBeenCalledTimes(1));
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
import title from "../../../src/utils/title";
|
|
6
|
+
|
|
7
|
+
// CatalogPage does `require("@thepalaceproject/web-opds-client")` and mounts the OPDS
|
|
8
|
+
// Web Client, a heavy third-party render target that fetches on mount. Replace it
|
|
9
|
+
// with a marker that echoes the props CatalogPage hands it and the legacy
|
|
10
|
+
// `tab`/`library` child context CatalogPage supplies (read via `contextTypes`), so
|
|
11
|
+
// we can assert the wiring without the real catalog.
|
|
12
|
+
jest.mock("@thepalaceproject/web-opds-client", () => {
|
|
13
|
+
const React = require("react");
|
|
14
|
+
const PropTypes = require("prop-types");
|
|
15
|
+
const OPDSCatalogMock = (props, context) => (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="opds-catalog"
|
|
18
|
+
data-collection-url={props.collectionUrl}
|
|
19
|
+
data-book-url={props.bookUrl}
|
|
20
|
+
data-tab={context.tab}
|
|
21
|
+
data-library={context.library && context.library()}
|
|
22
|
+
data-header-name={props.Header && props.Header.name}
|
|
23
|
+
data-book-details-container-name={
|
|
24
|
+
props.BookDetailsContainer && props.BookDetailsContainer.name
|
|
25
|
+
}
|
|
26
|
+
data-has-compute-breadcrumbs={String(!!props.computeBreadcrumbs)}
|
|
27
|
+
data-title-book={
|
|
28
|
+
props.pageTitleTemplate && props.pageTitleTemplate("Collection", "Book")
|
|
29
|
+
}
|
|
30
|
+
data-title-collection={
|
|
31
|
+
props.pageTitleTemplate && props.pageTitleTemplate("Collection", null)
|
|
32
|
+
}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
OPDSCatalogMock.contextTypes = {
|
|
36
|
+
tab: PropTypes.string,
|
|
37
|
+
library: PropTypes.func,
|
|
38
|
+
};
|
|
39
|
+
return OPDSCatalogMock;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// When there is no library, CatalogPage renders WelcomePage, which mounts the real
|
|
43
|
+
// Header — that needs react-router v3 context and fetches on mount. Mock Header to a
|
|
44
|
+
// marker so the WelcomePage path stays self-contained.
|
|
45
|
+
jest.mock("../../../src/components/Header", () => ({
|
|
46
|
+
__esModule: true,
|
|
47
|
+
default: function Header() {
|
|
48
|
+
return <div data-testid="header" />;
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
import CatalogPage from "../../../src/components/CatalogPage";
|
|
53
|
+
import Header from "../../../src/components/Header";
|
|
54
|
+
import BookDetailsContainer from "../../../src/components/BookDetailsContainer";
|
|
55
|
+
|
|
56
|
+
// The unit env has no `global.jsdom`, so compute the expected URLs from the real
|
|
57
|
+
// origin.
|
|
58
|
+
const origin = window.location.origin;
|
|
59
|
+
|
|
60
|
+
describe("CatalogPage", () => {
|
|
61
|
+
const baseParams = {
|
|
62
|
+
collectionUrl: "library/collectionurl",
|
|
63
|
+
bookUrl: "library/bookurl",
|
|
64
|
+
tab: "tab",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it("renders the OPDS catalog with expanded collection and book URLs", () => {
|
|
68
|
+
renderWithProviders(<CatalogPage params={baseParams} />);
|
|
69
|
+
|
|
70
|
+
const catalog = screen.getByTestId("opds-catalog");
|
|
71
|
+
expect(catalog).toHaveAttribute(
|
|
72
|
+
"data-collection-url",
|
|
73
|
+
`${origin}/library/collectionurl`
|
|
74
|
+
);
|
|
75
|
+
expect(catalog).toHaveAttribute(
|
|
76
|
+
"data-book-url",
|
|
77
|
+
`${origin}/library/works/bookurl`
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("forwards the admin header, book details container, breadcrumbs, and page-title template", () => {
|
|
82
|
+
renderWithProviders(<CatalogPage params={baseParams} />);
|
|
83
|
+
|
|
84
|
+
const catalog = screen.getByTestId("opds-catalog");
|
|
85
|
+
expect(catalog).toHaveAttribute("data-header-name", Header.name);
|
|
86
|
+
expect(catalog).toHaveAttribute(
|
|
87
|
+
"data-book-details-container-name",
|
|
88
|
+
BookDetailsContainer.name
|
|
89
|
+
);
|
|
90
|
+
expect(catalog).toHaveAttribute("data-has-compute-breadcrumbs", "true");
|
|
91
|
+
// pageTitleTemplate prefers the book title over the collection title.
|
|
92
|
+
expect(catalog).toHaveAttribute("data-title-book", title("Book"));
|
|
93
|
+
expect(catalog).toHaveAttribute(
|
|
94
|
+
"data-title-collection",
|
|
95
|
+
title("Collection")
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("expands a collection URL that already contains a query string", () => {
|
|
100
|
+
renderWithProviders(
|
|
101
|
+
<CatalogPage
|
|
102
|
+
params={{
|
|
103
|
+
...baseParams,
|
|
104
|
+
collectionUrl: "library/collectionurl?samplequery=test",
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
|
|
110
|
+
"data-collection-url",
|
|
111
|
+
`${origin}/library/collectionurl?samplequery=test`
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("puts the tab into the child context", () => {
|
|
116
|
+
renderWithProviders(<CatalogPage params={baseParams} />);
|
|
117
|
+
expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
|
|
118
|
+
"data-tab",
|
|
119
|
+
"tab"
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("derives the child-context library from the collection URL", () => {
|
|
124
|
+
renderWithProviders(<CatalogPage params={baseParams} />);
|
|
125
|
+
expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
|
|
126
|
+
"data-library",
|
|
127
|
+
"library"
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("derives the child-context library from the book URL when there is no collection URL", () => {
|
|
132
|
+
renderWithProviders(
|
|
133
|
+
<CatalogPage params={{ ...baseParams, collectionUrl: null }} />
|
|
134
|
+
);
|
|
135
|
+
expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
|
|
136
|
+
"data-library",
|
|
137
|
+
"library"
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("renders the welcome page when there is no library", () => {
|
|
142
|
+
renderWithProviders(
|
|
143
|
+
<CatalogPage params={{ collectionUrl: null, bookUrl: null, tab: null }} />
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.queryByTestId("opds-catalog")).not.toBeInTheDocument();
|
|
147
|
+
expect(
|
|
148
|
+
screen.getByText(/Welcome to the Circulation Admin Interface!/)
|
|
149
|
+
).toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("renders the footer", () => {
|
|
153
|
+
const { container } = renderWithProviders(
|
|
154
|
+
<CatalogPage params={baseParams} />
|
|
155
|
+
);
|
|
156
|
+
expect(container.querySelector("footer")).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
});
|