@thepalaceproject/circulation-admin 1.43.0-post.9 → 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 +22 -10
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/jest.config.js +11 -44
- package/package.json +1 -5
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +111 -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/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 +38 -1
- package/tests/jest/components/ChangePasswordForm.test.tsx +163 -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/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/CustomLists.test.tsx +621 -4
- package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
- package/tests/jest/components/EditableConfigList.test.tsx +598 -7
- package/tests/jest/components/EditableInput.test.tsx +250 -1
- package/tests/jest/components/EditorField.test.tsx +95 -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/Lane.test.tsx +132 -10
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +377 -9
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -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/MetadataServices.test.tsx +37 -1
- 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/ProtocolFormField.test.tsx +443 -7
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +0 -7
- package/tests/jest/components/TextWithEditMode.test.tsx +218 -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/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/README.md +0 -6
- package/tests/jest/legacy-setup.ts +0 -13
- package/tests/jest/legacyJsdomEnvironment.js +0 -49
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
import buildStore from "../../../src/store";
|
|
6
|
+
import {
|
|
7
|
+
PER_LIBRARY_SUPPRESS_REL,
|
|
8
|
+
PER_LIBRARY_UNSUPPRESS_REL,
|
|
9
|
+
} from "../../../src/features/book/bookEditorSlice";
|
|
10
|
+
|
|
11
|
+
// BookEditForm is a heavy child; the legacy enzyme tests only shallow-rendered
|
|
12
|
+
// it and asserted the props it received. Mock it to a marker that surfaces those
|
|
13
|
+
// props as data attributes so the same facts can be asserted against real DOM.
|
|
14
|
+
jest.mock("../../../src/components/BookEditForm", () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: (props: any) => (
|
|
17
|
+
<div
|
|
18
|
+
data-testid="book-edit-form"
|
|
19
|
+
data-title={props.title}
|
|
20
|
+
data-roles={JSON.stringify(props.roles ?? null)}
|
|
21
|
+
data-media={JSON.stringify(props.media ?? null)}
|
|
22
|
+
data-languages={JSON.stringify(props.languages ?? null)}
|
|
23
|
+
/>
|
|
24
|
+
),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import BookDetailsEditor, {
|
|
28
|
+
BookDetailsEditor as UnconnectedBookDetailsEditor,
|
|
29
|
+
} from "../../../src/components/BookDetailsEditor";
|
|
30
|
+
|
|
31
|
+
// The connected component derives bookData from OPDS parsing of a fetch, which is
|
|
32
|
+
// impractical to synthesize; render the unconnected class with injected props for
|
|
33
|
+
// the render-behavior tests (as the legacy enzyme tests did), and cover the
|
|
34
|
+
// connect wiring with the dedicated connected test below.
|
|
35
|
+
const renderEditor = (overrides: Record<string, unknown> = {}) => {
|
|
36
|
+
const props = {
|
|
37
|
+
bookUrl: "url",
|
|
38
|
+
csrfToken: "token",
|
|
39
|
+
canSuppress: true,
|
|
40
|
+
bookAdminUrl: "admin url",
|
|
41
|
+
bookData: undefined,
|
|
42
|
+
roles: undefined,
|
|
43
|
+
media: undefined,
|
|
44
|
+
languages: undefined,
|
|
45
|
+
isFetching: false,
|
|
46
|
+
fetchError: undefined,
|
|
47
|
+
editError: undefined,
|
|
48
|
+
refreshCatalog: jest.fn().mockResolvedValue(undefined),
|
|
49
|
+
fetchBookData: jest.fn(),
|
|
50
|
+
fetchRoles: jest.fn(),
|
|
51
|
+
fetchMedia: jest.fn(),
|
|
52
|
+
fetchLanguages: jest.fn(),
|
|
53
|
+
postBookData: jest.fn(),
|
|
54
|
+
suppressBook: jest.fn(),
|
|
55
|
+
unsuppressBook: jest.fn(),
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
const result = render(<UnconnectedBookDetailsEditor {...(props as any)} />);
|
|
59
|
+
return { props, ...result };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("BookDetailsEditor", () => {
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
jest.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("loads admin book url, roles, languages, and media on mount", () => {
|
|
68
|
+
const { props } = renderEditor({ bookUrl: "works/1234" });
|
|
69
|
+
|
|
70
|
+
expect(props.fetchBookData).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(props.fetchBookData).toHaveBeenCalledWith("admin/works/1234");
|
|
72
|
+
expect(props.fetchRoles).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(props.fetchMedia).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(props.fetchLanguages).toHaveBeenCalledTimes(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("loads admin book url when given a new book url", () => {
|
|
78
|
+
const { props, rerender } = renderEditor({ bookUrl: "works/1234" });
|
|
79
|
+
rerender(
|
|
80
|
+
<UnconnectedBookDetailsEditor {...(props as any)} bookUrl="works/5555" />
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(props.fetchBookData).toHaveBeenCalledTimes(2);
|
|
84
|
+
expect((props.fetchBookData as jest.Mock).mock.calls[1][0]).toBe(
|
|
85
|
+
"admin/works/5555"
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("shows the title", () => {
|
|
90
|
+
renderEditor({ bookData: { id: "id", title: "title" } });
|
|
91
|
+
expect(
|
|
92
|
+
screen.getByRole("heading", { level: 2, name: "title" })
|
|
93
|
+
).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("shows a Hide button for a per-library suppress link", () => {
|
|
97
|
+
renderEditor({
|
|
98
|
+
bookData: {
|
|
99
|
+
id: "id",
|
|
100
|
+
title: "title",
|
|
101
|
+
suppressPerLibraryLink: { href: "href", rel: PER_LIBRARY_SUPPRESS_REL },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(screen.getByRole("button", { name: "Hide" })).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("shows a Restore button in the visibility banner for a per-library unsuppress link", () => {
|
|
108
|
+
const { container } = renderEditor({
|
|
109
|
+
bookData: {
|
|
110
|
+
id: "id",
|
|
111
|
+
title: "title",
|
|
112
|
+
unsuppressPerLibraryLink: {
|
|
113
|
+
href: "href",
|
|
114
|
+
rel: PER_LIBRARY_UNSUPPRESS_REL,
|
|
115
|
+
},
|
|
116
|
+
visibilityStatus: "manually-suppressed",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
expect(container.querySelector(".visibility-status")).toBeInTheDocument();
|
|
120
|
+
expect(screen.getByRole("button", { name: "Restore" })).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("shows the restore banner when an unsuppress link exists without a visibilityStatus", () => {
|
|
124
|
+
const { container } = renderEditor({
|
|
125
|
+
bookData: {
|
|
126
|
+
id: "id",
|
|
127
|
+
title: "title",
|
|
128
|
+
unsuppressPerLibraryLink: {
|
|
129
|
+
href: "href",
|
|
130
|
+
rel: PER_LIBRARY_UNSUPPRESS_REL,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
expect(
|
|
135
|
+
container.querySelector(".visibility-status-manually-suppressed")
|
|
136
|
+
).toBeInTheDocument();
|
|
137
|
+
expect(screen.getByRole("button", { name: "Restore" })).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("shows the manually-suppressed banner without a restore button when there is no unsuppress link", () => {
|
|
141
|
+
const { container } = renderEditor({
|
|
142
|
+
bookData: {
|
|
143
|
+
id: "id",
|
|
144
|
+
title: "title",
|
|
145
|
+
visibilityStatus: "manually-suppressed",
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const banner = container.querySelector(
|
|
149
|
+
".visibility-status-manually-suppressed"
|
|
150
|
+
);
|
|
151
|
+
expect(banner).toBeInTheDocument();
|
|
152
|
+
expect(banner.textContent).toContain(
|
|
153
|
+
"manually hidden by a library staff member"
|
|
154
|
+
);
|
|
155
|
+
expect(
|
|
156
|
+
screen.queryByRole("button", { name: "Restore" })
|
|
157
|
+
).not.toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("shows the policy-filtered banner without a restore button", () => {
|
|
161
|
+
const { container } = renderEditor({
|
|
162
|
+
bookData: {
|
|
163
|
+
id: "id",
|
|
164
|
+
title: "title",
|
|
165
|
+
visibilityStatus: "policy-filtered",
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const banner = container.querySelector(
|
|
169
|
+
".visibility-status-policy-filtered"
|
|
170
|
+
);
|
|
171
|
+
expect(banner).toBeInTheDocument();
|
|
172
|
+
expect(banner.textContent).toContain("library content filtering");
|
|
173
|
+
expect(
|
|
174
|
+
screen.queryByRole("button", { name: "Restore" })
|
|
175
|
+
).not.toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("shows a Refresh Metadata button for a refresh link", () => {
|
|
179
|
+
renderEditor({
|
|
180
|
+
bookData: {
|
|
181
|
+
id: "id",
|
|
182
|
+
title: "title",
|
|
183
|
+
refreshLink: {
|
|
184
|
+
href: "href",
|
|
185
|
+
rel: "http://librarysimplified/terms/rel/refresh",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
expect(
|
|
190
|
+
screen.getByRole("button", { name: "Refresh Metadata" })
|
|
191
|
+
).toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("shows a fetch error message and no edit form", () => {
|
|
195
|
+
renderEditor({
|
|
196
|
+
bookData: { id: "id", title: "title" },
|
|
197
|
+
fetchError: { status: 500, response: "response", url: "" },
|
|
198
|
+
});
|
|
199
|
+
expect(screen.getByText(/Error: response/)).toBeInTheDocument();
|
|
200
|
+
expect(screen.queryByTestId("book-edit-form")).not.toBeInTheDocument();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("shows an edit error message together with the edit form", () => {
|
|
204
|
+
renderEditor({
|
|
205
|
+
bookData: {
|
|
206
|
+
id: "id",
|
|
207
|
+
title: "title",
|
|
208
|
+
editLink: {
|
|
209
|
+
href: "href",
|
|
210
|
+
rel: "http://librarysimplified.org/terms/rel/edit",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
editError: { status: 500, response: "response", url: "" },
|
|
214
|
+
});
|
|
215
|
+
expect(screen.getByTestId("book-edit-form")).toBeInTheDocument();
|
|
216
|
+
expect(screen.getByText(/Error: response/)).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("shows the book edit form with roles, media, and languages", () => {
|
|
220
|
+
const roles = { aut: "Author", nar: "Narrator" };
|
|
221
|
+
const media = {
|
|
222
|
+
"http://schema.org/AudioObject": "Audio",
|
|
223
|
+
"http://schema.org/Book": "Book",
|
|
224
|
+
};
|
|
225
|
+
const languages = { eng: ["English"], spa: ["Spanish", "Castilian"] };
|
|
226
|
+
renderEditor({
|
|
227
|
+
bookData: {
|
|
228
|
+
id: "id",
|
|
229
|
+
title: "title",
|
|
230
|
+
editLink: {
|
|
231
|
+
href: "href",
|
|
232
|
+
rel: "http://librarysimplified.org/terms/rel/edit",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
roles,
|
|
236
|
+
media,
|
|
237
|
+
languages,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const form = screen.getByTestId("book-edit-form");
|
|
241
|
+
expect(form).toHaveAttribute("data-title", "title");
|
|
242
|
+
expect(JSON.parse(form.getAttribute("data-roles"))).toStrictEqual(roles);
|
|
243
|
+
expect(JSON.parse(form.getAttribute("data-media"))).toStrictEqual(media);
|
|
244
|
+
expect(JSON.parse(form.getAttribute("data-languages"))).toStrictEqual(
|
|
245
|
+
languages
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("wires up the connected default export (mapStateToProps / mapDispatchToProps)", async () => {
|
|
250
|
+
// Stub fetch so mounting the connected panel does not hit the network. The
|
|
251
|
+
// roles/media/languages endpoints return JSON; the book endpoint returns a
|
|
252
|
+
// non-OPDS body so getBookData rejects (handled) and surfaces a fetch error.
|
|
253
|
+
jest
|
|
254
|
+
.spyOn(globalThis, "fetch")
|
|
255
|
+
.mockImplementation(async () => new Response("{}", { status: 200 }));
|
|
256
|
+
|
|
257
|
+
const store = buildStore();
|
|
258
|
+
renderWithProviders(
|
|
259
|
+
<BookDetailsEditor
|
|
260
|
+
store={store}
|
|
261
|
+
bookUrl="works/1234"
|
|
262
|
+
csrfToken="token"
|
|
263
|
+
canSuppress={true}
|
|
264
|
+
refreshCatalog={jest.fn().mockResolvedValue(undefined)}
|
|
265
|
+
/>,
|
|
266
|
+
{ reduxProviderProps: { store } }
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// getBookData's OPDS parse fails on the non-OPDS body, so mapStateToProps
|
|
270
|
+
// feeds the resulting fetch error back in and the panel renders an error.
|
|
271
|
+
expect(
|
|
272
|
+
await screen.findByText(/Failed to parse OPDS data/)
|
|
273
|
+
).toBeInTheDocument();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { Provider } from "react-redux";
|
|
4
|
+
import { render, screen } from "@testing-library/react";
|
|
5
|
+
import userEvent from "@testing-library/user-event";
|
|
6
|
+
|
|
7
|
+
import buildStore from "../../../src/store";
|
|
8
|
+
import { getBookData } from "../../../src/features/book/bookEditorSlice";
|
|
9
|
+
|
|
10
|
+
// Each tab panel is a connected component that fetches over the network on mount.
|
|
11
|
+
// This container's behavior is the tab SET, tab SWITCHING, and clearing book data,
|
|
12
|
+
// not the panels' internals, so mock each panel to a marker echoing the props the
|
|
13
|
+
// container is responsible for threading into it.
|
|
14
|
+
jest.mock("../../../src/components/BookDetailsEditor", () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: (props: any) => (
|
|
17
|
+
<div
|
|
18
|
+
data-testid="book-details-editor"
|
|
19
|
+
data-csrf-token={props.csrfToken}
|
|
20
|
+
data-book-url={props.bookUrl}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
}));
|
|
24
|
+
jest.mock("../../../src/components/Classifications", () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
default: (props: any) => (
|
|
27
|
+
<div data-testid="classifications" data-book-url={props.bookUrl} />
|
|
28
|
+
),
|
|
29
|
+
}));
|
|
30
|
+
jest.mock("../../../src/components/CustomListsForBook", () => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
default: (props: any) => (
|
|
33
|
+
<div
|
|
34
|
+
data-testid="custom-lists-for-book"
|
|
35
|
+
data-book-url={props.bookUrl}
|
|
36
|
+
data-library={props.library}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
}));
|
|
40
|
+
jest.mock("../../../src/components/BookCoverEditor", () => ({
|
|
41
|
+
__esModule: true,
|
|
42
|
+
default: (props: any) => (
|
|
43
|
+
<div data-testid="book-cover-editor" data-book-url={props.bookUrl} />
|
|
44
|
+
),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
import BookDetailsTabContainerConnected, {
|
|
48
|
+
BookDetailsTabContainer,
|
|
49
|
+
} from "../../../src/components/BookDetailsTabContainer";
|
|
50
|
+
|
|
51
|
+
// The container reads `router` and `pathFor` from the legacy React context the
|
|
52
|
+
// app supplies in production. Wrap it in a small provider that hands it those.
|
|
53
|
+
class LegacyContextProvider extends React.Component<{ context: any }> {
|
|
54
|
+
static childContextTypes = {
|
|
55
|
+
router: PropTypes.object,
|
|
56
|
+
pathFor: PropTypes.func,
|
|
57
|
+
};
|
|
58
|
+
getChildContext() {
|
|
59
|
+
return this.props.context;
|
|
60
|
+
}
|
|
61
|
+
render() {
|
|
62
|
+
return this.props.children;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const TEST_BOOK_URL = "book url";
|
|
67
|
+
const TEST_COLLECTION_URL = "collection url";
|
|
68
|
+
|
|
69
|
+
describe("BookDetailsTabContainer", () => {
|
|
70
|
+
const pathFor = (collectionUrl: string, bookUrl: string, tab?: string) =>
|
|
71
|
+
`${collectionUrl}::${bookUrl}::${tab}`;
|
|
72
|
+
|
|
73
|
+
type Overrides = {
|
|
74
|
+
bookData?: any;
|
|
75
|
+
bookUrl?: string;
|
|
76
|
+
clearBook?: jest.Mock;
|
|
77
|
+
push?: jest.Mock;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const renderContainer = (overrides: Overrides = {}) => {
|
|
81
|
+
const push = overrides.push || jest.fn();
|
|
82
|
+
const clearBook = overrides.clearBook || jest.fn();
|
|
83
|
+
const context = { router: { push }, pathFor };
|
|
84
|
+
// One store for the lifetime of the render: a fresh one per element() call
|
|
85
|
+
// would hand the panels a new store identity on every rerender.
|
|
86
|
+
const store = buildStore();
|
|
87
|
+
const element = (bookUrl: string, bookData: any) => (
|
|
88
|
+
<LegacyContextProvider context={context}>
|
|
89
|
+
<BookDetailsTabContainer
|
|
90
|
+
bookUrl={bookUrl}
|
|
91
|
+
collectionUrl={TEST_COLLECTION_URL}
|
|
92
|
+
csrfToken="token"
|
|
93
|
+
refreshCatalog={jest.fn()}
|
|
94
|
+
store={store}
|
|
95
|
+
library={() => "library"}
|
|
96
|
+
canSuppress={true}
|
|
97
|
+
complaintsCount={0}
|
|
98
|
+
bookData={bookData}
|
|
99
|
+
clearBook={clearBook}
|
|
100
|
+
tab={null}
|
|
101
|
+
>
|
|
102
|
+
<div className="bookDetails">Moby Dick</div>
|
|
103
|
+
</BookDetailsTabContainer>
|
|
104
|
+
</LegacyContextProvider>
|
|
105
|
+
);
|
|
106
|
+
const result = render(
|
|
107
|
+
element(overrides.bookUrl ?? TEST_BOOK_URL, overrides.bookData ?? null)
|
|
108
|
+
);
|
|
109
|
+
const rerenderWith = (bookUrl: string, bookData: any = null) =>
|
|
110
|
+
result.rerender(element(bookUrl, bookData));
|
|
111
|
+
return { ...result, push, clearBook, rerenderWith };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
it("shows book details", () => {
|
|
115
|
+
renderContainer();
|
|
116
|
+
expect(screen.getByText("Moby Dick")).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("shows details, edit, and classifications tabs", () => {
|
|
120
|
+
renderContainer();
|
|
121
|
+
for (const name of ["Details", "Edit", "Classifications"]) {
|
|
122
|
+
expect(screen.getByRole("link", { name })).toBeInTheDocument();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("only shows cover tab when the book data has a change cover link", () => {
|
|
127
|
+
const { rerenderWith } = renderContainer();
|
|
128
|
+
expect(
|
|
129
|
+
screen.queryByRole("link", { name: "Cover" })
|
|
130
|
+
).not.toBeInTheDocument();
|
|
131
|
+
|
|
132
|
+
rerenderWith(TEST_BOOK_URL, {
|
|
133
|
+
title: "title",
|
|
134
|
+
changeCoverLink: {
|
|
135
|
+
href: "/change-cover",
|
|
136
|
+
rel: "http://librarysimplified.org/terms/rel/change_cover",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
expect(screen.getByRole("link", { name: "Cover" })).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("shows editor", () => {
|
|
143
|
+
renderContainer();
|
|
144
|
+
const editor = screen.getByTestId("book-details-editor");
|
|
145
|
+
expect(editor).toHaveAttribute("data-csrf-token", "token");
|
|
146
|
+
expect(editor).toHaveAttribute("data-book-url", TEST_BOOK_URL);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("shows classifications", () => {
|
|
150
|
+
renderContainer();
|
|
151
|
+
expect(screen.getByTestId("classifications")).toHaveAttribute(
|
|
152
|
+
"data-book-url",
|
|
153
|
+
TEST_BOOK_URL
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("shows lists", () => {
|
|
158
|
+
renderContainer();
|
|
159
|
+
const lists = screen.getByTestId("custom-lists-for-book");
|
|
160
|
+
expect(lists).toHaveAttribute("data-book-url", TEST_BOOK_URL);
|
|
161
|
+
expect(lists).toHaveAttribute("data-library", "library");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("uses router to navigate when tab is clicked", async () => {
|
|
165
|
+
const { push } = renderContainer();
|
|
166
|
+
await userEvent.click(screen.getByRole("link", { name: "Edit" }));
|
|
167
|
+
expect(push).toHaveBeenCalledTimes(1);
|
|
168
|
+
expect(push).toHaveBeenCalledWith(
|
|
169
|
+
pathFor(TEST_COLLECTION_URL, TEST_BOOK_URL, "edit")
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("clears book data when receiving new book url", () => {
|
|
174
|
+
const clearBook = jest.fn();
|
|
175
|
+
const { rerenderWith } = renderContainer({ clearBook });
|
|
176
|
+
rerenderWith("new book url");
|
|
177
|
+
expect(clearBook).toHaveBeenCalledTimes(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("clears book data on unmount", () => {
|
|
181
|
+
const clearBook = jest.fn();
|
|
182
|
+
const { unmount } = renderContainer({ clearBook });
|
|
183
|
+
unmount();
|
|
184
|
+
expect(clearBook).toHaveBeenCalledTimes(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Render the CONNECTED default export so mapStateToProps / mapDispatchToProps
|
|
188
|
+
// (which run only when the connected component mounts) are exercised.
|
|
189
|
+
const renderConnected = (store = buildStore()) =>
|
|
190
|
+
render(
|
|
191
|
+
<Provider store={store}>
|
|
192
|
+
<LegacyContextProvider
|
|
193
|
+
context={{ router: { push: jest.fn() }, pathFor }}
|
|
194
|
+
>
|
|
195
|
+
<BookDetailsTabContainerConnected
|
|
196
|
+
bookUrl={TEST_BOOK_URL}
|
|
197
|
+
collectionUrl={TEST_COLLECTION_URL}
|
|
198
|
+
csrfToken="token"
|
|
199
|
+
refreshCatalog={jest.fn()}
|
|
200
|
+
library={() => "library"}
|
|
201
|
+
canSuppress={true}
|
|
202
|
+
tab={null}
|
|
203
|
+
>
|
|
204
|
+
<div className="bookDetails">Moby Dick</div>
|
|
205
|
+
</BookDetailsTabContainerConnected>
|
|
206
|
+
</LegacyContextProvider>
|
|
207
|
+
</Provider>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
it("wires up store and dispatch when connected", () => {
|
|
211
|
+
renderConnected();
|
|
212
|
+
expect(screen.getByRole("link", { name: "Details" })).toBeInTheDocument();
|
|
213
|
+
expect(screen.getByText("Moby Dick")).toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("takes book data from the store when connected", () => {
|
|
217
|
+
const store = buildStore();
|
|
218
|
+
store.dispatch(
|
|
219
|
+
getBookData.fulfilled(
|
|
220
|
+
{
|
|
221
|
+
title: "title",
|
|
222
|
+
changeCoverLink: {
|
|
223
|
+
href: "/change-cover",
|
|
224
|
+
rel: "http://librarysimplified.org/terms/rel/change_cover",
|
|
225
|
+
},
|
|
226
|
+
} as any,
|
|
227
|
+
"request-id",
|
|
228
|
+
{ url: TEST_BOOK_URL } as any
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
renderConnected(store);
|
|
232
|
+
// The cover tab appears only when bookData carries a changeCoverLink, so it
|
|
233
|
+
// is the observable proof that mapStateToProps threaded the store's book
|
|
234
|
+
// data into the container.
|
|
235
|
+
expect(screen.getByRole("link", { name: "Cover" })).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
});
|