@thepalaceproject/circulation-admin 1.35.0 → 1.36.0-post.2
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/dist/circulation-admin.css +1 -1
- package/dist/circulation-admin.js +1 -1
- package/package.json +3 -3
- package/tests/jest/components/CollectionImportButton.test.tsx +207 -0
- package/tests/jest/components/DebugAuthentication.test.tsx +281 -0
- package/tests/jest/components/DebugResultListItem.test.tsx +162 -0
package/package.json
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"numeral": "^2.0.6",
|
|
51
51
|
"opds-feed-parser": "0.0.17",
|
|
52
52
|
"prop-types": "^15.7.2",
|
|
53
|
-
"qs": "^6.14.
|
|
53
|
+
"qs": "^6.14.2",
|
|
54
54
|
"react": "^16.8.6",
|
|
55
55
|
"react-beautiful-dnd": "^2.3.1",
|
|
56
56
|
"react-bootstrap": "^0.32.4",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"typedoc": "^0.27.9",
|
|
138
138
|
"typescript": "^5.7.3",
|
|
139
139
|
"url-loader": "^4.1.1",
|
|
140
|
-
"webpack": "^5.
|
|
140
|
+
"webpack": "^5.105.2",
|
|
141
141
|
"webpack-cli": "^5.0.1",
|
|
142
142
|
"webpack-dev-server": "^5.2.2",
|
|
143
143
|
"webpack-merge": "^5.8.0"
|
|
@@ -152,5 +152,5 @@
|
|
|
152
152
|
"*.{js,jsx,ts,tsx,css,md}": "prettier --write",
|
|
153
153
|
"*.{js,css,md}": "prettier --write"
|
|
154
154
|
},
|
|
155
|
-
"version": "1.
|
|
155
|
+
"version": "1.36.0-post.2"
|
|
156
156
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import CollectionImportButton, {
|
|
5
|
+
CollectionImportButtonProps,
|
|
6
|
+
} from "../../../src/components/CollectionImportButton";
|
|
7
|
+
import { CollectionData, ProtocolData } from "../../../src/interfaces";
|
|
8
|
+
|
|
9
|
+
const protocolWithImport: ProtocolData = {
|
|
10
|
+
name: "Boundless",
|
|
11
|
+
label: "Boundless",
|
|
12
|
+
supports_import: true,
|
|
13
|
+
settings: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const protocolWithoutImport: ProtocolData = {
|
|
17
|
+
name: "Overdrive",
|
|
18
|
+
label: "Overdrive",
|
|
19
|
+
supports_import: false,
|
|
20
|
+
settings: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const savedCollection: CollectionData = {
|
|
24
|
+
id: 42,
|
|
25
|
+
protocol: "Boundless",
|
|
26
|
+
name: "Test Collection",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const unsavedCollection: CollectionData = {
|
|
30
|
+
protocol: "Boundless",
|
|
31
|
+
name: "Unsaved Collection",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function renderButton(overrides: Partial<CollectionImportButtonProps> = {}) {
|
|
35
|
+
const defaultProps: CollectionImportButtonProps = {
|
|
36
|
+
collection: savedCollection,
|
|
37
|
+
protocols: [protocolWithImport, protocolWithoutImport],
|
|
38
|
+
importCollection: jest.fn().mockResolvedValue(undefined),
|
|
39
|
+
disabled: false,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
...render(<CollectionImportButton {...defaultProps} />),
|
|
44
|
+
importCollection: defaultProps.importCollection as jest.Mock,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Expand the collapsed Import panel by clicking its header. */
|
|
49
|
+
async function expandPanel(user: ReturnType<typeof userEvent.setup>) {
|
|
50
|
+
await user.click(screen.getByText("Import"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("CollectionImportButton", () => {
|
|
54
|
+
it("does not render when protocol lacks supports_import", () => {
|
|
55
|
+
const collection: CollectionData = {
|
|
56
|
+
id: 1,
|
|
57
|
+
protocol: "Overdrive",
|
|
58
|
+
name: "OD Collection",
|
|
59
|
+
};
|
|
60
|
+
const { container } = renderButton({ collection });
|
|
61
|
+
expect(container.innerHTML).toBe("");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not render for unsaved collection (no id)", () => {
|
|
65
|
+
const { container } = renderButton({ collection: unsavedCollection });
|
|
66
|
+
expect(container.innerHTML).toBe("");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders panel header when supported", () => {
|
|
70
|
+
renderButton();
|
|
71
|
+
expect(screen.getByText("Import")).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders button and checkbox when panel is expanded", async () => {
|
|
75
|
+
const user = userEvent.setup();
|
|
76
|
+
renderButton();
|
|
77
|
+
await expandPanel(user);
|
|
78
|
+
expect(
|
|
79
|
+
screen.getByRole("button", { name: "Queue Import" })
|
|
80
|
+
).toBeInTheDocument();
|
|
81
|
+
expect(screen.getByRole("checkbox")).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByText("Force full re-import")).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("checkbox toggles force state", async () => {
|
|
86
|
+
const user = userEvent.setup();
|
|
87
|
+
renderButton();
|
|
88
|
+
await expandPanel(user);
|
|
89
|
+
const checkbox = screen.getByRole("checkbox");
|
|
90
|
+
expect(checkbox).not.toBeChecked();
|
|
91
|
+
await user.click(checkbox);
|
|
92
|
+
expect(checkbox).toBeChecked();
|
|
93
|
+
await user.click(checkbox);
|
|
94
|
+
expect(checkbox).not.toBeChecked();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("button triggers import with correct args (force=false)", async () => {
|
|
98
|
+
const user = userEvent.setup();
|
|
99
|
+
const { importCollection } = renderButton();
|
|
100
|
+
await expandPanel(user);
|
|
101
|
+
const button = screen.getByRole("button", { name: "Queue Import" });
|
|
102
|
+
await user.click(button);
|
|
103
|
+
expect(importCollection).toHaveBeenCalledWith(42, false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("button triggers import with force=true when checked", async () => {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
const { importCollection } = renderButton();
|
|
109
|
+
await expandPanel(user);
|
|
110
|
+
const checkbox = screen.getByRole("checkbox");
|
|
111
|
+
await user.click(checkbox);
|
|
112
|
+
const button = screen.getByRole("button", { name: "Queue Import" });
|
|
113
|
+
await user.click(button);
|
|
114
|
+
expect(importCollection).toHaveBeenCalledWith(42, true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("shows success feedback with alert-success styling after import", async () => {
|
|
118
|
+
const user = userEvent.setup();
|
|
119
|
+
renderButton();
|
|
120
|
+
await expandPanel(user);
|
|
121
|
+
await user.click(screen.getByRole("button", { name: "Queue Import" }));
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
const feedback = screen.getByText("Import task queued.");
|
|
124
|
+
expect(feedback).toBeInTheDocument();
|
|
125
|
+
expect(feedback).toHaveClass("alert", "alert-success");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("shows error feedback with alert-danger styling on failure", async () => {
|
|
130
|
+
const user = userEvent.setup();
|
|
131
|
+
const mockImport = jest
|
|
132
|
+
.fn()
|
|
133
|
+
.mockRejectedValue({ response: "Something went wrong" });
|
|
134
|
+
renderButton({ importCollection: mockImport });
|
|
135
|
+
await expandPanel(user);
|
|
136
|
+
await user.click(screen.getByRole("button", { name: "Queue Import" }));
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
const feedback = screen.getByText("Something went wrong");
|
|
139
|
+
expect(feedback).toBeInTheDocument();
|
|
140
|
+
expect(feedback).toHaveClass("alert", "alert-danger");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("resets force checkbox and feedback when switching collections", async () => {
|
|
145
|
+
const user = userEvent.setup();
|
|
146
|
+
const { rerender, importCollection } = renderButton();
|
|
147
|
+
await expandPanel(user);
|
|
148
|
+
|
|
149
|
+
const checkbox = screen.getByRole("checkbox");
|
|
150
|
+
await user.click(checkbox);
|
|
151
|
+
expect(checkbox).toBeChecked();
|
|
152
|
+
|
|
153
|
+
await user.click(screen.getByRole("button", { name: "Queue Import" }));
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(screen.getByText("Import task queued.")).toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const nextCollection: CollectionData = {
|
|
159
|
+
id: 99,
|
|
160
|
+
protocol: "Boundless",
|
|
161
|
+
name: "Another Collection",
|
|
162
|
+
};
|
|
163
|
+
rerender(
|
|
164
|
+
<CollectionImportButton
|
|
165
|
+
collection={nextCollection}
|
|
166
|
+
protocols={[protocolWithImport, protocolWithoutImport]}
|
|
167
|
+
importCollection={importCollection}
|
|
168
|
+
disabled={false}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(screen.getByRole("checkbox")).not.toBeChecked();
|
|
174
|
+
expect(screen.queryByText("Import task queued.")).not.toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("disables button and checkbox when disabled prop is true", async () => {
|
|
179
|
+
const user = userEvent.setup();
|
|
180
|
+
renderButton({ disabled: true });
|
|
181
|
+
await expandPanel(user);
|
|
182
|
+
expect(screen.getByRole("button", { name: "Queue Import" })).toBeDisabled();
|
|
183
|
+
expect(screen.getByRole("checkbox")).toBeDisabled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("shows 'Queuing...' text while importing", async () => {
|
|
187
|
+
const user = userEvent.setup();
|
|
188
|
+
let resolveImport: () => void;
|
|
189
|
+
const pendingImport = new Promise<void>((resolve) => {
|
|
190
|
+
resolveImport = resolve;
|
|
191
|
+
});
|
|
192
|
+
const mockImport = jest.fn().mockReturnValue(pendingImport);
|
|
193
|
+
renderButton({ importCollection: mockImport });
|
|
194
|
+
await expandPanel(user);
|
|
195
|
+
|
|
196
|
+
await user.click(screen.getByRole("button", { name: "Queue Import" }));
|
|
197
|
+
|
|
198
|
+
expect(screen.getByRole("button", { name: "Queuing..." })).toBeDisabled();
|
|
199
|
+
|
|
200
|
+
resolveImport();
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(
|
|
203
|
+
screen.getByRole("button", { name: "Queue Import" })
|
|
204
|
+
).toBeEnabled();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { setupServer } from "msw/node";
|
|
3
|
+
import { http, HttpResponse } from "msw";
|
|
4
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
|
+
import DebugAuthentication from "../../../src/components/DebugAuthentication";
|
|
7
|
+
import {
|
|
8
|
+
AuthMethodsResponse,
|
|
9
|
+
PatronDebugResponse,
|
|
10
|
+
} from "../../../src/api/patronDebug";
|
|
11
|
+
import { waitFor, fireEvent } from "@testing-library/react";
|
|
12
|
+
|
|
13
|
+
const LIBRARY = "test-library";
|
|
14
|
+
const SECOND_LIBRARY = "another-library";
|
|
15
|
+
const AUTH_METHODS_PATH = `/${LIBRARY}/admin/manage_patrons/auth_methods`;
|
|
16
|
+
const DEBUG_AUTH_PATH = `/${LIBRARY}/admin/manage_patrons/debug_auth`;
|
|
17
|
+
const SECOND_AUTH_METHODS_PATH = `/${SECOND_LIBRARY}/admin/manage_patrons/auth_methods`;
|
|
18
|
+
|
|
19
|
+
const MOCK_AUTH_METHODS: AuthMethodsResponse = {
|
|
20
|
+
authMethods: [
|
|
21
|
+
{
|
|
22
|
+
id: 1,
|
|
23
|
+
name: "Test SIP2",
|
|
24
|
+
protocol: "api.sip",
|
|
25
|
+
supportsDebug: true,
|
|
26
|
+
supportsPassword: true,
|
|
27
|
+
identifierLabel: "Barcode",
|
|
28
|
+
passwordLabel: "PIN",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 2,
|
|
32
|
+
name: "SAML Provider",
|
|
33
|
+
protocol: "api.saml.provider",
|
|
34
|
+
supportsDebug: false,
|
|
35
|
+
supportsPassword: false,
|
|
36
|
+
identifierLabel: "Username",
|
|
37
|
+
passwordLabel: "Password",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const MOCK_DEBUG_RESULTS: PatronDebugResponse = {
|
|
43
|
+
results: [
|
|
44
|
+
{ label: "Server-Side Validation", success: true, details: "ok" },
|
|
45
|
+
{
|
|
46
|
+
label: "SIP2 Connection",
|
|
47
|
+
success: true,
|
|
48
|
+
details: "sip.example.com:6001",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: "Password Validation",
|
|
52
|
+
success: false,
|
|
53
|
+
details: "valid_patron_password=N",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("DebugAuthentication", () => {
|
|
59
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
60
|
+
const queryClient = new QueryClient({
|
|
61
|
+
defaultOptions: {
|
|
62
|
+
queries: {
|
|
63
|
+
retry: false,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
logger: {
|
|
67
|
+
log: console.log,
|
|
68
|
+
warn: console.warn,
|
|
69
|
+
error: process.env.NODE_ENV === "test" ? () => {} : console.error,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
/* eslint-enable @typescript-eslint/no-empty-function */
|
|
73
|
+
|
|
74
|
+
const server = setupServer(
|
|
75
|
+
http.get(AUTH_METHODS_PATH, () =>
|
|
76
|
+
HttpResponse.json(MOCK_AUTH_METHODS, { status: 200 })
|
|
77
|
+
),
|
|
78
|
+
http.post(DEBUG_AUTH_PATH, () =>
|
|
79
|
+
HttpResponse.json(MOCK_DEBUG_RESULTS, { status: 200 })
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
beforeAll(() => server.listen());
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
server.resetHandlers();
|
|
86
|
+
queryClient.clear();
|
|
87
|
+
});
|
|
88
|
+
afterAll(() => server.close());
|
|
89
|
+
|
|
90
|
+
const renderComponent = () => {
|
|
91
|
+
return renderWithProviders(
|
|
92
|
+
<DebugAuthentication library={LIBRARY} csrfToken="test-csrf-token" />,
|
|
93
|
+
{ queryClient }
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
it("displays loading state initially", () => {
|
|
98
|
+
const { getByText } = renderComponent();
|
|
99
|
+
expect(getByText("Loading authentication methods...")).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("renders auth method dropdown after loading", async () => {
|
|
103
|
+
const { getByLabelText, getByText } = renderComponent();
|
|
104
|
+
await waitFor(() => {
|
|
105
|
+
expect(getByLabelText("Authentication Method")).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
expect(getByText("Test SIP2 (api.sip)")).toBeInTheDocument();
|
|
108
|
+
expect(getByText("SAML Provider (api.saml.provider)")).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("shows 'not supported' message for non-debug methods", async () => {
|
|
112
|
+
const { getByLabelText, getByText } = renderComponent();
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(getByLabelText("Authentication Method")).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
fireEvent.change(getByLabelText("Authentication Method"), {
|
|
118
|
+
target: { value: "2" },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(
|
|
122
|
+
getByText(
|
|
123
|
+
"Debug authentication is not supported for this authentication method."
|
|
124
|
+
)
|
|
125
|
+
).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("shows form fields for debug-capable method", async () => {
|
|
129
|
+
const { getByLabelText, getByText } = renderComponent();
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(getByLabelText("Authentication Method")).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
fireEvent.change(getByLabelText("Authentication Method"), {
|
|
135
|
+
target: { value: "1" },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Should show fields with the method's labels
|
|
139
|
+
expect(getByLabelText("Barcode")).toBeInTheDocument();
|
|
140
|
+
expect(getByLabelText("PIN")).toBeInTheDocument();
|
|
141
|
+
expect(getByText("Run Debug")).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("runs debug and displays results", async () => {
|
|
145
|
+
const { getByLabelText, getByText } = renderComponent();
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
expect(getByLabelText("Authentication Method")).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Select the SIP2 method
|
|
151
|
+
fireEvent.change(getByLabelText("Authentication Method"), {
|
|
152
|
+
target: { value: "1" },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Fill in the form
|
|
156
|
+
fireEvent.change(getByLabelText("Barcode"), {
|
|
157
|
+
target: { value: "12345" },
|
|
158
|
+
});
|
|
159
|
+
fireEvent.change(getByLabelText("PIN"), {
|
|
160
|
+
target: { value: "1111" },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Submit
|
|
164
|
+
fireEvent.click(getByText("Run Debug"));
|
|
165
|
+
|
|
166
|
+
// Wait for results
|
|
167
|
+
await waitFor(() => {
|
|
168
|
+
expect(getByText("Server-Side Validation")).toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
expect(getByText("SIP2 Connection")).toBeInTheDocument();
|
|
171
|
+
expect(getByText("Password Validation")).toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("auto-selects and hides dropdown when there is only one method", async () => {
|
|
175
|
+
const singleMethod: AuthMethodsResponse = {
|
|
176
|
+
authMethods: [MOCK_AUTH_METHODS.authMethods[0]],
|
|
177
|
+
};
|
|
178
|
+
server.use(
|
|
179
|
+
http.get(AUTH_METHODS_PATH, () =>
|
|
180
|
+
HttpResponse.json(singleMethod, { status: 200 })
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const { getByLabelText, queryByLabelText } = renderComponent();
|
|
185
|
+
|
|
186
|
+
// The form fields should appear automatically without selecting a method.
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(getByLabelText("Barcode")).toBeInTheDocument();
|
|
189
|
+
});
|
|
190
|
+
expect(getByLabelText("PIN")).toBeInTheDocument();
|
|
191
|
+
|
|
192
|
+
// The dropdown should not be rendered.
|
|
193
|
+
expect(queryByLabelText("Authentication Method")).not.toBeInTheDocument();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("reconciles selected method when switching to a different library", async () => {
|
|
197
|
+
const singleMethodForSecondLibrary: AuthMethodsResponse = {
|
|
198
|
+
authMethods: [
|
|
199
|
+
{
|
|
200
|
+
id: 99,
|
|
201
|
+
name: "Second Library API",
|
|
202
|
+
protocol: "api.second",
|
|
203
|
+
supportsDebug: true,
|
|
204
|
+
supportsPassword: false,
|
|
205
|
+
identifierLabel: "Email",
|
|
206
|
+
passwordLabel: "Password",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
server.use(
|
|
212
|
+
http.get(SECOND_AUTH_METHODS_PATH, () =>
|
|
213
|
+
HttpResponse.json(singleMethodForSecondLibrary, { status: 200 })
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const {
|
|
218
|
+
getByLabelText,
|
|
219
|
+
queryByLabelText,
|
|
220
|
+
rerender,
|
|
221
|
+
} = renderWithProviders(
|
|
222
|
+
<DebugAuthentication library={LIBRARY} csrfToken="test-csrf-token" />,
|
|
223
|
+
{ queryClient }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await waitFor(() => {
|
|
227
|
+
expect(getByLabelText("Authentication Method")).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
fireEvent.change(getByLabelText("Authentication Method"), {
|
|
231
|
+
target: { value: "1" },
|
|
232
|
+
});
|
|
233
|
+
expect(getByLabelText("Barcode")).toBeInTheDocument();
|
|
234
|
+
|
|
235
|
+
rerender(
|
|
236
|
+
<DebugAuthentication
|
|
237
|
+
library={SECOND_LIBRARY}
|
|
238
|
+
csrfToken="test-csrf-token"
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
await waitFor(() => {
|
|
243
|
+
expect(getByLabelText("Email")).toBeInTheDocument();
|
|
244
|
+
});
|
|
245
|
+
expect(queryByLabelText("Authentication Method")).not.toBeInTheDocument();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("shows warning when library has no auth methods", async () => {
|
|
249
|
+
server.use(
|
|
250
|
+
http.get(AUTH_METHODS_PATH, () =>
|
|
251
|
+
HttpResponse.json({ authMethods: [] }, { status: 200 })
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const { getByText, queryByLabelText } = renderComponent();
|
|
256
|
+
await waitFor(() => {
|
|
257
|
+
expect(
|
|
258
|
+
getByText(
|
|
259
|
+
"This library has no patron authentication integrations configured."
|
|
260
|
+
)
|
|
261
|
+
).toBeInTheDocument();
|
|
262
|
+
});
|
|
263
|
+
expect(queryByLabelText("Authentication Method")).not.toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("handles API error on fetching auth methods", async () => {
|
|
267
|
+
server.use(
|
|
268
|
+
http.get(AUTH_METHODS_PATH, () => new HttpResponse(null, { status: 500 }))
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const { getByText } = renderComponent();
|
|
272
|
+
await waitFor(
|
|
273
|
+
() => {
|
|
274
|
+
expect(
|
|
275
|
+
getByText(/Error loading authentication methods/)
|
|
276
|
+
).toBeInTheDocument();
|
|
277
|
+
},
|
|
278
|
+
{ timeout: 5000 }
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import DebugResultListItem from "../../../src/components/DebugResultListItem";
|
|
4
|
+
import { PatronDebugResult } from "../../../src/api/patronDebug";
|
|
5
|
+
|
|
6
|
+
describe("DebugResultListItem", () => {
|
|
7
|
+
const renderResult = (result: PatronDebugResult, sequence = 0) => {
|
|
8
|
+
return render(<DebugResultListItem result={result} sequence={sequence} />);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
it("renders success result with string details", () => {
|
|
12
|
+
const result: PatronDebugResult = {
|
|
13
|
+
label: "SIP2 Connection",
|
|
14
|
+
success: true,
|
|
15
|
+
details: "Connected to server:6001",
|
|
16
|
+
};
|
|
17
|
+
const { getByText } = renderResult(result);
|
|
18
|
+
expect(getByText("SIP2 Connection")).toBeInTheDocument();
|
|
19
|
+
expect(getByText("passed")).toBeInTheDocument();
|
|
20
|
+
expect(getByText("Connected to server:6001")).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders failure result with string details", () => {
|
|
24
|
+
const result: PatronDebugResult = {
|
|
25
|
+
label: "Password Validation",
|
|
26
|
+
success: false,
|
|
27
|
+
details: "Password does not match",
|
|
28
|
+
};
|
|
29
|
+
const { getByText, container } = renderResult(result);
|
|
30
|
+
expect(getByText("Password Validation")).toBeInTheDocument();
|
|
31
|
+
expect(getByText("failed")).toBeInTheDocument();
|
|
32
|
+
const li = container.querySelector("li");
|
|
33
|
+
expect(li).toHaveClass("failure");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("renders success result with CSS class", () => {
|
|
37
|
+
const result: PatronDebugResult = {
|
|
38
|
+
label: "Step 1",
|
|
39
|
+
success: true,
|
|
40
|
+
details: null,
|
|
41
|
+
};
|
|
42
|
+
const { container } = renderResult(result);
|
|
43
|
+
const li = container.querySelector("li");
|
|
44
|
+
expect(li).toHaveClass("success");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders null details without detail section", () => {
|
|
48
|
+
const result: PatronDebugResult = {
|
|
49
|
+
label: "Step 1",
|
|
50
|
+
success: true,
|
|
51
|
+
details: null,
|
|
52
|
+
};
|
|
53
|
+
const { container } = renderResult(result);
|
|
54
|
+
expect(container.querySelector(".debug-result-detail")).toBeNull();
|
|
55
|
+
expect(container.querySelector(".debug-result-table")).toBeNull();
|
|
56
|
+
expect(container.querySelector(".debug-result-list")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders list details as ordered list in panel", () => {
|
|
60
|
+
const result: PatronDebugResult = {
|
|
61
|
+
label: "Multi Result",
|
|
62
|
+
success: true,
|
|
63
|
+
details: ["item one", "item two", "item three"],
|
|
64
|
+
};
|
|
65
|
+
const { getByText, container } = renderResult(result);
|
|
66
|
+
expect(getByText("item one")).toBeInTheDocument();
|
|
67
|
+
expect(getByText("item two")).toBeInTheDocument();
|
|
68
|
+
expect(getByText("item three")).toBeInTheDocument();
|
|
69
|
+
const ol = container.querySelector(".debug-result-list");
|
|
70
|
+
expect(ol).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("renders dict details as key-value table in panel", () => {
|
|
74
|
+
const result: PatronDebugResult = {
|
|
75
|
+
label: "Patron Data",
|
|
76
|
+
success: true,
|
|
77
|
+
details: {
|
|
78
|
+
username: "jdoe",
|
|
79
|
+
barcode: "12345",
|
|
80
|
+
fines: "0.00",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const { getByText, container } = renderResult(result);
|
|
84
|
+
expect(getByText("username")).toBeInTheDocument();
|
|
85
|
+
expect(getByText("jdoe")).toBeInTheDocument();
|
|
86
|
+
expect(getByText("barcode")).toBeInTheDocument();
|
|
87
|
+
expect(getByText("12345")).toBeInTheDocument();
|
|
88
|
+
const table = container.querySelector(".debug-result-table");
|
|
89
|
+
expect(table).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders dict details with nested object values as JSON", () => {
|
|
93
|
+
const result: PatronDebugResult = {
|
|
94
|
+
label: "SirsiDynix Data",
|
|
95
|
+
success: true,
|
|
96
|
+
details: {
|
|
97
|
+
patronType: { key: "testtype", label: "Test Type" },
|
|
98
|
+
name: "Jane Doe",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const { getByText, container } = renderResult(result);
|
|
102
|
+
expect(getByText("patronType")).toBeInTheDocument();
|
|
103
|
+
// Nested object should render as pretty-printed JSON inside a <code> tag
|
|
104
|
+
const codeEl = container.querySelector(
|
|
105
|
+
"td.debug-result-value code"
|
|
106
|
+
) as HTMLElement;
|
|
107
|
+
expect(codeEl).toBeInTheDocument();
|
|
108
|
+
expect(codeEl.textContent).toBe(
|
|
109
|
+
JSON.stringify({ key: "testtype", label: "Test Type" }, null, 2)
|
|
110
|
+
);
|
|
111
|
+
expect(getByText("name")).toBeInTheDocument();
|
|
112
|
+
expect(getByText("Jane Doe")).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders top-level number details as text content", () => {
|
|
116
|
+
const result: PatronDebugResult = {
|
|
117
|
+
label: "Retry Count",
|
|
118
|
+
success: true,
|
|
119
|
+
details: 3,
|
|
120
|
+
};
|
|
121
|
+
const { getByText, container } = renderResult(result);
|
|
122
|
+
expect(getByText("Retry Count")).toBeInTheDocument();
|
|
123
|
+
expect(getByText("3")).toBeInTheDocument();
|
|
124
|
+
expect(container.querySelector(".debug-result-table")).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("renders top-level boolean details as text content", () => {
|
|
128
|
+
const result: PatronDebugResult = {
|
|
129
|
+
label: "Eligibility",
|
|
130
|
+
success: false,
|
|
131
|
+
details: false,
|
|
132
|
+
};
|
|
133
|
+
const { getByText, container } = renderResult(result);
|
|
134
|
+
expect(getByText("Eligibility")).toBeInTheDocument();
|
|
135
|
+
expect(getByText("false")).toBeInTheDocument();
|
|
136
|
+
expect(container.querySelector(".debug-result-table")).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("renders dict details with mixed value types (number, boolean, null)", () => {
|
|
140
|
+
const result: PatronDebugResult = {
|
|
141
|
+
label: "Mixed Data",
|
|
142
|
+
success: true,
|
|
143
|
+
details: {
|
|
144
|
+
username: "jdoe",
|
|
145
|
+
fines: 2.5,
|
|
146
|
+
active: true,
|
|
147
|
+
block_reason: null,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const { getByText, container } = renderResult(result);
|
|
151
|
+
expect(getByText("username")).toBeInTheDocument();
|
|
152
|
+
expect(getByText("jdoe")).toBeInTheDocument();
|
|
153
|
+
expect(getByText("fines")).toBeInTheDocument();
|
|
154
|
+
expect(getByText("2.5")).toBeInTheDocument();
|
|
155
|
+
expect(getByText("active")).toBeInTheDocument();
|
|
156
|
+
expect(getByText("true")).toBeInTheDocument();
|
|
157
|
+
expect(getByText("block_reason")).toBeInTheDocument();
|
|
158
|
+
expect(getByText("null")).toBeInTheDocument();
|
|
159
|
+
const table = container.querySelector(".debug-result-table");
|
|
160
|
+
expect(table).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
});
|