@thepalaceproject/circulation-admin 1.43.0 → 1.44.0-post.4
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,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
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { installFormDataShim } from "../testUtils/formDataShim";
|
|
3
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import BookEditForm from "../../../src/components/BookEditForm";
|
|
7
|
+
import {
|
|
8
|
+
BookData,
|
|
9
|
+
RolesData,
|
|
10
|
+
MediaData,
|
|
11
|
+
LanguagesData,
|
|
12
|
+
} from "../../../src/interfaces";
|
|
13
|
+
installFormDataShim();
|
|
14
|
+
|
|
15
|
+
describe("BookEditForm", () => {
|
|
16
|
+
const roles: RolesData = {
|
|
17
|
+
aut: "Author",
|
|
18
|
+
nar: "Narrator",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const media: MediaData = {
|
|
22
|
+
"http://schema.org/AudioObject": "Audio",
|
|
23
|
+
"http://schema.org/Book": "Book",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const languages: LanguagesData = {
|
|
27
|
+
eng: ["English"],
|
|
28
|
+
spa: ["Spanish", "Castilian"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const bookData: BookData = {
|
|
32
|
+
id: "id",
|
|
33
|
+
title: "title",
|
|
34
|
+
subtitle: "subtitle",
|
|
35
|
+
authors: [{ name: "An Author", role: "aut" }],
|
|
36
|
+
contributors: [
|
|
37
|
+
{ name: "A Narrator", role: "nar" },
|
|
38
|
+
{ name: "Another Narrator", role: "nar" },
|
|
39
|
+
],
|
|
40
|
+
fiction: true,
|
|
41
|
+
audience: "Young Adult",
|
|
42
|
+
targetAgeRange: ["12", "16"],
|
|
43
|
+
summary: "summary",
|
|
44
|
+
series: "series",
|
|
45
|
+
seriesPosition: 3,
|
|
46
|
+
medium: "http://schema.org/AudioObject",
|
|
47
|
+
language: "eng",
|
|
48
|
+
publisher: "publisher",
|
|
49
|
+
imprint: "imprint",
|
|
50
|
+
issued: "2017-04-03",
|
|
51
|
+
rating: 4,
|
|
52
|
+
editLink: {
|
|
53
|
+
href: "href",
|
|
54
|
+
rel: "edit",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type Props = React.ComponentProps<typeof BookEditForm>;
|
|
59
|
+
|
|
60
|
+
const makeProps = (overrides: Partial<Props> = {}): Props => ({
|
|
61
|
+
...(bookData as Props),
|
|
62
|
+
roles,
|
|
63
|
+
media,
|
|
64
|
+
languages,
|
|
65
|
+
disabled: false,
|
|
66
|
+
refresh: jest.fn(),
|
|
67
|
+
editBook: jest.fn().mockResolvedValue(undefined),
|
|
68
|
+
...overrides,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const renderForm = (overrides: Partial<Props> = {}) => {
|
|
72
|
+
const props = makeProps(overrides);
|
|
73
|
+
const result = render(<BookEditForm {...props} />);
|
|
74
|
+
const rerender = (nextOverrides: Partial<Props>) =>
|
|
75
|
+
result.rerender(<BookEditForm {...makeProps(nextOverrides)} />);
|
|
76
|
+
return { ...result, rerender, props };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const inputsByName = (container: HTMLElement, name: string) =>
|
|
80
|
+
Array.from(
|
|
81
|
+
container.querySelectorAll<HTMLInputElement>(`input[name="${name}"]`)
|
|
82
|
+
);
|
|
83
|
+
const selectsByName = (container: HTMLElement, name: string) =>
|
|
84
|
+
Array.from(
|
|
85
|
+
container.querySelectorAll<HTMLSelectElement>(`select[name="${name}"]`)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
describe("rendering", () => {
|
|
89
|
+
it("shows editable input with title", () => {
|
|
90
|
+
const { container } = renderForm();
|
|
91
|
+
expect(screen.getByText("Title")).toBeInTheDocument();
|
|
92
|
+
expect(container.querySelector('input[name="title"]')).toHaveValue(
|
|
93
|
+
"title"
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("shows editable input with subtitle", () => {
|
|
98
|
+
const { container } = renderForm();
|
|
99
|
+
expect(screen.getByText("Subtitle")).toBeInTheDocument();
|
|
100
|
+
expect(container.querySelector('input[name="subtitle"]')).toHaveValue(
|
|
101
|
+
"subtitle"
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("shows authors and contributors", () => {
|
|
106
|
+
const { container } = renderForm();
|
|
107
|
+
|
|
108
|
+
const names = inputsByName(container, "contributor-name");
|
|
109
|
+
const contributorRoles = selectsByName(container, "contributor-role");
|
|
110
|
+
expect(names).toHaveLength(4);
|
|
111
|
+
expect(contributorRoles).toHaveLength(4);
|
|
112
|
+
|
|
113
|
+
expect(names[0]).toHaveValue("An Author");
|
|
114
|
+
expect(contributorRoles[0]).toHaveValue("Author");
|
|
115
|
+
expect(names[1]).toHaveValue("A Narrator");
|
|
116
|
+
expect(contributorRoles[1]).toHaveValue("Narrator");
|
|
117
|
+
expect(names[2]).toHaveValue("Another Narrator");
|
|
118
|
+
expect(contributorRoles[2]).toHaveValue("Narrator");
|
|
119
|
+
|
|
120
|
+
// The last inputs are for adding a new contributor.
|
|
121
|
+
expect(names[3]).toHaveValue("");
|
|
122
|
+
expect(contributorRoles[3]).toHaveValue("Author");
|
|
123
|
+
expect(
|
|
124
|
+
container.querySelector("button.add-contributor")
|
|
125
|
+
).toBeInTheDocument();
|
|
126
|
+
|
|
127
|
+
// Existing authors and contributors are removable.
|
|
128
|
+
expect(
|
|
129
|
+
container.querySelectorAll(".with-remove-button .remove-btn")
|
|
130
|
+
).toHaveLength(3);
|
|
131
|
+
|
|
132
|
+
// All role inputs have the same options.
|
|
133
|
+
[contributorRoles[1], contributorRoles[3]].forEach((select) => {
|
|
134
|
+
const options = within(select).getAllByRole("option");
|
|
135
|
+
expect(options).toHaveLength(2);
|
|
136
|
+
expect(options[0]).toHaveValue("Author");
|
|
137
|
+
expect(options[1]).toHaveValue("Narrator");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("shows editable input with series", () => {
|
|
142
|
+
const { container } = renderForm();
|
|
143
|
+
expect(container.querySelector('input[name="series"]')).toHaveValue(
|
|
144
|
+
"series"
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("shows editable input with series position", () => {
|
|
149
|
+
const { container } = renderForm();
|
|
150
|
+
expect(
|
|
151
|
+
container.querySelector('input[name="series_position"]')
|
|
152
|
+
).toHaveValue("3");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("shows editable input with medium", () => {
|
|
156
|
+
const { container } = renderForm();
|
|
157
|
+
expect(screen.getByText("Medium")).toBeInTheDocument();
|
|
158
|
+
expect(container.querySelector('select[name="medium"]')).toHaveValue(
|
|
159
|
+
"Audio"
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("shows a language field", () => {
|
|
164
|
+
const { container, rerender } = renderForm();
|
|
165
|
+
expect(screen.getByText("Language")).toBeInTheDocument();
|
|
166
|
+
expect(container.querySelector('input[name="language"]')).toHaveValue(
|
|
167
|
+
"English"
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
rerender({ language: "fre" });
|
|
171
|
+
expect(container.querySelector('input[name="language"]')).toHaveValue(
|
|
172
|
+
"fre"
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("shows editable input with publisher", () => {
|
|
177
|
+
const { container } = renderForm();
|
|
178
|
+
expect(screen.getByText("Publisher")).toBeInTheDocument();
|
|
179
|
+
expect(container.querySelector('input[name="publisher"]')).toHaveValue(
|
|
180
|
+
"publisher"
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("shows editable input with imprint", () => {
|
|
185
|
+
const { container } = renderForm();
|
|
186
|
+
expect(screen.getByText("Imprint")).toBeInTheDocument();
|
|
187
|
+
expect(container.querySelector('input[name="imprint"]')).toHaveValue(
|
|
188
|
+
"imprint"
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("shows editable input with publication date", () => {
|
|
193
|
+
const { container } = renderForm();
|
|
194
|
+
expect(screen.getByText("Publication Date")).toBeInTheDocument();
|
|
195
|
+
expect(container.querySelector('input[name="issued"]')).toHaveValue(
|
|
196
|
+
"2017-04-03"
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("shows editable input with rating", () => {
|
|
201
|
+
const { container } = renderForm();
|
|
202
|
+
expect(screen.getByText(/Rating/)).toBeInTheDocument();
|
|
203
|
+
const rating = container.querySelector<HTMLInputElement>(
|
|
204
|
+
'input[name="rating"]'
|
|
205
|
+
);
|
|
206
|
+
expect(rating).toHaveValue(4);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("shows editable textarea with summary", () => {
|
|
210
|
+
const { container } = renderForm();
|
|
211
|
+
const editor = container.querySelector<HTMLElement>(".editor");
|
|
212
|
+
expect(within(editor).getByText("Summary")).toBeInTheDocument();
|
|
213
|
+
expect(editor.querySelector(".DraftEditor-root")).toHaveTextContent(
|
|
214
|
+
"summary"
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("removes a contributor", async () => {
|
|
220
|
+
const user = userEvent.setup();
|
|
221
|
+
const { container } = renderForm();
|
|
222
|
+
|
|
223
|
+
let removeButtons = screen.getAllByRole("button", { name: "Delete" });
|
|
224
|
+
expect(removeButtons).toHaveLength(3);
|
|
225
|
+
|
|
226
|
+
// Remove the first narrator (index 1: An Author, A Narrator, Another Narrator).
|
|
227
|
+
await user.click(removeButtons[1]);
|
|
228
|
+
|
|
229
|
+
removeButtons = screen.getAllByRole("button", { name: "Delete" });
|
|
230
|
+
expect(removeButtons).toHaveLength(2);
|
|
231
|
+
|
|
232
|
+
const names = inputsByName(container, "contributor-name");
|
|
233
|
+
const contributorRoles = selectsByName(container, "contributor-role");
|
|
234
|
+
expect(names).toHaveLength(3);
|
|
235
|
+
expect(contributorRoles).toHaveLength(3);
|
|
236
|
+
expect(names[0]).toHaveValue("An Author");
|
|
237
|
+
expect(contributorRoles[0]).toHaveValue("Author");
|
|
238
|
+
expect(names[1]).toHaveValue("Another Narrator");
|
|
239
|
+
expect(contributorRoles[1]).toHaveValue("Narrator");
|
|
240
|
+
expect(names[2]).toHaveValue("");
|
|
241
|
+
expect(contributorRoles[2]).toHaveValue("Author");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("adds a contributor", async () => {
|
|
245
|
+
const user = userEvent.setup();
|
|
246
|
+
const { container } = renderForm();
|
|
247
|
+
|
|
248
|
+
let names = inputsByName(container, "contributor-name");
|
|
249
|
+
let contributorRoles = selectsByName(container, "contributor-role");
|
|
250
|
+
expect(names).toHaveLength(4);
|
|
251
|
+
expect(contributorRoles).toHaveLength(4);
|
|
252
|
+
|
|
253
|
+
// Fill in the empty "add" name field (the last one); its role defaults to
|
|
254
|
+
// Author. Typing enables the Add button.
|
|
255
|
+
await user.type(names[3], "New Author");
|
|
256
|
+
await user.click(container.querySelector("button.add-contributor"));
|
|
257
|
+
|
|
258
|
+
names = inputsByName(container, "contributor-name");
|
|
259
|
+
contributorRoles = selectsByName(container, "contributor-role");
|
|
260
|
+
expect(names).toHaveLength(5);
|
|
261
|
+
expect(contributorRoles).toHaveLength(5);
|
|
262
|
+
|
|
263
|
+
expect(names[0]).toHaveValue("An Author");
|
|
264
|
+
expect(contributorRoles[0]).toHaveValue("Author");
|
|
265
|
+
expect(names[1]).toHaveValue("A Narrator");
|
|
266
|
+
expect(contributorRoles[1]).toHaveValue("Narrator");
|
|
267
|
+
expect(names[2]).toHaveValue("Another Narrator");
|
|
268
|
+
expect(contributorRoles[2]).toHaveValue("Narrator");
|
|
269
|
+
expect(names[3]).toHaveValue("New Author");
|
|
270
|
+
expect(contributorRoles[3]).toHaveValue("Author");
|
|
271
|
+
expect(names[4]).toHaveValue("");
|
|
272
|
+
expect(contributorRoles[4]).toHaveValue("Author");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("calls editBook on submit", async () => {
|
|
276
|
+
const user = userEvent.setup();
|
|
277
|
+
const editBook = jest.fn().mockResolvedValue(undefined);
|
|
278
|
+
renderForm({ editBook });
|
|
279
|
+
|
|
280
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
281
|
+
|
|
282
|
+
expect(editBook).toHaveBeenCalledTimes(1);
|
|
283
|
+
const [url, data] = editBook.mock.calls[0];
|
|
284
|
+
expect(url).toBe("href");
|
|
285
|
+
expect(data.get("title")).toBe(bookData.title);
|
|
286
|
+
expect(data.get("subtitle")).toBe(bookData.subtitle);
|
|
287
|
+
|
|
288
|
+
// The last contributor field is the empty one for adding a new contributor.
|
|
289
|
+
// If the user had filled it in without clicking "Add", it would still be
|
|
290
|
+
// submitted.
|
|
291
|
+
expect(data.getAll("contributor-name")).toStrictEqual([
|
|
292
|
+
"An Author",
|
|
293
|
+
"A Narrator",
|
|
294
|
+
"Another Narrator",
|
|
295
|
+
"",
|
|
296
|
+
]);
|
|
297
|
+
expect(data.getAll("contributor-role")).toStrictEqual([
|
|
298
|
+
"Author",
|
|
299
|
+
"Narrator",
|
|
300
|
+
"Narrator",
|
|
301
|
+
"Author",
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
expect(data.get("series")).toBe(bookData.series);
|
|
305
|
+
expect(data.get("series_position")).toBe(String(bookData.seriesPosition));
|
|
306
|
+
expect(data.get("medium")).toBe("Audio");
|
|
307
|
+
expect(data.get("language")).toBe("English");
|
|
308
|
+
expect(data.get("publisher")).toBe(bookData.publisher);
|
|
309
|
+
expect(data.get("imprint")).toBe(bookData.imprint);
|
|
310
|
+
expect(data.get("issued")).toBe(bookData.issued);
|
|
311
|
+
expect(data.get("rating")).toBe("4");
|
|
312
|
+
expect(data.get("summary")).toContain(bookData.summary);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("refreshes book after editing", async () => {
|
|
316
|
+
const user = userEvent.setup();
|
|
317
|
+
const editBook = jest.fn().mockResolvedValue(undefined);
|
|
318
|
+
const refresh = jest.fn().mockResolvedValue(undefined);
|
|
319
|
+
renderForm({ editBook, refresh });
|
|
320
|
+
|
|
321
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
322
|
+
|
|
323
|
+
await waitFor(() => expect(refresh).toHaveBeenCalledTimes(1));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("only adds updated summary content if it's not empty", async () => {
|
|
327
|
+
const user = userEvent.setup();
|
|
328
|
+
const editBook = jest.fn().mockResolvedValue(undefined);
|
|
329
|
+
const refresh = jest.fn().mockResolvedValue(undefined);
|
|
330
|
+
// Since we are adding an empty string, there should be no `summary` value
|
|
331
|
+
// in the FormData object that gets passed to the `editBook` function.
|
|
332
|
+
renderForm({
|
|
333
|
+
editBook,
|
|
334
|
+
refresh,
|
|
335
|
+
summary: "<p></p>",
|
|
336
|
+
editLink: { href: "test-url", rel: "something" },
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
340
|
+
|
|
341
|
+
await waitFor(() => expect(editBook).toHaveBeenCalledTimes(1));
|
|
342
|
+
expect(editBook.mock.calls[0][1].get("summary")).toBeNull();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("disables all inputs", () => {
|
|
346
|
+
const { container } = renderForm({ disabled: true });
|
|
347
|
+
|
|
348
|
+
container
|
|
349
|
+
.querySelectorAll("input, select")
|
|
350
|
+
.forEach((input) => expect(input).toBeDisabled());
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
PER_LIBRARY_UNSUPPRESS_REL,
|
|
8
8
|
} from "../../../src/features/book/bookEditorSlice";
|
|
9
9
|
import { BookDetailsEditor } from "../../../src/components/BookDetailsEditor";
|
|
10
|
-
import { expect } from "chai";
|
|
11
10
|
import { store } from "../../../src/store";
|
|
12
11
|
import * as fetchMock from "fetch-mock-jest";
|
|
13
12
|
|
|
@@ -93,8 +92,8 @@ describe("BookDetails", () => {
|
|
|
93
92
|
const hideButton = queryByRole("button", { name: "Hide" });
|
|
94
93
|
const restoreButton = queryByRole("button", { name: "Restore" });
|
|
95
94
|
|
|
96
|
-
expect(hideButton).
|
|
97
|
-
expect(restoreButton).
|
|
95
|
+
expect(hideButton).toBeNull();
|
|
96
|
+
expect(restoreButton).toBeNull();
|
|
98
97
|
});
|
|
99
98
|
|
|
100
99
|
it("don't show restore button if not a library's admin", () => {
|
|
@@ -110,8 +109,8 @@ describe("BookDetails", () => {
|
|
|
110
109
|
const hideButton = queryByRole("button", { name: "Hide" });
|
|
111
110
|
const restoreButton = queryByRole("button", { name: "Restore" });
|
|
112
111
|
|
|
113
|
-
expect(hideButton).
|
|
114
|
-
expect(restoreButton).
|
|
112
|
+
expect(hideButton).toBeNull();
|
|
113
|
+
expect(restoreButton).toBeNull();
|
|
115
114
|
});
|
|
116
115
|
|
|
117
116
|
it("uses modal for suppress book confirmation", async () => {
|
|
@@ -149,8 +148,8 @@ describe("BookDetails", () => {
|
|
|
149
148
|
await user.click(cancelButton);
|
|
150
149
|
confirmButton = queryByRole("button", { name: "Suppress Availability" });
|
|
151
150
|
cancelButton = queryByRole("button", { name: "Cancel" });
|
|
152
|
-
expect(confirmButton).
|
|
153
|
-
expect(cancelButton).
|
|
151
|
+
expect(confirmButton).toBeNull();
|
|
152
|
+
expect(cancelButton).toBeNull();
|
|
154
153
|
|
|
155
154
|
// Clicking `Hide` again should show the modal again.
|
|
156
155
|
await user.click(hideButton);
|
|
@@ -163,15 +162,15 @@ describe("BookDetails", () => {
|
|
|
163
162
|
getByRole("button", { name: "Dismiss" });
|
|
164
163
|
|
|
165
164
|
// Check that the API was invoked.
|
|
166
|
-
expect(suppressBook.mock.calls.length).
|
|
167
|
-
expect(suppressBook.mock.calls[0][0]).
|
|
165
|
+
expect(suppressBook.mock.calls.length).toBe(1);
|
|
166
|
+
expect(suppressBook.mock.calls[0][0]).toBe("/suppress/href");
|
|
168
167
|
const fetchCalls = fetchMock.calls();
|
|
169
|
-
expect(fetchCalls.length).
|
|
168
|
+
expect(fetchCalls.length).toBe(1);
|
|
170
169
|
const fetchCall = fetchCalls[0];
|
|
171
170
|
const fetchOptions = fetchCalls[0][1];
|
|
172
|
-
expect(fetchCall[0]).
|
|
173
|
-
expect(fetchOptions["headers"]["X-CSRF-Token"]).
|
|
174
|
-
expect(fetchOptions["method"]).
|
|
171
|
+
expect(fetchCall[0]).toBe("/suppress/href");
|
|
172
|
+
expect(fetchOptions["headers"]["X-CSRF-Token"]).toContain("token");
|
|
173
|
+
expect(fetchOptions["method"]).toBe("POST");
|
|
175
174
|
});
|
|
176
175
|
it("uses modal for unsuppress book confirmation", async () => {
|
|
177
176
|
// Configure standard constructors so that RTK Query works in tests with FetchMockJest
|
|
@@ -213,8 +212,8 @@ describe("BookDetails", () => {
|
|
|
213
212
|
await user.click(cancelButton);
|
|
214
213
|
confirmButton = queryByRole("button", { name: "Restore Availability" });
|
|
215
214
|
cancelButton = queryByRole("button", { name: "Cancel" });
|
|
216
|
-
expect(confirmButton).
|
|
217
|
-
expect(cancelButton).
|
|
215
|
+
expect(confirmButton).toBeNull();
|
|
216
|
+
expect(cancelButton).toBeNull();
|
|
218
217
|
|
|
219
218
|
// Clicking `Restore` again should show the modal again.
|
|
220
219
|
await user.click(restoreButton);
|
|
@@ -227,14 +226,14 @@ describe("BookDetails", () => {
|
|
|
227
226
|
getByRole("button", { name: "Dismiss" });
|
|
228
227
|
|
|
229
228
|
// Check that the API was invoked.
|
|
230
|
-
expect(unsuppressBook.mock.calls.length).
|
|
231
|
-
expect(unsuppressBook.mock.calls[0][0]).
|
|
229
|
+
expect(unsuppressBook.mock.calls.length).toBe(1);
|
|
230
|
+
expect(unsuppressBook.mock.calls[0][0]).toBe("/unsuppress/href");
|
|
232
231
|
const fetchCalls = fetchMock.calls();
|
|
233
|
-
expect(fetchCalls.length).
|
|
232
|
+
expect(fetchCalls.length).toBe(1);
|
|
234
233
|
const fetchCall = fetchCalls[0];
|
|
235
234
|
const fetchOptions = fetchCalls[0][1];
|
|
236
|
-
expect(fetchCall[0]).
|
|
237
|
-
expect(fetchOptions["headers"]["X-CSRF-Token"]).
|
|
238
|
-
expect(fetchOptions["method"]).
|
|
235
|
+
expect(fetchCall[0]).toBe("/unsuppress/href");
|
|
236
|
+
expect(fetchOptions["headers"]["X-CSRF-Token"]).toContain("token");
|
|
237
|
+
expect(fetchOptions["method"]).toBe("DELETE");
|
|
239
238
|
});
|
|
240
239
|
});
|