@thepalaceproject/circulation-admin 1.43.0 → 1.44.0-post.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +28 -7
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +22 -8
- package/package.json +6 -20
- package/tests/jest/actions.test.ts +962 -0
- package/tests/jest/components/AccountPage.test.tsx +41 -0
- package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +93 -20
- package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
- package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
- package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
- package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
- package/tests/jest/components/Announcement.test.tsx +76 -0
- package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
- package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
- package/tests/jest/components/Autocomplete.test.tsx +67 -0
- package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
- package/tests/jest/components/BookDetails.test.tsx +133 -0
- package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
- package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/BookEditor.test.tsx +20 -21
- package/tests/jest/components/CatalogPage.test.tsx +158 -0
- package/tests/jest/components/CatalogServices.test.tsx +100 -0
- package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
- package/tests/jest/components/Classifications.test.tsx +166 -0
- package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
- package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
- package/tests/jest/components/Collections.test.tsx +335 -7
- package/tests/jest/components/ColorPicker.test.tsx +58 -0
- package/tests/jest/components/ComplaintForm.test.tsx +171 -0
- package/tests/jest/components/Complaints.test.tsx +259 -0
- package/tests/jest/components/ConfigPage.test.tsx +58 -0
- package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/Contributors.test.tsx +157 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
- package/tests/jest/components/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomLists.test.tsx +621 -4
- package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
- package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
- package/tests/jest/components/DashboardPage.test.tsx +76 -0
- package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
- package/tests/jest/components/EditableConfigList.test.tsx +606 -9
- package/tests/jest/components/EditableInput.test.tsx +250 -1
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
- package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
- package/tests/jest/components/ErrorMessage.test.tsx +97 -0
- package/tests/jest/components/Footer.test.tsx +141 -0
- package/tests/jest/components/GenreForm.test.tsx +152 -0
- package/tests/jest/components/Header.test.tsx +421 -0
- package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
- package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/JsonField.test.tsx +20 -0
- package/tests/jest/components/Lane.test.tsx +132 -10
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +377 -13
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/Libraries.test.tsx +48 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/LoadButton.test.tsx +49 -0
- package/tests/jest/components/ManagePatrons.test.tsx +73 -0
- package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
- package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
- package/tests/jest/components/MetadataServices.test.tsx +95 -0
- package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
- package/tests/jest/components/PatronInfo.test.tsx +54 -0
- package/tests/jest/components/ProtocolFormField.test.tsx +479 -7
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SaveButton.test.tsx +47 -0
- package/tests/jest/components/SelfTestResult.test.tsx +164 -0
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
- package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
- package/tests/jest/components/SetupPage.test.tsx +41 -0
- package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +4 -15
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -0
- package/tests/jest/components/ToolTip.test.tsx +46 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
- package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
- package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
- package/tests/jest/components/WelcomePage.test.tsx +43 -0
- package/tests/jest/components/WithEditButton.test.tsx +55 -0
- package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
- package/tests/jest/editorAdapter.test.ts +245 -0
- package/tests/jest/features/book.test.ts +75 -80
- package/tests/jest/fixtures/classificationsData.ts +45 -0
- package/tests/jest/fixtures/genreData.ts +811 -0
- package/tests/jest/index.test.tsx +50 -0
- package/tests/jest/models/Admin.test.ts +206 -0
- package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
- package/tests/jest/reducers/classifications.test.ts +161 -0
- package/tests/jest/reducers/collections.test.ts +96 -0
- package/tests/jest/reducers/complaints.test.ts +166 -0
- package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
- package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
- package/tests/jest/reducers/customListDetails.test.ts +120 -0
- package/tests/jest/reducers/customListEditor.test.ts +2823 -0
- package/tests/jest/reducers/managePatrons.test.ts +71 -0
- package/tests/jest/testUtils/formDataShim.test.ts +135 -0
- package/tests/jest/testUtils/formDataShim.ts +112 -0
- package/tests/jest/utils/sharedFunctions.test.ts +121 -0
- package/tsconfig.json +3 -4
- package/testReporter.js +0 -29
- package/tests/jest/README.md +0 -6
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import BookDetails from "../../../src/components/BookDetails";
|
|
5
|
+
import { BookData } from "@thepalaceproject/web-opds-client/lib/interfaces";
|
|
6
|
+
|
|
7
|
+
const book: BookData = {
|
|
8
|
+
id: "urn:librarysimplified.org/terms/id/3M%20ID/crrmnr9",
|
|
9
|
+
url: "http://circulation.librarysimplified.org/works/3M/crrmnr9",
|
|
10
|
+
title: "The Mayan Secrets",
|
|
11
|
+
authors: ["Clive Cussler", "Thomas Perry"],
|
|
12
|
+
contributors: ["contributor 1"],
|
|
13
|
+
summary:
|
|
14
|
+
"<b>Sam and Remi Fargo race for treasure—and survival—in this lightning-paced new adventure from #1<i> New York Times</i> bestselling author Clive Cussler.</b><br /><br />Husband-and-wife team Sam and Remi Fargo are in Mexico when they come upon a remarkable discovery—the mummified remainsof a man clutching an ancient sealed pot. Within the pot is a Mayan book larger than any known before.<br /><br />The book contains astonishing information about the Mayans, their cities, and about mankind itself. The secrets are so powerful that some people would do anything to possess them—as the Fargos are about to find out. Many men and women are going to die for that book.",
|
|
15
|
+
imageUrl: "https://dlotdqc6pnwqb.cloudfront.net/3M/crrmnr9/cover.jpg",
|
|
16
|
+
borrowUrl: "borrow url",
|
|
17
|
+
openAccessLinks: [{ url: "secrets.epub", type: "application/epub+zip" }],
|
|
18
|
+
publisher: "Penguin Publishing Group",
|
|
19
|
+
published: "February 29, 2016",
|
|
20
|
+
categories: ["Children", "10-12", "Fiction", "Adventure", "Fantasy"],
|
|
21
|
+
raw: {
|
|
22
|
+
category: [
|
|
23
|
+
{
|
|
24
|
+
$: {
|
|
25
|
+
scheme: { value: "http://schema.org/audience" },
|
|
26
|
+
label: { value: "Children" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
$: {
|
|
31
|
+
scheme: { value: "http://schema.org/typicalAgeRange" },
|
|
32
|
+
label: { value: "10-12" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
$: {
|
|
37
|
+
scheme: { value: "http://librarysimplified.org/terms/fiction/" },
|
|
38
|
+
label: { value: "Fiction" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
$: {
|
|
43
|
+
scheme: {
|
|
44
|
+
value: "http://librarysimplified.org/terms/genres/Simplified/",
|
|
45
|
+
},
|
|
46
|
+
label: { value: "Adventure" },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
$: {
|
|
51
|
+
scheme: {
|
|
52
|
+
value: "http://librarysimplified.org/terms/genres/Simplified/",
|
|
53
|
+
},
|
|
54
|
+
label: { value: "Fantasy" },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
"bibframe:distribution": [
|
|
59
|
+
{
|
|
60
|
+
$: {
|
|
61
|
+
"bibframe:ProviderName": {
|
|
62
|
+
value: "Overdrive",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
link: [
|
|
68
|
+
{
|
|
69
|
+
$: {
|
|
70
|
+
rel: { value: "issues" },
|
|
71
|
+
href: { value: "http://example.com/report" },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
$: {
|
|
76
|
+
rel: { value: "http://librarysimplified.org/terms/rel/revoke" },
|
|
77
|
+
href: { value: "http://example.com/revoke" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
describe("BookDetails", () => {
|
|
85
|
+
const updateBook = jest.fn();
|
|
86
|
+
|
|
87
|
+
it("shows audience and target age", () => {
|
|
88
|
+
const { container } = render(
|
|
89
|
+
<BookDetails book={book} updateBook={updateBook} />
|
|
90
|
+
);
|
|
91
|
+
const audience = container.querySelector(".audience");
|
|
92
|
+
expect(audience).toBeInTheDocument();
|
|
93
|
+
expect(audience).toHaveTextContent("Audience: Children (age 10-12)");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("shows categories", () => {
|
|
97
|
+
const { container } = render(
|
|
98
|
+
<BookDetails book={book} updateBook={updateBook} />
|
|
99
|
+
);
|
|
100
|
+
const categories = container.querySelector(".categories");
|
|
101
|
+
expect(categories).toBeInTheDocument();
|
|
102
|
+
expect(categories).toHaveTextContent("Categories: Adventure, Fantasy");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("doesn't show categories when there aren't any", () => {
|
|
106
|
+
const bookCopy: BookData = {
|
|
107
|
+
...book,
|
|
108
|
+
raw: { category: [], link: [] },
|
|
109
|
+
};
|
|
110
|
+
const { container } = render(
|
|
111
|
+
<BookDetails book={bookCopy} updateBook={updateBook} />
|
|
112
|
+
);
|
|
113
|
+
expect(container.querySelector(".categories")).not.toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("shows distributor", () => {
|
|
117
|
+
const { container } = render(
|
|
118
|
+
<BookDetails book={book} updateBook={updateBook} />
|
|
119
|
+
);
|
|
120
|
+
const distributor = container.querySelector(".distributed-by");
|
|
121
|
+
expect(distributor).toBeInTheDocument();
|
|
122
|
+
expect(distributor).toHaveTextContent("Distributed By: Overdrive");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("doesn't render any circulation link content", () => {
|
|
126
|
+
const { container } = render(
|
|
127
|
+
<BookDetails book={book} updateBook={updateBook} />
|
|
128
|
+
);
|
|
129
|
+
const circulationLinks = container.querySelector(".circulation-links");
|
|
130
|
+
expect(circulationLinks).toBeInTheDocument();
|
|
131
|
+
expect(circulationLinks).toBeEmptyDOMElement();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import buildStore from "../../../src/store";
|
|
6
|
+
import { BookData } from "@thepalaceproject/web-opds-client/lib/interfaces";
|
|
7
|
+
|
|
8
|
+
// BookDetailsTabContainer is a connected TabContainer that mounts every one of its
|
|
9
|
+
// tab panels on render (the metadata editor, classifications, cover, and lists
|
|
10
|
+
// editors), each of which fetches over the network on mount. That machinery is
|
|
11
|
+
// incidental to what BookDetailsContainer composes, so mock it to a marker that
|
|
12
|
+
// echoes the props it receives and renders the book child it is handed.
|
|
13
|
+
jest.mock("../../../src/components/BookDetailsTabContainer", () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: (props: any) => (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="book-details-tab-container"
|
|
18
|
+
data-book-url={props.bookUrl}
|
|
19
|
+
data-collection-url={props.collectionUrl}
|
|
20
|
+
data-csrf-token={props.csrfToken}
|
|
21
|
+
data-has-store={String(!!props.store)}
|
|
22
|
+
data-has-library={String(!!props.library)}
|
|
23
|
+
data-has-refresh-catalog={String(!!props.refreshCatalog)}
|
|
24
|
+
>
|
|
25
|
+
{props.children}
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
import BookDetailsContainer from "../../../src/components/BookDetailsContainer";
|
|
31
|
+
import BookDetails from "../../../src/components/BookDetails";
|
|
32
|
+
|
|
33
|
+
// BookDetailsContainer reads its configuration from the legacy React context that
|
|
34
|
+
// the OPDS web client supplies in production. `renderWithProviders` does not supply
|
|
35
|
+
// the `library` (nor `tab`) keys it reads, so wrap it in a small legacy context
|
|
36
|
+
// provider that hands it everything it needs.
|
|
37
|
+
class LegacyContextProvider extends React.Component<{ context: any }> {
|
|
38
|
+
static childContextTypes = {
|
|
39
|
+
editorStore: PropTypes.object,
|
|
40
|
+
csrfToken: PropTypes.string,
|
|
41
|
+
tab: PropTypes.string,
|
|
42
|
+
library: PropTypes.func,
|
|
43
|
+
admin: PropTypes.object,
|
|
44
|
+
};
|
|
45
|
+
getChildContext() {
|
|
46
|
+
return this.props.context;
|
|
47
|
+
}
|
|
48
|
+
render() {
|
|
49
|
+
return this.props.children;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const bookData: BookData = {
|
|
54
|
+
id: "book id",
|
|
55
|
+
url: "book url",
|
|
56
|
+
title: "book title",
|
|
57
|
+
raw: { category: [], link: [] },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
describe("BookDetailsContainer", () => {
|
|
61
|
+
let store;
|
|
62
|
+
let context;
|
|
63
|
+
let refreshCatalog;
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
store = buildStore();
|
|
67
|
+
refreshCatalog = jest.fn();
|
|
68
|
+
context = {
|
|
69
|
+
editorStore: store,
|
|
70
|
+
csrfToken: "token",
|
|
71
|
+
library: jest.fn(),
|
|
72
|
+
admin: { isLibraryManager: () => true },
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const renderContainer = () =>
|
|
77
|
+
render(
|
|
78
|
+
<LegacyContextProvider context={context}>
|
|
79
|
+
<BookDetailsContainer
|
|
80
|
+
book={bookData}
|
|
81
|
+
bookUrl="book url"
|
|
82
|
+
collectionUrl="collection url"
|
|
83
|
+
refreshCatalog={refreshCatalog}
|
|
84
|
+
>
|
|
85
|
+
<BookDetails book={bookData} updateBook={jest.fn()} />
|
|
86
|
+
</BookDetailsContainer>
|
|
87
|
+
</LegacyContextProvider>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
it("renders BookDetails with its child's props", () => {
|
|
91
|
+
renderContainer();
|
|
92
|
+
// The container recreates the real BookDetails from the child element's props,
|
|
93
|
+
// so the book it was handed shows up (its title is rendered as the heading).
|
|
94
|
+
expect(
|
|
95
|
+
screen.getByRole("heading", { name: "book title" })
|
|
96
|
+
).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("shows a tab container with initial tab", () => {
|
|
100
|
+
renderContainer();
|
|
101
|
+
const tabContainer = screen.getByTestId("book-details-tab-container");
|
|
102
|
+
expect(tabContainer).toBeInTheDocument();
|
|
103
|
+
expect(tabContainer).toHaveAttribute("data-book-url", "book url");
|
|
104
|
+
expect(tabContainer).toHaveAttribute(
|
|
105
|
+
"data-collection-url",
|
|
106
|
+
"collection url"
|
|
107
|
+
);
|
|
108
|
+
expect(tabContainer).toHaveAttribute("data-csrf-token", "token");
|
|
109
|
+
// The library function, redux store, and refreshCatalog callback are forwarded
|
|
110
|
+
// from context/props; they are not serializable to the DOM, so assert they were
|
|
111
|
+
// passed through.
|
|
112
|
+
expect(tabContainer).toHaveAttribute("data-has-store", "true");
|
|
113
|
+
expect(tabContainer).toHaveAttribute("data-has-library", "true");
|
|
114
|
+
expect(tabContainer).toHaveAttribute("data-has-refresh-catalog", "true");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -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
|
+
});
|