@thepalaceproject/circulation-admin 1.43.0-post.8 → 1.43.0-post.9

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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
  3. package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
  4. package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
  5. package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
  6. package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
  7. package/tests/jest/components/CatalogServices.test.tsx +63 -0
  8. package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
  9. package/tests/jest/components/CustomListSearch.test.tsx +275 -0
  10. package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
  11. package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
  12. package/tests/jest/components/ErrorMessage.test.tsx +97 -0
  13. package/tests/jest/components/Footer.test.tsx +141 -0
  14. package/tests/jest/components/LanguageField.test.tsx +40 -0
  15. package/tests/jest/components/LoadButton.test.tsx +49 -0
  16. package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
  17. package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
  18. package/tests/jest/components/MetadataServices.test.tsx +59 -0
  19. package/tests/jest/components/SelfTestResult.test.tsx +164 -0
  20. package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
  21. package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
  22. package/tests/jest/components/TabContainer.test.tsx +68 -0
  23. package/tests/jest/components/Timestamp.test.tsx +74 -0
  24. package/tests/jest/components/WithEditButton.test.tsx +55 -0
  25. package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
  26. package/tests/jest/index.test.tsx +50 -0
  27. package/tests/jest/components/AdvancedSearchBuilder.test.tsx +0 -38
@@ -0,0 +1,49 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import LoadButton from "../../../src/components/LoadButton";
6
+
7
+ describe("LoadButton", () => {
8
+ it("displays 'Load more' text when not fetching", () => {
9
+ render(<LoadButton isFetching={false} loadMore={jest.fn()} />);
10
+
11
+ expect(
12
+ screen.getByRole("button", { name: "Load more" })
13
+ ).toBeInTheDocument();
14
+ });
15
+
16
+ it("renders the MoreDotsIcon only while fetching", () => {
17
+ const { container, rerender } = render(
18
+ <LoadButton isFetching={false} loadMore={jest.fn()} />
19
+ );
20
+
21
+ // The MoreDotsIcon is a Font Awesome-style <svg>, so query for it directly.
22
+ expect(container.querySelector("svg")).not.toBeInTheDocument();
23
+
24
+ rerender(<LoadButton isFetching={true} loadMore={jest.fn()} />);
25
+
26
+ expect(container.querySelector("svg")).toBeInTheDocument();
27
+ expect(screen.queryByText("Load more")).not.toBeInTheDocument();
28
+ });
29
+
30
+ it("calls loadMore when clicked", async () => {
31
+ const user = userEvent.setup();
32
+ const loadMore = jest.fn();
33
+ render(<LoadButton isFetching={false} loadMore={loadMore} />);
34
+
35
+ await user.click(screen.getByRole("button"));
36
+
37
+ expect(loadMore).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it("does nothing when disabled", async () => {
41
+ const user = userEvent.setup();
42
+ const loadMore = jest.fn();
43
+ render(<LoadButton isFetching={true} loadMore={loadMore} />);
44
+
45
+ await user.click(screen.getByRole("button"));
46
+
47
+ expect(loadMore).not.toHaveBeenCalled();
48
+ });
49
+ });
@@ -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,164 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ import SelfTestResult from "../../../src/components/SelfTestResult";
5
+ import { SelfTestsResult } from "../../../src/interfaces";
6
+
7
+ const stringResult: SelfTestsResult = {
8
+ duration: 0,
9
+ end: "",
10
+ name: "Test Result",
11
+ result: "abc",
12
+ start: "",
13
+ success: true,
14
+ };
15
+
16
+ describe("SelfTestResult", () => {
17
+ it("displays a result with a string", () => {
18
+ const { container } = render(
19
+ <SelfTestResult result={stringResult} isFetching={false} />
20
+ );
21
+
22
+ expect(container.querySelector("li")).toHaveClass("success");
23
+
24
+ expect(screen.getByRole("heading", { level: 4 })).toHaveTextContent(
25
+ "Test Result"
26
+ );
27
+
28
+ const testResult = container.querySelector("pre");
29
+ expect(testResult).toHaveClass("result-description");
30
+ expect(testResult).toHaveTextContent("abc");
31
+
32
+ expect(screen.getByText("success: true")).toHaveClass(
33
+ "success-description"
34
+ );
35
+
36
+ // A single-line string result is not shown in a collapsible panel.
37
+ expect(container.querySelector(".panel")).not.toBeInTheDocument();
38
+ });
39
+
40
+ it("displays a result with a multi-line string", () => {
41
+ const longStringResult: SelfTestsResult = {
42
+ ...stringResult,
43
+ result: "abc \n def",
44
+ };
45
+ const { container } = render(
46
+ <SelfTestResult result={longStringResult} isFetching={false} />
47
+ );
48
+
49
+ const panel = container.querySelector(".panel");
50
+ expect(panel).toBeInTheDocument();
51
+ expect(panel).toHaveClass("panel-success");
52
+ expect(screen.getByText("Results")).toBeInTheDocument();
53
+
54
+ expect(container.querySelector("pre")?.textContent).toBe("abc \n def");
55
+ });
56
+
57
+ it("displays a result with an array", () => {
58
+ const arrayResult: SelfTestsResult = {
59
+ ...stringResult,
60
+ result: ["Thing 1", "Thing 2"],
61
+ };
62
+ const { container } = render(
63
+ <SelfTestResult result={arrayResult} isFetching={false} />
64
+ );
65
+
66
+ const panel = container.querySelector(".panel");
67
+ expect(panel).toBeInTheDocument();
68
+ expect(panel).toHaveClass("panel-success");
69
+ expect(screen.getByText("Results (2)")).toBeInTheDocument();
70
+
71
+ const items = container.querySelectorAll("ol li");
72
+ expect(items).toHaveLength(2);
73
+ expect(items[0]).toHaveTextContent("Thing 1");
74
+ expect(items[1]).toHaveTextContent("Thing 2");
75
+ });
76
+
77
+ it("displays a result with an empty array", () => {
78
+ const emptyArrayResult: SelfTestsResult = { ...stringResult, result: [] };
79
+ const { container } = render(
80
+ <SelfTestResult result={emptyArrayResult} isFetching={false} />
81
+ );
82
+
83
+ expect(container.querySelector(".panel")).not.toBeInTheDocument();
84
+ expect(
85
+ screen.getByText("The test ran successfully, but no results were found.")
86
+ ).toHaveClass("warning");
87
+ });
88
+
89
+ it("displays a result with an exception", () => {
90
+ const exceptionResult: SelfTestsResult = {
91
+ ...stringResult,
92
+ success: false,
93
+ exception: { class: "IntegrationException", message: "Problem!" },
94
+ };
95
+ const { container } = render(
96
+ <SelfTestResult result={exceptionResult} isFetching={false} />
97
+ );
98
+
99
+ expect(container.querySelector("li")).toHaveClass("failure");
100
+ expect(screen.getByText("success: false")).toHaveClass(
101
+ "success-description"
102
+ );
103
+ expect(screen.getByText("exception: Problem!")).toHaveClass(
104
+ "exception-description"
105
+ );
106
+ });
107
+
108
+ it("displays debug_message when present", () => {
109
+ const exceptionResult: SelfTestsResult = {
110
+ ...stringResult,
111
+ success: false,
112
+ exception: {
113
+ class: "IntegrationException",
114
+ debug_message: "debug message...",
115
+ message: "Problem!",
116
+ },
117
+ };
118
+ render(<SelfTestResult result={exceptionResult} isFetching={false} />);
119
+
120
+ expect(screen.getByText("debug: debug message...")).toHaveClass(
121
+ "debug-description"
122
+ );
123
+ });
124
+
125
+ it("does not display debug_message when it is empty", () => {
126
+ const exceptionResult: SelfTestsResult = {
127
+ ...stringResult,
128
+ success: false,
129
+ exception: {
130
+ class: "IntegrationException",
131
+ debug_message: "",
132
+ message: "Problem!",
133
+ },
134
+ };
135
+ const { container } = render(
136
+ <SelfTestResult result={exceptionResult} isFetching={false} />
137
+ );
138
+
139
+ expect(
140
+ container.querySelector(".exception-description")
141
+ ).toBeInTheDocument();
142
+ expect(
143
+ container.querySelector(".debug-description")
144
+ ).not.toBeInTheDocument();
145
+ });
146
+
147
+ it("does not display debug_message when it is absent", () => {
148
+ const exceptionResult: SelfTestsResult = {
149
+ ...stringResult,
150
+ success: false,
151
+ exception: { class: "IntegrationException", message: "Problem!" },
152
+ };
153
+ const { container } = render(
154
+ <SelfTestResult result={exceptionResult} isFetching={false} />
155
+ );
156
+
157
+ expect(
158
+ container.querySelector(".exception-description")
159
+ ).toBeInTheDocument();
160
+ expect(
161
+ container.querySelector(".debug-description")
162
+ ).not.toBeInTheDocument();
163
+ });
164
+ });