@thepalaceproject/circulation-admin 1.43.0-post.10 → 1.43.0-post.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/tests/jest/components/Announcement.test.tsx +76 -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/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/CatalogPage.test.tsx +158 -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/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/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 +315 -0
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/GenreForm.test.tsx +152 -0
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/Lane.test.tsx +130 -7
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +367 -2
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
import title from "../../../src/utils/title";
|
|
6
|
+
|
|
7
|
+
// CatalogPage does `require("@thepalaceproject/web-opds-client")` and mounts the OPDS
|
|
8
|
+
// Web Client, a heavy third-party render target that fetches on mount. Replace it
|
|
9
|
+
// with a marker that echoes the props CatalogPage hands it and the legacy
|
|
10
|
+
// `tab`/`library` child context CatalogPage supplies (read via `contextTypes`), so
|
|
11
|
+
// we can assert the wiring without the real catalog.
|
|
12
|
+
jest.mock("@thepalaceproject/web-opds-client", () => {
|
|
13
|
+
const React = require("react");
|
|
14
|
+
const PropTypes = require("prop-types");
|
|
15
|
+
const OPDSCatalogMock = (props, context) => (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="opds-catalog"
|
|
18
|
+
data-collection-url={props.collectionUrl}
|
|
19
|
+
data-book-url={props.bookUrl}
|
|
20
|
+
data-tab={context.tab}
|
|
21
|
+
data-library={context.library && context.library()}
|
|
22
|
+
data-header-name={props.Header && props.Header.name}
|
|
23
|
+
data-book-details-container-name={
|
|
24
|
+
props.BookDetailsContainer && props.BookDetailsContainer.name
|
|
25
|
+
}
|
|
26
|
+
data-has-compute-breadcrumbs={String(!!props.computeBreadcrumbs)}
|
|
27
|
+
data-title-book={
|
|
28
|
+
props.pageTitleTemplate && props.pageTitleTemplate("Collection", "Book")
|
|
29
|
+
}
|
|
30
|
+
data-title-collection={
|
|
31
|
+
props.pageTitleTemplate && props.pageTitleTemplate("Collection", null)
|
|
32
|
+
}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
OPDSCatalogMock.contextTypes = {
|
|
36
|
+
tab: PropTypes.string,
|
|
37
|
+
library: PropTypes.func,
|
|
38
|
+
};
|
|
39
|
+
return OPDSCatalogMock;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// When there is no library, CatalogPage renders WelcomePage, which mounts the real
|
|
43
|
+
// Header — that needs react-router v3 context and fetches on mount. Mock Header to a
|
|
44
|
+
// marker so the WelcomePage path stays self-contained.
|
|
45
|
+
jest.mock("../../../src/components/Header", () => ({
|
|
46
|
+
__esModule: true,
|
|
47
|
+
default: function Header() {
|
|
48
|
+
return <div data-testid="header" />;
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
import CatalogPage from "../../../src/components/CatalogPage";
|
|
53
|
+
import Header from "../../../src/components/Header";
|
|
54
|
+
import BookDetailsContainer from "../../../src/components/BookDetailsContainer";
|
|
55
|
+
|
|
56
|
+
// The unit env has no `global.jsdom`, so compute the expected URLs from the real
|
|
57
|
+
// origin.
|
|
58
|
+
const origin = window.location.origin;
|
|
59
|
+
|
|
60
|
+
describe("CatalogPage", () => {
|
|
61
|
+
const baseParams = {
|
|
62
|
+
collectionUrl: "library/collectionurl",
|
|
63
|
+
bookUrl: "library/bookurl",
|
|
64
|
+
tab: "tab",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it("renders the OPDS catalog with expanded collection and book URLs", () => {
|
|
68
|
+
renderWithProviders(<CatalogPage params={baseParams} />);
|
|
69
|
+
|
|
70
|
+
const catalog = screen.getByTestId("opds-catalog");
|
|
71
|
+
expect(catalog).toHaveAttribute(
|
|
72
|
+
"data-collection-url",
|
|
73
|
+
`${origin}/library/collectionurl`
|
|
74
|
+
);
|
|
75
|
+
expect(catalog).toHaveAttribute(
|
|
76
|
+
"data-book-url",
|
|
77
|
+
`${origin}/library/works/bookurl`
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("forwards the admin header, book details container, breadcrumbs, and page-title template", () => {
|
|
82
|
+
renderWithProviders(<CatalogPage params={baseParams} />);
|
|
83
|
+
|
|
84
|
+
const catalog = screen.getByTestId("opds-catalog");
|
|
85
|
+
expect(catalog).toHaveAttribute("data-header-name", Header.name);
|
|
86
|
+
expect(catalog).toHaveAttribute(
|
|
87
|
+
"data-book-details-container-name",
|
|
88
|
+
BookDetailsContainer.name
|
|
89
|
+
);
|
|
90
|
+
expect(catalog).toHaveAttribute("data-has-compute-breadcrumbs", "true");
|
|
91
|
+
// pageTitleTemplate prefers the book title over the collection title.
|
|
92
|
+
expect(catalog).toHaveAttribute("data-title-book", title("Book"));
|
|
93
|
+
expect(catalog).toHaveAttribute(
|
|
94
|
+
"data-title-collection",
|
|
95
|
+
title("Collection")
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("expands a collection URL that already contains a query string", () => {
|
|
100
|
+
renderWithProviders(
|
|
101
|
+
<CatalogPage
|
|
102
|
+
params={{
|
|
103
|
+
...baseParams,
|
|
104
|
+
collectionUrl: "library/collectionurl?samplequery=test",
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
|
|
110
|
+
"data-collection-url",
|
|
111
|
+
`${origin}/library/collectionurl?samplequery=test`
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("puts the tab into the child context", () => {
|
|
116
|
+
renderWithProviders(<CatalogPage params={baseParams} />);
|
|
117
|
+
expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
|
|
118
|
+
"data-tab",
|
|
119
|
+
"tab"
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("derives the child-context library from the collection URL", () => {
|
|
124
|
+
renderWithProviders(<CatalogPage params={baseParams} />);
|
|
125
|
+
expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
|
|
126
|
+
"data-library",
|
|
127
|
+
"library"
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("derives the child-context library from the book URL when there is no collection URL", () => {
|
|
132
|
+
renderWithProviders(
|
|
133
|
+
<CatalogPage params={{ ...baseParams, collectionUrl: null }} />
|
|
134
|
+
);
|
|
135
|
+
expect(screen.getByTestId("opds-catalog")).toHaveAttribute(
|
|
136
|
+
"data-library",
|
|
137
|
+
"library"
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("renders the welcome page when there is no library", () => {
|
|
142
|
+
renderWithProviders(
|
|
143
|
+
<CatalogPage params={{ collectionUrl: null, bookUrl: null, tab: null }} />
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.queryByTestId("opds-catalog")).not.toBeInTheDocument();
|
|
147
|
+
expect(
|
|
148
|
+
screen.getByText(/Welcome to the Circulation Admin Interface!/)
|
|
149
|
+
).toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("renders the footer", () => {
|
|
153
|
+
const { container } = renderWithProviders(
|
|
154
|
+
<CatalogPage params={baseParams} />
|
|
155
|
+
);
|
|
156
|
+
expect(container.querySelector("footer")).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
|
+
import buildStore from "../../../src/store";
|
|
7
|
+
import genreData from "../../../src/components/__tests__/genreData";
|
|
8
|
+
import classificationsData from "../../../src/components/__tests__/classificationsData";
|
|
9
|
+
// Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
|
|
10
|
+
// are exercised. The genre tree and classifications are fetched on mount and fed
|
|
11
|
+
// back in through the Redux store; `book` and `refreshCatalog` are own props.
|
|
12
|
+
import Classifications, {
|
|
13
|
+
Classifications as UnconnectedClassifications,
|
|
14
|
+
} from "../../../src/components/Classifications";
|
|
15
|
+
|
|
16
|
+
// A `targetAgeRange` is required: ClassificationsForm reads it in bookChanged on
|
|
17
|
+
// re-render (the legacy enzyme test used shallow, so the child never re-rendered).
|
|
18
|
+
const bookData = {
|
|
19
|
+
title: "title",
|
|
20
|
+
fiction: true,
|
|
21
|
+
categories: ["Space Opera"],
|
|
22
|
+
targetAgeRange: [null, null],
|
|
23
|
+
};
|
|
24
|
+
const bookUrl = "works/1234";
|
|
25
|
+
const classificationsUrl = "admin/works/1234/classifications";
|
|
26
|
+
|
|
27
|
+
const jsonResponse = (body: unknown) =>
|
|
28
|
+
new Response(JSON.stringify(body), {
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Resolves each fetch with the appropriate body based on the request URL/method.
|
|
34
|
+
const stubFetch = () =>
|
|
35
|
+
jest
|
|
36
|
+
.spyOn(globalThis, "fetch")
|
|
37
|
+
.mockImplementation(async (url, opts?: RequestInit) => {
|
|
38
|
+
const u = String(url);
|
|
39
|
+
if (opts?.method === "POST") {
|
|
40
|
+
return new Response("success", { status: 200 });
|
|
41
|
+
}
|
|
42
|
+
if (u === "/admin/genres") {
|
|
43
|
+
return jsonResponse(genreData);
|
|
44
|
+
}
|
|
45
|
+
if (u.endsWith("/classifications")) {
|
|
46
|
+
return jsonResponse({ classifications: classificationsData });
|
|
47
|
+
}
|
|
48
|
+
// getBookData (OPDS) on refresh — non-OPDS body so it settles quietly.
|
|
49
|
+
return new Response("{}", { status: 200 });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const renderConnected = (
|
|
53
|
+
overrides: Partial<{ refreshCatalog: () => Promise<any> }> = {}
|
|
54
|
+
) => {
|
|
55
|
+
const store = buildStore();
|
|
56
|
+
const refreshCatalog =
|
|
57
|
+
overrides.refreshCatalog ?? jest.fn().mockResolvedValue(undefined);
|
|
58
|
+
const result = renderWithProviders(
|
|
59
|
+
<Classifications
|
|
60
|
+
store={store}
|
|
61
|
+
csrfToken="token"
|
|
62
|
+
bookUrl={bookUrl}
|
|
63
|
+
book={bookData as any}
|
|
64
|
+
refreshCatalog={refreshCatalog}
|
|
65
|
+
/>,
|
|
66
|
+
{ reduxProviderProps: { store } }
|
|
67
|
+
);
|
|
68
|
+
return { ...result, refreshCatalog };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe("Classifications", () => {
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
jest.restoreAllMocks();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("shows the book title", async () => {
|
|
77
|
+
stubFetch();
|
|
78
|
+
renderConnected();
|
|
79
|
+
expect(
|
|
80
|
+
await screen.findByRole("heading", { level: 2, name: "title" })
|
|
81
|
+
).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("shows the updating indicator while fetching", () => {
|
|
85
|
+
// A never-resolving fetch keeps the panel in its fetching state.
|
|
86
|
+
jest
|
|
87
|
+
.spyOn(globalThis, "fetch")
|
|
88
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
89
|
+
const { container } = renderConnected();
|
|
90
|
+
expect(container.querySelector(".updating-loader")).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("shows the classifications form and table once the data loads, and hides the updating indicator", async () => {
|
|
94
|
+
stubFetch();
|
|
95
|
+
const { container } = renderConnected();
|
|
96
|
+
|
|
97
|
+
// The form renders once the genre tree loads.
|
|
98
|
+
expect(
|
|
99
|
+
await screen.findByRole("button", { name: "Save" })
|
|
100
|
+
).toBeInTheDocument();
|
|
101
|
+
// The table renders once the classifications load.
|
|
102
|
+
expect(
|
|
103
|
+
screen.getByRole("heading", { name: "Related Classifications" })
|
|
104
|
+
).toBeInTheDocument();
|
|
105
|
+
// A sample classification from the fixture shows in the table.
|
|
106
|
+
expect(screen.getByText("FICTION / Ghost")).toBeInTheDocument();
|
|
107
|
+
// Fetching is done, so the updating indicator is gone.
|
|
108
|
+
expect(container.querySelector(".updating-loader")).not.toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("shows a fetch error", () => {
|
|
112
|
+
// A network failure on mount would reject the (uncaught) fetch promise and
|
|
113
|
+
// crash the worker, so drive the error through the prop that connect derives.
|
|
114
|
+
render(
|
|
115
|
+
<UnconnectedClassifications
|
|
116
|
+
store={buildStore() as any}
|
|
117
|
+
csrfToken="token"
|
|
118
|
+
bookUrl={bookUrl}
|
|
119
|
+
book={bookData as any}
|
|
120
|
+
refreshCatalog={jest.fn().mockResolvedValue(undefined)}
|
|
121
|
+
bookAdminUrl="book admin url"
|
|
122
|
+
fetchError={{ status: 500, url: "url", response: "response" } as any}
|
|
123
|
+
fetchGenreTree={jest.fn()}
|
|
124
|
+
fetchClassifications={jest.fn()}
|
|
125
|
+
fetchBook={jest.fn()}
|
|
126
|
+
editClassifications={jest.fn().mockResolvedValue(undefined)}
|
|
127
|
+
genreTree={null}
|
|
128
|
+
classifications={null}
|
|
129
|
+
isFetching={false}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
expect(screen.getByText(/Error: response/)).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("fetches the genre tree and classifications on mount", async () => {
|
|
136
|
+
const fetchSpy = stubFetch();
|
|
137
|
+
renderConnected();
|
|
138
|
+
await screen.findByRole("button", { name: "Save" });
|
|
139
|
+
|
|
140
|
+
const urls = fetchSpy.mock.calls.map(([u]) => String(u));
|
|
141
|
+
expect(urls).toContain("/admin/genres");
|
|
142
|
+
expect(urls).toContain(classificationsUrl);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("refreshes the book, classifications, and catalog after editing classifications", async () => {
|
|
146
|
+
const user = userEvent.setup();
|
|
147
|
+
const fetchSpy = stubFetch();
|
|
148
|
+
const { container, refreshCatalog } = renderConnected();
|
|
149
|
+
|
|
150
|
+
await screen.findByRole("button", { name: "Save" });
|
|
151
|
+
|
|
152
|
+
// The form requires an audience selection before it will submit.
|
|
153
|
+
await user.selectOptions(
|
|
154
|
+
container.querySelector('select[name="audience"]') as HTMLSelectElement,
|
|
155
|
+
"Adult"
|
|
156
|
+
);
|
|
157
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
158
|
+
|
|
159
|
+
// Editing POSTs the edit_classifications URL, then refreshes the catalog.
|
|
160
|
+
await waitFor(() => expect(refreshCatalog).toHaveBeenCalledTimes(1));
|
|
161
|
+
const postCall = fetchSpy.mock.calls.find(
|
|
162
|
+
([, opts]) => (opts as RequestInit)?.method === "POST"
|
|
163
|
+
);
|
|
164
|
+
expect(String(postCall[0])).toBe("admin/works/1234/edit_classifications");
|
|
165
|
+
});
|
|
166
|
+
});
|