@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,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,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen, within } from "@testing-library/react";
|
|
3
|
+
import CatalogServicesConnected, {
|
|
4
|
+
CatalogServices,
|
|
5
|
+
} from "../../../src/components/CatalogServices";
|
|
6
|
+
import renderWithContext from "../testUtils/renderWithContext";
|
|
7
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
8
|
+
import buildStore from "../../../src/store";
|
|
9
|
+
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
|
|
10
|
+
|
|
11
|
+
describe("CatalogServices", () => {
|
|
12
|
+
const data = {
|
|
13
|
+
catalog_services: [
|
|
14
|
+
{
|
|
15
|
+
id: 2,
|
|
16
|
+
protocol: "test protocol",
|
|
17
|
+
settings: {
|
|
18
|
+
test_setting: "test setting",
|
|
19
|
+
},
|
|
20
|
+
libraries: [
|
|
21
|
+
{
|
|
22
|
+
short_name: "nypl",
|
|
23
|
+
test_library_setting: "test library setting",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
name: "nypl catalog",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
protocols: [
|
|
30
|
+
{
|
|
31
|
+
name: "test protocol",
|
|
32
|
+
label: "test protocol label",
|
|
33
|
+
settings: [],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
allLibraries: [
|
|
37
|
+
{
|
|
38
|
+
short_name: "nypl",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const config = { csrfToken: "token", featureFlags: defaultFeatureFlags };
|
|
44
|
+
|
|
45
|
+
it("shows catalog service list", () => {
|
|
46
|
+
renderWithContext(
|
|
47
|
+
<CatalogServices
|
|
48
|
+
data={data}
|
|
49
|
+
fetchData={jest.fn()}
|
|
50
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
51
|
+
csrfToken="token"
|
|
52
|
+
isFetching={false}
|
|
53
|
+
/>,
|
|
54
|
+
config
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const items = screen.getAllByRole("listitem");
|
|
58
|
+
expect(items).toHaveLength(1);
|
|
59
|
+
expect(items[0]).toHaveTextContent("nypl catalog: test protocol label");
|
|
60
|
+
|
|
61
|
+
const editLink = within(items[0]).getByRole("link");
|
|
62
|
+
expect(editLink).toHaveAttribute(
|
|
63
|
+
"href",
|
|
64
|
+
"/admin/web/config/catalogServices/edit/2"
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("CatalogServices - connected wiring", () => {
|
|
70
|
+
afterEach(() => jest.restoreAllMocks());
|
|
71
|
+
|
|
72
|
+
it("wires mapStateToProps/mapDispatchToProps and renders the fetched list", async () => {
|
|
73
|
+
jest.spyOn(globalThis, "fetch").mockImplementation(
|
|
74
|
+
async () =>
|
|
75
|
+
new Response(
|
|
76
|
+
JSON.stringify({
|
|
77
|
+
catalog_services: [
|
|
78
|
+
{ id: 2, protocol: "test protocol", name: "Test Catalog" },
|
|
79
|
+
],
|
|
80
|
+
protocols: [{ name: "test protocol", label: "TP", settings: [] }],
|
|
81
|
+
}),
|
|
82
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
const store = buildStore();
|
|
86
|
+
|
|
87
|
+
renderWithProviders(<CatalogServicesConnected csrfToken="token" />, {
|
|
88
|
+
reduxProviderProps: { store },
|
|
89
|
+
appConfigSettings: { roles: [{ role: "system" }] },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// The edit link only appears once the on-mount fetch resolves and
|
|
93
|
+
// mapStateToProps feeds the fetched service back in as props.
|
|
94
|
+
const editLink = await screen.findByRole("link", { name: /edit/i });
|
|
95
|
+
expect(editLink).toHaveAttribute(
|
|
96
|
+
"href",
|
|
97
|
+
"/admin/web/config/catalogServices/edit/2"
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { installFormDataShim } from "../testUtils/formDataShim";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
7
|
+
import ConnectedChangePasswordForm, {
|
|
8
|
+
ChangePasswordForm,
|
|
9
|
+
} from "../../../src/components/ChangePasswordForm";
|
|
10
|
+
// The reusable-components `Form` builds `new FormData(formElement)` on
|
|
11
|
+
// submit, which the unit jsdom env's undici FormData rejects; install the
|
|
12
|
+
// shared shim that reads the form's successful controls.
|
|
13
|
+
installFormDataShim();
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Render the unconnected class so tests can pass a `changePassword` spy and
|
|
19
|
+
// drive props directly. A separate test below renders the connected default
|
|
20
|
+
// export to cover the connect wiring (mapStateToProps/mapDispatchToProps).
|
|
21
|
+
const renderUnconnected = (
|
|
22
|
+
props: Partial<React.ComponentProps<typeof ChangePasswordForm>> = {}
|
|
23
|
+
) =>
|
|
24
|
+
render(
|
|
25
|
+
<ChangePasswordForm
|
|
26
|
+
isFetching={false}
|
|
27
|
+
csrfToken="token"
|
|
28
|
+
changePassword={jest.fn().mockResolvedValue(undefined)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const passwordInput = (container: HTMLElement) =>
|
|
34
|
+
container.querySelector('input[name="password"]') as HTMLInputElement;
|
|
35
|
+
const confirmInput = (container: HTMLElement) =>
|
|
36
|
+
container.querySelector('input[name="confirm_password"]') as HTMLInputElement;
|
|
37
|
+
|
|
38
|
+
describe("ChangePasswordForm", () => {
|
|
39
|
+
it("shows an error message when there is a fetch error", () => {
|
|
40
|
+
const { container, rerender } = renderUnconnected();
|
|
41
|
+
expect(container.querySelector(".alert-danger")).not.toBeInTheDocument();
|
|
42
|
+
|
|
43
|
+
rerender(
|
|
44
|
+
<ChangePasswordForm
|
|
45
|
+
isFetching={false}
|
|
46
|
+
csrfToken="token"
|
|
47
|
+
changePassword={jest.fn()}
|
|
48
|
+
fetchError={{ status: 500, response: "response", url: "" }}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(container.querySelector(".alert-danger")).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText("Error: response")).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("shows a loading indicator while fetching", () => {
|
|
57
|
+
const { rerender } = renderUnconnected({ isFetching: false });
|
|
58
|
+
expect(screen.queryByText("Loading")).not.toBeInTheDocument();
|
|
59
|
+
|
|
60
|
+
rerender(
|
|
61
|
+
<ChangePasswordForm
|
|
62
|
+
isFetching={true}
|
|
63
|
+
csrfToken="token"
|
|
64
|
+
changePassword={jest.fn()}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(screen.getByText("Loading")).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("renders the two labeled password inputs", () => {
|
|
72
|
+
const { container } = renderUnconnected();
|
|
73
|
+
|
|
74
|
+
expect(passwordInput(container)).toHaveAttribute("type", "password");
|
|
75
|
+
expect(confirmInput(container)).toHaveAttribute("type", "password");
|
|
76
|
+
|
|
77
|
+
const labels = container.querySelectorAll("label.control-label");
|
|
78
|
+
expect(labels[0]).toHaveTextContent("New Password");
|
|
79
|
+
expect(labels[1]).toHaveTextContent("Confirm New Password");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("does not submit a blank form", async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
const changePassword = jest.fn().mockResolvedValue(undefined);
|
|
85
|
+
renderUnconnected({ changePassword });
|
|
86
|
+
|
|
87
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
88
|
+
|
|
89
|
+
expect(changePassword).not.toHaveBeenCalled();
|
|
90
|
+
expect(screen.getByText("Fields cannot be blank.")).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Rendered fresh, so the error message asserted below can only have come from
|
|
94
|
+
// this partial submit rather than lingering from an earlier blank one.
|
|
95
|
+
it("does not submit a partially blank form", async () => {
|
|
96
|
+
const user = userEvent.setup();
|
|
97
|
+
const changePassword = jest.fn().mockResolvedValue(undefined);
|
|
98
|
+
const { container } = renderUnconnected({ changePassword });
|
|
99
|
+
expect(screen.queryByText("Fields cannot be blank.")).toBeNull();
|
|
100
|
+
|
|
101
|
+
// Only the first field filled.
|
|
102
|
+
await user.type(passwordInput(container), "newPassword");
|
|
103
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
104
|
+
|
|
105
|
+
expect(changePassword).not.toHaveBeenCalled();
|
|
106
|
+
expect(screen.getByText("Fields cannot be blank.")).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("does not submit when the passwords do not match", async () => {
|
|
110
|
+
const user = userEvent.setup();
|
|
111
|
+
const changePassword = jest.fn().mockResolvedValue(undefined);
|
|
112
|
+
const { container } = renderUnconnected({ changePassword });
|
|
113
|
+
|
|
114
|
+
await user.type(passwordInput(container), "newPassword");
|
|
115
|
+
await user.type(confirmInput(container), "somethingElse");
|
|
116
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
117
|
+
|
|
118
|
+
expect(changePassword).not.toHaveBeenCalled();
|
|
119
|
+
expect(screen.getByText("Passwords do not match.")).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("submits the new password and shows a success message", async () => {
|
|
123
|
+
const user = userEvent.setup();
|
|
124
|
+
const changePassword = jest.fn().mockResolvedValue(undefined);
|
|
125
|
+
const { container } = renderUnconnected({ changePassword });
|
|
126
|
+
|
|
127
|
+
await user.type(passwordInput(container), "newPassword");
|
|
128
|
+
await user.type(confirmInput(container), "newPassword");
|
|
129
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
130
|
+
|
|
131
|
+
expect(changePassword).toHaveBeenCalledTimes(1);
|
|
132
|
+
const submittedData = changePassword.mock.calls[0][0];
|
|
133
|
+
expect(submittedData.get("password")).toBe("newPassword");
|
|
134
|
+
expect(submittedData.get("confirm_password")).toBe("newPassword");
|
|
135
|
+
|
|
136
|
+
expect(
|
|
137
|
+
await screen.findByText("Password changed successfully")
|
|
138
|
+
).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("dispatches the password change through the connected store", async () => {
|
|
142
|
+
const user = userEvent.setup();
|
|
143
|
+
const fetchSpy = jest
|
|
144
|
+
.spyOn(globalThis, "fetch")
|
|
145
|
+
.mockResolvedValue(new Response("ok", { status: 200 }));
|
|
146
|
+
|
|
147
|
+
const { container } = renderWithProviders(
|
|
148
|
+
<ConnectedChangePasswordForm csrfToken="token" />
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await user.type(passwordInput(container), "newPassword");
|
|
152
|
+
await user.type(confirmInput(container), "newPassword");
|
|
153
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
154
|
+
|
|
155
|
+
expect(
|
|
156
|
+
await screen.findByText("Password changed successfully")
|
|
157
|
+
).toBeInTheDocument();
|
|
158
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
159
|
+
"/admin/change_password",
|
|
160
|
+
expect.objectContaining({ method: "POST" })
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, within } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import CirculationEventsDownloadForm from "../../../src/components/CirculationEventsDownloadForm";
|
|
6
|
+
|
|
7
|
+
describe("CirculationEventsDownloadForm", () => {
|
|
8
|
+
let hide: jest.Mock;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
hide = jest.fn();
|
|
12
|
+
render(<CirculationEventsDownloadForm show={true} hide={hide} />);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("renders start date and end date inputs", () => {
|
|
16
|
+
// The modal renders into a portal, so query the whole document.
|
|
17
|
+
const dates =
|
|
18
|
+
document.querySelectorAll<HTMLInputElement>("input[type='date']");
|
|
19
|
+
expect(dates).toHaveLength(2);
|
|
20
|
+
expect(dates[0].name).toBe("dateStart");
|
|
21
|
+
expect(dates[1].name).toBe("dateEnd");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders download and close buttons", async () => {
|
|
25
|
+
const user = userEvent.setup();
|
|
26
|
+
|
|
27
|
+
expect(
|
|
28
|
+
screen.getByRole("button", { name: "Download" })
|
|
29
|
+
).toBeInTheDocument();
|
|
30
|
+
|
|
31
|
+
// The modal header's dismiss control is also labeled "Close" (its visible
|
|
32
|
+
// "×" is aria-hidden), so scope to the footer for the reusable Close button.
|
|
33
|
+
const footer = document.querySelector<HTMLElement>(".modal-footer");
|
|
34
|
+
const closeButton = within(footer).getByRole("button", { name: "Close" });
|
|
35
|
+
expect(closeButton).toBeInTheDocument();
|
|
36
|
+
|
|
37
|
+
await user.click(closeButton);
|
|
38
|
+
|
|
39
|
+
expect(hide).toHaveBeenCalledTimes(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -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 "../fixtures/genreData";
|
|
8
|
+
import classificationsData from "../fixtures/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
|
+
});
|