@thepalaceproject/circulation-admin 1.43.0-post.1 → 1.43.0-post.11
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 +16 -7
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +56 -9
- package/package.json +6 -16
- 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/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/Autocomplete.test.tsx +67 -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/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/CatalogServices.test.tsx +63 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
- package/tests/jest/components/Classifications.test.tsx +166 -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/ConfigPage.test.tsx +58 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomListsForBook.test.tsx +315 -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/EditableConfigList.test.tsx +8 -2
- 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/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/LaneEditor.test.tsx +2 -6
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -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 +59 -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/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/SelfTestsCategory.test.tsx +200 -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 -8
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -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/index.test.tsx +50 -0
- package/tests/jest/legacy-setup.ts +13 -0
- package/tests/jest/legacyJsdomEnvironment.js +49 -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/components/AdvancedSearchBuilder.test.tsx +0 -38
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
|
|
6
|
+
// Header and Footer are page chrome; Header pulls in the react-router v3 context
|
|
7
|
+
// that renderWithProviders does not supply. Mock them to markers.
|
|
8
|
+
jest.mock("../../../src/components/Header", () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
default: () => <div data-testid="header" />,
|
|
11
|
+
}));
|
|
12
|
+
jest.mock("../../../src/components/Footer", () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: () => <div data-testid="footer" />,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import ManagePatrons from "../../../src/components/ManagePatrons";
|
|
18
|
+
|
|
19
|
+
describe("ManagePatrons", () => {
|
|
20
|
+
it("shows Header", () => {
|
|
21
|
+
renderWithProviders(<ManagePatrons params={{ library: "", tab: "" }} />);
|
|
22
|
+
|
|
23
|
+
expect(screen.getByTestId("header")).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should have a .manage-patrons-page class", () => {
|
|
27
|
+
const { container } = renderWithProviders(
|
|
28
|
+
<ManagePatrons params={{ library: "", tab: "" }} />
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(container.querySelector(".manage-patrons-page")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("shows heading", () => {
|
|
35
|
+
renderWithProviders(<ManagePatrons params={{ library: "", tab: "" }} />);
|
|
36
|
+
|
|
37
|
+
expect(
|
|
38
|
+
screen.getByRole("heading", { name: "Patron Manager" })
|
|
39
|
+
).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("shows ManagePatronsTabContainer", () => {
|
|
43
|
+
// A library manager for "NYPL" sees a tab per patron action, which proves
|
|
44
|
+
// both the library and tab props reached the tab container.
|
|
45
|
+
const { container } = renderWithProviders(
|
|
46
|
+
<ManagePatrons params={{ library: "NYPL", tab: "resetAdobeId" }} />,
|
|
47
|
+
{ appConfigSettings: { roles: [{ role: "manager", library: "NYPL" }] } }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const tabContainer = container.querySelector(".tab-container");
|
|
51
|
+
expect(tabContainer).toBeInTheDocument();
|
|
52
|
+
|
|
53
|
+
// The tab <li>s carry role="presentation", so query them directly.
|
|
54
|
+
const tabs = (tabContainer as HTMLElement).querySelectorAll(
|
|
55
|
+
"ul.nav-tabs > li"
|
|
56
|
+
);
|
|
57
|
+
expect(tabs).toHaveLength(2);
|
|
58
|
+
expect(
|
|
59
|
+
screen.getByRole("link", { name: "Reset Adobe ID" })
|
|
60
|
+
).toBeInTheDocument();
|
|
61
|
+
expect(
|
|
62
|
+
screen.getByRole("link", { name: "Debug Authentication" })
|
|
63
|
+
).toBeInTheDocument();
|
|
64
|
+
// The `tab` prop selects the active tab.
|
|
65
|
+
expect(tabs[0]).toHaveClass("active");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("shows Footer", () => {
|
|
69
|
+
renderWithProviders(<ManagePatrons params={{ library: "", tab: "" }} />);
|
|
70
|
+
|
|
71
|
+
expect(screen.getByTestId("footer")).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
|
+
import { installFormDataShim } from "../testUtils/formDataShim";
|
|
7
|
+
import buildStore from "../../../src/store";
|
|
8
|
+
import { ManagePatronsForm } from "../../../src/components/ManagePatronsForm";
|
|
9
|
+
import { PatronData } from "../../../src/interfaces";
|
|
10
|
+
|
|
11
|
+
// The reusable-components `Form` builds `new FormData(formElement)` on submit,
|
|
12
|
+
// which the unit jsdom environment's undici FormData rejects; install the shared
|
|
13
|
+
// shim that reads the form's successful controls.
|
|
14
|
+
installFormDataShim();
|
|
15
|
+
|
|
16
|
+
const patron: PatronData = {
|
|
17
|
+
authorization_expires: "",
|
|
18
|
+
username: "User Name",
|
|
19
|
+
personal_name: "Personal Name",
|
|
20
|
+
email_address: "user@email.com",
|
|
21
|
+
authorization_identifier: "1234",
|
|
22
|
+
authorization_identifiers: [""],
|
|
23
|
+
block_reason: "",
|
|
24
|
+
external_type: "",
|
|
25
|
+
fines: "",
|
|
26
|
+
permanent_id: "",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// `clearPatronData` is called on unmount (RTL cleanup), so it must always be a
|
|
30
|
+
// function; `patronLookup` too, so the connected dispatch props are stubbed.
|
|
31
|
+
const renderForm = (props: Record<string, unknown> = {}) =>
|
|
32
|
+
renderWithProviders(
|
|
33
|
+
<ManagePatronsForm
|
|
34
|
+
store={buildStore()}
|
|
35
|
+
csrfToken="token"
|
|
36
|
+
library="nypl"
|
|
37
|
+
patronLookup={jest.fn()}
|
|
38
|
+
clearPatronData={jest.fn()}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
describe("ManagePatronsForm", () => {
|
|
44
|
+
describe("rendering", () => {
|
|
45
|
+
it("has .manage-patrons-form class", () => {
|
|
46
|
+
const { container } = renderForm();
|
|
47
|
+
expect(
|
|
48
|
+
container.querySelector(".manage-patrons-form")
|
|
49
|
+
).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("shows an editable identifier input", () => {
|
|
53
|
+
renderForm();
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByPlaceholderText("Enter the patron's identifier")
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("has a form with an input field and a button", () => {
|
|
61
|
+
const { container } = renderForm();
|
|
62
|
+
expect(container.querySelectorAll("form")).toHaveLength(1);
|
|
63
|
+
expect(container.querySelectorAll("form input")).toHaveLength(1);
|
|
64
|
+
expect(container.querySelectorAll("form button")).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("doesn't initially show an alert or error message", () => {
|
|
68
|
+
const { container } = renderForm();
|
|
69
|
+
expect(container.querySelector(".alert")).not.toBeInTheDocument();
|
|
70
|
+
expect(screen.queryByText(/Patron found/)).not.toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("behavior", () => {
|
|
75
|
+
it("calls patronLookup on submit", async () => {
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
const patronLookup = jest.fn();
|
|
78
|
+
renderForm({ patronLookup });
|
|
79
|
+
|
|
80
|
+
expect(patronLookup).not.toHaveBeenCalled();
|
|
81
|
+
|
|
82
|
+
await user.type(
|
|
83
|
+
screen.getByPlaceholderText("Enter the patron's identifier"),
|
|
84
|
+
"1234"
|
|
85
|
+
);
|
|
86
|
+
await user.click(screen.getByRole("button"));
|
|
87
|
+
|
|
88
|
+
expect(patronLookup).toHaveBeenCalledTimes(1);
|
|
89
|
+
// The second argument is the library short name.
|
|
90
|
+
expect(patronLookup.mock.calls[0][1]).toBe("nypl");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("displays an error message if there is an error", () => {
|
|
94
|
+
const fetchError = { status: 400, response: "", url: "" };
|
|
95
|
+
const { container, rerender } = renderForm();
|
|
96
|
+
expect(container.querySelector(".alert-danger")).not.toBeInTheDocument();
|
|
97
|
+
|
|
98
|
+
rerender(
|
|
99
|
+
<ManagePatronsForm
|
|
100
|
+
store={buildStore()}
|
|
101
|
+
csrfToken="token"
|
|
102
|
+
library="nypl"
|
|
103
|
+
patronLookup={jest.fn()}
|
|
104
|
+
clearPatronData={jest.fn()}
|
|
105
|
+
fetchError={fetchError}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(container.querySelector(".alert-danger")).toBeInTheDocument();
|
|
110
|
+
expect(screen.getByText(/Error:/)).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should display a no-identifier message if the input field is blank when searching", () => {
|
|
114
|
+
const fetchError = {
|
|
115
|
+
status: 400,
|
|
116
|
+
response: "No patron identifier provided",
|
|
117
|
+
url: "",
|
|
118
|
+
};
|
|
119
|
+
const { rerender } = renderForm();
|
|
120
|
+
expect(
|
|
121
|
+
screen.queryByText("Error: No patron identifier provided")
|
|
122
|
+
).not.toBeInTheDocument();
|
|
123
|
+
|
|
124
|
+
rerender(
|
|
125
|
+
<ManagePatronsForm
|
|
126
|
+
store={buildStore()}
|
|
127
|
+
csrfToken="token"
|
|
128
|
+
library="nypl"
|
|
129
|
+
patronLookup={jest.fn()}
|
|
130
|
+
clearPatronData={jest.fn()}
|
|
131
|
+
fetchError={fetchError}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(
|
|
136
|
+
screen.getByText("Error: No patron identifier provided")
|
|
137
|
+
).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should display a success alert message when a patron is found", () => {
|
|
141
|
+
const { container, rerender } = renderForm();
|
|
142
|
+
expect(container.querySelector(".alert-success")).not.toBeInTheDocument();
|
|
143
|
+
|
|
144
|
+
rerender(
|
|
145
|
+
<ManagePatronsForm
|
|
146
|
+
store={buildStore()}
|
|
147
|
+
csrfToken="token"
|
|
148
|
+
library="nypl"
|
|
149
|
+
patronLookup={jest.fn()}
|
|
150
|
+
clearPatronData={jest.fn()}
|
|
151
|
+
patron={patron}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const alert = container.querySelector(".alert-success");
|
|
156
|
+
expect(alert).toBeInTheDocument();
|
|
157
|
+
expect(alert).toHaveTextContent(
|
|
158
|
+
`Patron found: ${patron.authorization_identifier}`
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should show the PatronInfo list when a patron is found", () => {
|
|
163
|
+
const { container, rerender } = renderForm();
|
|
164
|
+
expect(container.querySelector(".patron-info")).not.toBeInTheDocument();
|
|
165
|
+
|
|
166
|
+
rerender(
|
|
167
|
+
<ManagePatronsForm
|
|
168
|
+
store={buildStore()}
|
|
169
|
+
csrfToken="token"
|
|
170
|
+
library="nypl"
|
|
171
|
+
patronLookup={jest.fn()}
|
|
172
|
+
clearPatronData={jest.fn()}
|
|
173
|
+
patron={patron}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(container.querySelector(".patron-info")).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import buildStore from "../../../src/store";
|
|
7
|
+
import Admin from "../../../src/models/Admin";
|
|
8
|
+
|
|
9
|
+
// Both tab panels fetch on mount (ResetAdobeId is a connected component; Debug
|
|
10
|
+
// Authentication uses react-query). They're incidental to a tab-navigation test,
|
|
11
|
+
// so mock them to markers. ResetAdobeId echoes the csrfToken asserted below.
|
|
12
|
+
jest.mock("../../../src/components/ResetAdobeId", () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: (props: any) => (
|
|
15
|
+
<div data-testid="reset-adobe-id" data-csrf-token={props.csrfToken} />
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
jest.mock("../../../src/components/DebugAuthentication", () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: () => <div data-testid="debug-authentication" />,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import ManagePatronsTabContainer from "../../../src/components/ManagePatronsTabContainer";
|
|
24
|
+
|
|
25
|
+
// ManagePatronsTabContainer reads `admin`, `router`, and `pathFor` from the legacy
|
|
26
|
+
// React context the app supplies in production. Wrap it in a small provider that
|
|
27
|
+
// hands it those.
|
|
28
|
+
class LegacyContextProvider extends React.Component<{ context: any }> {
|
|
29
|
+
static childContextTypes = {
|
|
30
|
+
router: PropTypes.object,
|
|
31
|
+
pathFor: PropTypes.func,
|
|
32
|
+
admin: PropTypes.object,
|
|
33
|
+
};
|
|
34
|
+
getChildContext() {
|
|
35
|
+
return this.props.context;
|
|
36
|
+
}
|
|
37
|
+
render() {
|
|
38
|
+
return this.props.children;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("ManagePatronsTabContainer", () => {
|
|
43
|
+
const systemAdmin = new Admin([{ role: "system" }]);
|
|
44
|
+
const libraryManagerNYPL = new Admin([{ role: "manager", library: "NYPL" }]);
|
|
45
|
+
const libraryManagerA = new Admin([{ role: "manager", library: "a" }]);
|
|
46
|
+
const librarian = new Admin([{ role: "librarian", library: "NYPL" }]);
|
|
47
|
+
|
|
48
|
+
const renderWithAdmin = (admin: Admin, push: jest.Mock = jest.fn()) => {
|
|
49
|
+
const context = { router: { push }, pathFor: jest.fn(), admin };
|
|
50
|
+
const result = render(
|
|
51
|
+
<LegacyContextProvider context={context}>
|
|
52
|
+
<ManagePatronsTabContainer
|
|
53
|
+
tab={null}
|
|
54
|
+
csrfToken="token"
|
|
55
|
+
store={buildStore()}
|
|
56
|
+
library="NYPL"
|
|
57
|
+
/>
|
|
58
|
+
</LegacyContextProvider>
|
|
59
|
+
);
|
|
60
|
+
return { ...result, push };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("for system admin", () => {
|
|
64
|
+
it("shows tabs", () => {
|
|
65
|
+
renderWithAdmin(systemAdmin);
|
|
66
|
+
expect(
|
|
67
|
+
screen.getByRole("link", { name: "Reset Adobe ID" })
|
|
68
|
+
).toBeInTheDocument();
|
|
69
|
+
expect(
|
|
70
|
+
screen.getByRole("link", { name: "Debug Authentication" })
|
|
71
|
+
).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("shows components", () => {
|
|
75
|
+
renderWithAdmin(systemAdmin);
|
|
76
|
+
expect(screen.getByTestId("reset-adobe-id")).toHaveAttribute(
|
|
77
|
+
"data-csrf-token",
|
|
78
|
+
"token"
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("uses router to navigate when tab is clicked", async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
const { push } = renderWithAdmin(systemAdmin);
|
|
85
|
+
await user.click(screen.getByRole("link", { name: "Reset Adobe ID" }));
|
|
86
|
+
expect(push).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(push).toHaveBeenCalledWith("/admin/web/patrons/NYPL/resetAdobeId");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("for right library manager", () => {
|
|
92
|
+
it("shows tabs", () => {
|
|
93
|
+
renderWithAdmin(libraryManagerNYPL);
|
|
94
|
+
expect(
|
|
95
|
+
screen.getByRole("link", { name: "Reset Adobe ID" })
|
|
96
|
+
).toBeInTheDocument();
|
|
97
|
+
expect(
|
|
98
|
+
screen.getByRole("link", { name: "Debug Authentication" })
|
|
99
|
+
).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("marks the current tab as active", () => {
|
|
103
|
+
const { container } = renderWithAdmin(libraryManagerNYPL);
|
|
104
|
+
const tabItems = container.querySelectorAll("ul.nav-tabs li");
|
|
105
|
+
expect(tabItems).toHaveLength(2);
|
|
106
|
+
expect(tabItems[0]).toHaveClass("active");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("shows components", () => {
|
|
110
|
+
renderWithAdmin(libraryManagerNYPL);
|
|
111
|
+
expect(screen.getByTestId("reset-adobe-id")).toHaveAttribute(
|
|
112
|
+
"data-csrf-token",
|
|
113
|
+
"token"
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("uses router to navigate when tab is clicked", async () => {
|
|
118
|
+
const user = userEvent.setup();
|
|
119
|
+
const { push } = renderWithAdmin(libraryManagerNYPL);
|
|
120
|
+
await user.click(screen.getByRole("link", { name: "Reset Adobe ID" }));
|
|
121
|
+
expect(push).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(push).toHaveBeenCalledWith("/admin/web/patrons/NYPL/resetAdobeId");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("for wrong library manager", () => {
|
|
127
|
+
it("doesn't show tabs", () => {
|
|
128
|
+
renderWithAdmin(libraryManagerA);
|
|
129
|
+
expect(
|
|
130
|
+
screen.queryByRole("link", { name: "Reset Adobe ID" })
|
|
131
|
+
).not.toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("for librarian", () => {
|
|
136
|
+
it("doesn't show tabs", () => {
|
|
137
|
+
renderWithAdmin(librarian);
|
|
138
|
+
expect(
|
|
139
|
+
screen.queryByRole("link", { name: "Reset Adobe ID" })
|
|
140
|
+
).not.toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen, within } from "@testing-library/react";
|
|
3
|
+
import { MetadataServices } from "../../../src/components/MetadataServices";
|
|
4
|
+
import renderWithContext from "../testUtils/renderWithContext";
|
|
5
|
+
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
|
|
6
|
+
|
|
7
|
+
describe("MetadataServices", () => {
|
|
8
|
+
const data = {
|
|
9
|
+
metadata_services: [
|
|
10
|
+
{
|
|
11
|
+
id: 2,
|
|
12
|
+
protocol: "test protocol",
|
|
13
|
+
name: "sample name",
|
|
14
|
+
settings: {
|
|
15
|
+
test_setting: "test setting",
|
|
16
|
+
},
|
|
17
|
+
libraries: [{ short_name: "nypl" }],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
protocols: [
|
|
21
|
+
{
|
|
22
|
+
name: "test protocol",
|
|
23
|
+
label: "test protocol label",
|
|
24
|
+
sitewide: false,
|
|
25
|
+
settings: [],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
allLibraries: [
|
|
29
|
+
{
|
|
30
|
+
short_name: "nypl",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const config = { csrfToken: "token", featureFlags: defaultFeatureFlags };
|
|
36
|
+
|
|
37
|
+
it("shows metadata service list", () => {
|
|
38
|
+
renderWithContext(
|
|
39
|
+
<MetadataServices
|
|
40
|
+
data={data}
|
|
41
|
+
fetchData={jest.fn()}
|
|
42
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
43
|
+
csrfToken="token"
|
|
44
|
+
isFetching={false}
|
|
45
|
+
/>,
|
|
46
|
+
config
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const items = screen.getAllByRole("listitem");
|
|
50
|
+
expect(items).toHaveLength(1);
|
|
51
|
+
expect(items[0]).toHaveTextContent("sample name: test protocol");
|
|
52
|
+
|
|
53
|
+
const editLink = within(items[0]).getByRole("link");
|
|
54
|
+
expect(editLink).toHaveAttribute(
|
|
55
|
+
"href",
|
|
56
|
+
"/admin/web/config/metadata/edit/2"
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen, waitFor } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
import buildStore from "../../../src/store";
|
|
6
|
+
// Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
|
|
7
|
+
// are exercised: the panel fetches its data on mount, the fetch populates the
|
|
8
|
+
// Redux store, and mapStateToProps feeds it back in as props.
|
|
9
|
+
import PatronAuthServices from "../../../src/components/PatronAuthServices";
|
|
10
|
+
|
|
11
|
+
const listData = {
|
|
12
|
+
patron_auth_services: [
|
|
13
|
+
{
|
|
14
|
+
id: 2,
|
|
15
|
+
protocol: "test protocol",
|
|
16
|
+
settings: { test_setting: "test setting" },
|
|
17
|
+
libraries: [
|
|
18
|
+
{ short_name: "nypl", test_library_setting: "test library setting" },
|
|
19
|
+
],
|
|
20
|
+
name: "nypl protocol",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
protocols: [
|
|
24
|
+
{ name: "test protocol", label: "test protocol label", settings: [] },
|
|
25
|
+
],
|
|
26
|
+
allLibraries: [{ short_name: "nypl" }],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stub `fetch` so the panel's on-mount fetch resolves with `body`. Returning a
|
|
31
|
+
* fresh Response per call avoids "body already used" when several fetches run.
|
|
32
|
+
*/
|
|
33
|
+
const stubFetch = (body: unknown) =>
|
|
34
|
+
jest.spyOn(globalThis, "fetch").mockImplementation(
|
|
35
|
+
async () =>
|
|
36
|
+
new Response(JSON.stringify(body), {
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// A fresh store per test keeps the fetched data isolated between tests.
|
|
43
|
+
const renderConnected = (element: React.ReactElement) =>
|
|
44
|
+
renderWithProviders(element, {
|
|
45
|
+
reduxProviderProps: { store: buildStore() },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("PatronAuthServices", () => {
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
jest.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("shows the patron auth service list", async () => {
|
|
54
|
+
stubFetch(listData);
|
|
55
|
+
const { container } = renderConnected(
|
|
56
|
+
<PatronAuthServices csrfToken="token" />
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// The list appears once the connected component's on-mount fetch resolves.
|
|
60
|
+
// Its heading is the protocol-aware label built by PatronAuthServices.label().
|
|
61
|
+
expect(
|
|
62
|
+
await screen.findByRole("heading", {
|
|
63
|
+
level: 3,
|
|
64
|
+
name: /nypl protocol: test protocol label/,
|
|
65
|
+
})
|
|
66
|
+
).toBeInTheDocument();
|
|
67
|
+
|
|
68
|
+
const editLink = container.querySelector("a.edit-item");
|
|
69
|
+
expect(editLink).not.toBeNull();
|
|
70
|
+
expect(editLink.getAttribute("href")).toBe(
|
|
71
|
+
"/admin/web/config/patronAuth/edit/2"
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// No neighborhood analytics panel is shown in list mode.
|
|
75
|
+
expect(
|
|
76
|
+
screen.queryByText("Patron Neighborhood Analytics: Disabled")
|
|
77
|
+
).not.toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("shows the neighborhood analytics panel when creating a service whose protocol has a neighborhood_mode setting", async () => {
|
|
81
|
+
const neighborhoodSetting = {
|
|
82
|
+
key: "neighborhood_mode",
|
|
83
|
+
label: "Neighborhood Mode",
|
|
84
|
+
description: "",
|
|
85
|
+
options: [],
|
|
86
|
+
};
|
|
87
|
+
const createData = {
|
|
88
|
+
patron_auth_services: [],
|
|
89
|
+
protocols: [
|
|
90
|
+
{
|
|
91
|
+
name: "test protocol",
|
|
92
|
+
label: "test protocol label",
|
|
93
|
+
settings: [neighborhoodSetting],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
allLibraries: [{ short_name: "nypl" }],
|
|
97
|
+
};
|
|
98
|
+
stubFetch(createData);
|
|
99
|
+
|
|
100
|
+
const { container } = renderConnected(
|
|
101
|
+
<PatronAuthServices csrfToken="token" editOrCreate="create" />
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Once the protocol data loads, the create form's extra section (the
|
|
105
|
+
// NeighborhoodAnalyticsForm) renders because the selected protocol carries a
|
|
106
|
+
// "neighborhood_mode" setting.
|
|
107
|
+
await waitFor(() =>
|
|
108
|
+
expect(container.querySelector("#neighborhood-panel")).toBeInTheDocument()
|
|
109
|
+
);
|
|
110
|
+
expect(
|
|
111
|
+
screen.getByText("Patron Neighborhood Analytics: Disabled")
|
|
112
|
+
).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -612,7 +612,9 @@ describe("PatronBlockingRulesEditor", () => {
|
|
|
612
612
|
});
|
|
613
613
|
|
|
614
614
|
it("disables all editing inputs and buttons when disabled prop is true", () => {
|
|
615
|
-
renderEditor(
|
|
615
|
+
renderEditor(
|
|
616
|
+
<PatronBlockingRulesEditor value={existingRules} disabled={true} />
|
|
617
|
+
);
|
|
616
618
|
|
|
617
619
|
// The Help button stays enabled even in disabled mode (read-only affordance).
|
|
618
620
|
const editingButtons = screen
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import PatronInfo from "../../../src/components/PatronInfo";
|
|
5
|
+
import { PatronData } from "../../../src/interfaces";
|
|
6
|
+
|
|
7
|
+
const patrons: PatronData[] = [
|
|
8
|
+
{
|
|
9
|
+
authorization_expires: "",
|
|
10
|
+
username: "User Name",
|
|
11
|
+
personal_name: "Personal Name",
|
|
12
|
+
email_address: "user@email.com",
|
|
13
|
+
authorization_identifier: "1234",
|
|
14
|
+
authorization_identifiers: [""],
|
|
15
|
+
block_reason: "",
|
|
16
|
+
external_type: "",
|
|
17
|
+
fines: "",
|
|
18
|
+
permanent_id: "",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
authorization_expires: "",
|
|
22
|
+
personal_name: "Personal Name2",
|
|
23
|
+
authorization_identifier: "5678",
|
|
24
|
+
authorization_identifiers: [""],
|
|
25
|
+
block_reason: "",
|
|
26
|
+
external_type: "",
|
|
27
|
+
fines: "",
|
|
28
|
+
permanent_id: "",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
describe("PatronInfo", () => {
|
|
33
|
+
it("should display the patron's information", () => {
|
|
34
|
+
const { container } = render(<PatronInfo patron={patrons[0]} />);
|
|
35
|
+
|
|
36
|
+
const items = container.querySelectorAll("ul.patron-data-list > li");
|
|
37
|
+
expect(items).toHaveLength(4);
|
|
38
|
+
expect(items[0]).toHaveTextContent("Username:User Name");
|
|
39
|
+
expect(items[1]).toHaveTextContent("Personal Name:Personal Name");
|
|
40
|
+
expect(items[2]).toHaveTextContent("Email Address:user@email.com");
|
|
41
|
+
expect(items[3]).toHaveTextContent("Identifier:1234");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should update the patron's information if a new patron was searched and found", () => {
|
|
45
|
+
const { container, rerender } = render(<PatronInfo patron={patrons[0]} />);
|
|
46
|
+
|
|
47
|
+
rerender(<PatronInfo patron={patrons[1]} />);
|
|
48
|
+
|
|
49
|
+
const items = container.querySelectorAll("ul.patron-data-list > li");
|
|
50
|
+
expect(items).toHaveLength(2);
|
|
51
|
+
expect(items[0]).toHaveTextContent("Personal Name:Personal Name2");
|
|
52
|
+
expect(items[1]).toHaveTextContent("Identifier:5678");
|
|
53
|
+
});
|
|
54
|
+
});
|