@thepalaceproject/circulation-admin 1.38.0 → 1.39.0-post.1
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/README.md +42 -0
- package/dist/circulation-admin.css +1 -1
- package/dist/circulation-admin.js +1 -1
- package/jest.config.js +1 -0
- package/package.json +4 -2
- package/scripts/syncPatronBlockingDocs.js +43 -0
- package/tests/jest/api/patronBlockingRules.test.ts +28 -6
- package/tests/jest/components/CollectionImportButton.test.tsx +145 -7
- package/tests/jest/components/Collections.test.tsx +220 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +545 -0
- package/tests/jest/components/EditableConfigList.test.tsx +399 -0
- package/tests/jest/components/IndividualAdmins.test.tsx +390 -0
- package/tests/jest/components/PatronAuthServiceEditForm.test.tsx +39 -16
- package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +234 -46
- package/tests/jest/components/PatronBlockingRulesHelpModal.test.tsx +148 -0
- package/webpack.common.js +4 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { fireEvent } from "@testing-library/react";
|
|
3
|
+
import { IndividualAdmins } from "../../../src/components/IndividualAdmins";
|
|
4
|
+
import renderWithContext from "../testUtils/renderWithContext";
|
|
5
|
+
import {
|
|
6
|
+
ConfigurationSettings,
|
|
7
|
+
IndividualAdminsData,
|
|
8
|
+
} from "../../../src/interfaces";
|
|
9
|
+
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
|
|
10
|
+
|
|
11
|
+
// NB: This adds tests to the already existing tests in:
|
|
12
|
+
// - `src/components/__tests__/IndividualAdmins-test.tsx`.
|
|
13
|
+
//
|
|
14
|
+
// Those tests should eventually be migrated here and
|
|
15
|
+
// adapted to the Jest/React Testing Library paradigm.
|
|
16
|
+
|
|
17
|
+
describe("IndividualAdmins - role association disclosure", () => {
|
|
18
|
+
// ── Shared fixtures ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const allLibraries = [
|
|
21
|
+
{ short_name: "gamma", name: "Gamma Library", uuid: "uuid-gamma" },
|
|
22
|
+
{ short_name: "alpha", name: "Alpha Library", uuid: "uuid-alpha" },
|
|
23
|
+
{ short_name: "beta", name: "Beta Library", uuid: "uuid-beta" },
|
|
24
|
+
{ short_name: "delta", name: "Delta Library" }, // no uuid
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const sysAdminConfig: Partial<ConfigurationSettings> = {
|
|
28
|
+
csrfToken: "",
|
|
29
|
+
featureFlags: defaultFeatureFlags,
|
|
30
|
+
roles: [{ role: "system" }],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const renderAdmins = (
|
|
34
|
+
admins: IndividualAdminsData["individualAdmins"],
|
|
35
|
+
config = sysAdminConfig
|
|
36
|
+
) =>
|
|
37
|
+
renderWithContext(
|
|
38
|
+
<IndividualAdmins
|
|
39
|
+
data={{ individualAdmins: admins, allLibraries }}
|
|
40
|
+
fetchData={jest.fn()}
|
|
41
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
42
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
43
|
+
csrfToken="token"
|
|
44
|
+
isFetching={false}
|
|
45
|
+
/>,
|
|
46
|
+
config
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// ── Toggle visibility ─────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
it("shows no toggle for an admin with no roles field", () => {
|
|
52
|
+
const { container } = renderAdmins([{ email: "noroles@example.com" }]);
|
|
53
|
+
expect(container.querySelector(".association-toggle")).toBeNull();
|
|
54
|
+
expect(container.querySelector(".library-count")).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("shows 'no roles' and a disabled toggle for an admin with an empty roles array", () => {
|
|
58
|
+
const { container } = renderAdmins([
|
|
59
|
+
{ email: "empty@example.com", roles: [] },
|
|
60
|
+
]);
|
|
61
|
+
const toggle = container.querySelector<HTMLButtonElement>(
|
|
62
|
+
".association-toggle"
|
|
63
|
+
);
|
|
64
|
+
expect(toggle).not.toBeNull();
|
|
65
|
+
expect(toggle.disabled).toBe(true);
|
|
66
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
67
|
+
" (no roles)"
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("shows '1 role' for a system admin", () => {
|
|
72
|
+
const { container } = renderAdmins([
|
|
73
|
+
{ email: "sys@example.com", roles: [{ role: "system" }] },
|
|
74
|
+
]);
|
|
75
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
76
|
+
" (1 role)"
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("shows 'N roles' for an admin with multiple role entries", () => {
|
|
81
|
+
const { container } = renderAdmins([
|
|
82
|
+
{
|
|
83
|
+
email: "mgr@example.com",
|
|
84
|
+
roles: [
|
|
85
|
+
{ role: "manager", library: "alpha" },
|
|
86
|
+
{ role: "manager", library: "beta" },
|
|
87
|
+
{ role: "librarian", library: "gamma" },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
92
|
+
" (3 roles)"
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("counts deduplicated display entries, not raw roles, when a library has both manager and librarian roles", () => {
|
|
97
|
+
// Two raw roles for the same library collapse to one display entry (highest wins).
|
|
98
|
+
// The count shown to the user should reflect what is displayed, not item.roles.length.
|
|
99
|
+
const { container } = renderAdmins([
|
|
100
|
+
{
|
|
101
|
+
email: "mgr@example.com",
|
|
102
|
+
roles: [
|
|
103
|
+
{ role: "librarian", library: "alpha" },
|
|
104
|
+
{ role: "manager", library: "alpha" },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
109
|
+
" (1 role)"
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ── System admin ──────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
it("shows only 'sysadmin' in the expanded list for a system-role admin", () => {
|
|
116
|
+
const { container } = renderAdmins([
|
|
117
|
+
{ email: "sys@example.com", roles: [{ role: "system" }] },
|
|
118
|
+
]);
|
|
119
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
120
|
+
|
|
121
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
122
|
+
expect(items).toHaveLength(1);
|
|
123
|
+
expect(items[0].textContent).toBe("sysadmin");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("shows only 'sysadmin' even when other roles are also present", () => {
|
|
127
|
+
const { container } = renderAdmins([
|
|
128
|
+
{
|
|
129
|
+
email: "sys@example.com",
|
|
130
|
+
roles: [{ role: "system" }, { role: "manager", library: "alpha" }],
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
134
|
+
|
|
135
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
136
|
+
expect(items).toHaveLength(1);
|
|
137
|
+
expect(items[0].textContent).toBe("sysadmin");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── Library-specific roles ────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
it("shows '<library> - Manager' for a library manager role", () => {
|
|
143
|
+
const { container } = renderAdmins([
|
|
144
|
+
{
|
|
145
|
+
email: "mgr@example.com",
|
|
146
|
+
roles: [{ role: "manager", library: "alpha" }],
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
150
|
+
|
|
151
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
152
|
+
expect(items).toHaveLength(1);
|
|
153
|
+
expect(items[0].textContent).toBe("Alpha Library - Manager");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("shows '<library> - Librarian' for a librarian role", () => {
|
|
157
|
+
const { container } = renderAdmins([
|
|
158
|
+
{
|
|
159
|
+
email: "lib@example.com",
|
|
160
|
+
roles: [{ role: "librarian", library: "beta" }],
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
164
|
+
|
|
165
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
166
|
+
expect(items).toHaveLength(1);
|
|
167
|
+
expect(items[0].textContent).toBe("Beta Library - Librarian");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("shows the highest role (Manager) when a library has both manager and librarian roles", () => {
|
|
171
|
+
const { container } = renderAdmins([
|
|
172
|
+
{
|
|
173
|
+
email: "mgr@example.com",
|
|
174
|
+
roles: [
|
|
175
|
+
{ role: "librarian", library: "alpha" },
|
|
176
|
+
{ role: "manager", library: "alpha" },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
181
|
+
|
|
182
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
183
|
+
expect(items).toHaveLength(1);
|
|
184
|
+
expect(items[0].textContent).toBe("Alpha Library - Manager");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── Sitewide roles ────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
it("shows 'All libraries - Manager' for a manager-all role", () => {
|
|
190
|
+
const { container } = renderAdmins([
|
|
191
|
+
{ email: "mgr@example.com", roles: [{ role: "manager-all" }] },
|
|
192
|
+
]);
|
|
193
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
194
|
+
|
|
195
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
196
|
+
expect(items[0].textContent).toBe("All libraries - Manager");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("shows 'All libraries - Librarian' for a librarian-all role", () => {
|
|
200
|
+
const { container } = renderAdmins([
|
|
201
|
+
{ email: "lib@example.com", roles: [{ role: "librarian-all" }] },
|
|
202
|
+
]);
|
|
203
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
204
|
+
|
|
205
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
206
|
+
expect(items[0].textContent).toBe("All libraries - Librarian");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("shows Manager (not Librarian) when both manager-all and librarian-all are present", () => {
|
|
210
|
+
const { container } = renderAdmins([
|
|
211
|
+
{
|
|
212
|
+
email: "mgr@example.com",
|
|
213
|
+
roles: [{ role: "librarian-all" }, { role: "manager-all" }],
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
217
|
+
|
|
218
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
219
|
+
// Only the manager-all entry; librarian-all is superseded.
|
|
220
|
+
expect(Array.from(items).map((li) => li.textContent)).toEqual([
|
|
221
|
+
"All libraries - Manager",
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ── Combined sitewide + library-specific roles ────────────────────────────
|
|
226
|
+
|
|
227
|
+
it("shows both 'All libraries' and per-library entries when manager-all and library roles coexist", () => {
|
|
228
|
+
const { container } = renderAdmins([
|
|
229
|
+
{
|
|
230
|
+
email: "mgr@example.com",
|
|
231
|
+
roles: [
|
|
232
|
+
{ role: "manager-all" },
|
|
233
|
+
{ role: "manager", library: "alpha" },
|
|
234
|
+
{ role: "librarian", library: "beta" },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
]);
|
|
238
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
239
|
+
|
|
240
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
241
|
+
const texts = Array.from(items).map((li) => li.textContent);
|
|
242
|
+
expect(texts).toContain("All libraries - Manager");
|
|
243
|
+
expect(texts).toContain("Alpha Library - Manager");
|
|
244
|
+
expect(texts).toContain("Beta Library - Librarian");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("shows both 'All libraries' and per-library entries when librarian-all and library roles coexist", () => {
|
|
248
|
+
const { container } = renderAdmins([
|
|
249
|
+
{
|
|
250
|
+
email: "lib@example.com",
|
|
251
|
+
roles: [
|
|
252
|
+
{ role: "librarian-all" },
|
|
253
|
+
{ role: "librarian", library: "gamma" },
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
258
|
+
|
|
259
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
260
|
+
const texts = Array.from(items).map((li) => li.textContent);
|
|
261
|
+
expect(texts).toContain("All libraries - Librarian");
|
|
262
|
+
expect(texts).toContain("Gamma Library - Librarian");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ── Sorting ───────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
it("sorts library role entries alphabetically by library display name", () => {
|
|
268
|
+
const { container } = renderAdmins([
|
|
269
|
+
{
|
|
270
|
+
email: "mgr@example.com",
|
|
271
|
+
roles: [
|
|
272
|
+
{ role: "manager", library: "gamma" },
|
|
273
|
+
{ role: "manager", library: "alpha" },
|
|
274
|
+
{ role: "librarian", library: "beta" },
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
]);
|
|
278
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
279
|
+
|
|
280
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
281
|
+
expect(items[0].textContent).toBe("Alpha Library - Manager");
|
|
282
|
+
expect(items[1].textContent).toBe("Beta Library - Librarian");
|
|
283
|
+
expect(items[2].textContent).toBe("Gamma Library - Manager");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ── Links ─────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
it("links the library name to its config page when a uuid is available", () => {
|
|
289
|
+
const { container } = renderAdmins([
|
|
290
|
+
{
|
|
291
|
+
email: "mgr@example.com",
|
|
292
|
+
roles: [{ role: "manager", library: "alpha" }],
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
296
|
+
|
|
297
|
+
const link = container.querySelector<HTMLAnchorElement>(
|
|
298
|
+
".associated-items a"
|
|
299
|
+
);
|
|
300
|
+
expect(link).not.toBeNull();
|
|
301
|
+
expect(link.textContent).toBe("Alpha Library");
|
|
302
|
+
expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-alpha");
|
|
303
|
+
// The role suffix should not be part of the link.
|
|
304
|
+
expect(link.nextSibling.textContent).toBe(" - Manager");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("renders the library name as plain text when no uuid is available", () => {
|
|
308
|
+
const { container } = renderAdmins([
|
|
309
|
+
{
|
|
310
|
+
email: "lib@example.com",
|
|
311
|
+
roles: [{ role: "librarian", library: "delta" }],
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
315
|
+
|
|
316
|
+
expect(container.querySelector(".associated-items a")).toBeNull();
|
|
317
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
318
|
+
"Delta Library - Librarian"
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ── Expand all / Collapse all buttons ────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
it("Expand all expands all admins that have roles", () => {
|
|
325
|
+
const { container } = renderAdmins([
|
|
326
|
+
{ email: "sys@example.com", roles: [{ role: "system" }] },
|
|
327
|
+
{
|
|
328
|
+
email: "mgr@example.com",
|
|
329
|
+
roles: [{ role: "manager", library: "alpha" }],
|
|
330
|
+
},
|
|
331
|
+
{ email: "none@example.com" }, // no roles field → no toggle
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
fireEvent.click(container.querySelector(".expand-all"));
|
|
335
|
+
|
|
336
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("Collapse all collapses all expanded admins", () => {
|
|
340
|
+
const { container } = renderAdmins([
|
|
341
|
+
{ email: "sys@example.com", roles: [{ role: "system" }] },
|
|
342
|
+
{
|
|
343
|
+
email: "mgr@example.com",
|
|
344
|
+
roles: [{ role: "manager", library: "alpha" }],
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
|
|
348
|
+
fireEvent.click(container.querySelector(".expand-all"));
|
|
349
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
|
|
350
|
+
|
|
351
|
+
fireEvent.click(container.querySelector(".collapse-all"));
|
|
352
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ── Alt+click toggle-all ──────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
it("alt+click expands all admins that have roles", () => {
|
|
358
|
+
const { container } = renderAdmins([
|
|
359
|
+
{ email: "sys@example.com", roles: [{ role: "system" }] },
|
|
360
|
+
{
|
|
361
|
+
email: "mgr@example.com",
|
|
362
|
+
roles: [{ role: "manager", library: "alpha" }],
|
|
363
|
+
},
|
|
364
|
+
{ email: "none@example.com" }, // no roles field → no toggle
|
|
365
|
+
]);
|
|
366
|
+
const toggles = container.querySelectorAll(".association-toggle");
|
|
367
|
+
expect(toggles).toHaveLength(2);
|
|
368
|
+
|
|
369
|
+
fireEvent.click(toggles[0], { altKey: true });
|
|
370
|
+
|
|
371
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("alt+click collapses all when all are already expanded", () => {
|
|
375
|
+
const { container } = renderAdmins([
|
|
376
|
+
{ email: "sys@example.com", roles: [{ role: "system" }] },
|
|
377
|
+
{
|
|
378
|
+
email: "mgr@example.com",
|
|
379
|
+
roles: [{ role: "manager", library: "alpha" }],
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
const toggles = container.querySelectorAll(".association-toggle");
|
|
383
|
+
|
|
384
|
+
fireEvent.click(toggles[0], { altKey: true });
|
|
385
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
|
|
386
|
+
|
|
387
|
+
fireEvent.click(toggles[0], { altKey: true });
|
|
388
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(0);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -2,6 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
import * as fetchMock from "fetch-mock-jest";
|
|
5
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
6
|
import PatronAuthServiceEditForm from "../../../src/components/PatronAuthServiceEditForm";
|
|
6
7
|
import {
|
|
7
8
|
PatronAuthServicesData,
|
|
@@ -66,6 +67,16 @@ function buildSIP2Item(rules: PatronBlockingRule[] = []) {
|
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
/** Renders with a fresh QueryClient so useAvailableFields (useQuery) works. */
|
|
71
|
+
function renderForm(element: React.ReactElement) {
|
|
72
|
+
const queryClient = new QueryClient({
|
|
73
|
+
defaultOptions: { queries: { retry: false } },
|
|
74
|
+
});
|
|
75
|
+
return render(
|
|
76
|
+
<QueryClientProvider client={queryClient}>{element}</QueryClientProvider>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
// Guard: any blur on the Rule Expression textarea calls validatePatronBlockingRuleExpression.
|
|
70
81
|
// All describe blocks get a default 200 mock so tests that incidentally trigger blur
|
|
71
82
|
// don't fail with "only absolute URLs are supported" from the fetch polyfill.
|
|
@@ -80,7 +91,9 @@ afterEach(() => {
|
|
|
80
91
|
describe("PatronAuthServiceEditForm – capability gating", () => {
|
|
81
92
|
it("shows PatronBlockingRulesEditor in expanded library settings for SIP2", async () => {
|
|
82
93
|
const user = userEvent.setup();
|
|
83
|
-
|
|
94
|
+
renderForm(
|
|
95
|
+
<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />
|
|
96
|
+
);
|
|
84
97
|
|
|
85
98
|
await expandLibrariesPanel(user);
|
|
86
99
|
const editButton = screen.getByRole("button", { name: /Edit/i });
|
|
@@ -99,7 +112,7 @@ describe("PatronAuthServiceEditForm – capability gating", () => {
|
|
|
99
112
|
settings: { idp_url: "https://idp.example.com" },
|
|
100
113
|
libraries: [{ short_name: LIBRARY_SHORT_NAME }],
|
|
101
114
|
};
|
|
102
|
-
|
|
115
|
+
renderForm(<PatronAuthServiceEditForm {...baseProps} item={item} />);
|
|
103
116
|
await expandLibrariesPanel(user);
|
|
104
117
|
|
|
105
118
|
// For non-SIP2 with no library_settings, the Edit button should not render
|
|
@@ -119,7 +132,7 @@ describe("PatronAuthServiceEditForm – load / initial value", () => {
|
|
|
119
132
|
message: "Card expired",
|
|
120
133
|
},
|
|
121
134
|
];
|
|
122
|
-
|
|
135
|
+
renderForm(
|
|
123
136
|
<PatronAuthServiceEditForm
|
|
124
137
|
{...baseProps}
|
|
125
138
|
item={buildSIP2Item(existingRules)}
|
|
@@ -144,7 +157,7 @@ describe("PatronAuthServiceEditForm – load / initial value", () => {
|
|
|
144
157
|
|
|
145
158
|
it("shows empty editor when library has no existing patron_blocking_rules", async () => {
|
|
146
159
|
const user = userEvent.setup();
|
|
147
|
-
|
|
160
|
+
renderForm(
|
|
148
161
|
<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item([])} />
|
|
149
162
|
);
|
|
150
163
|
|
|
@@ -160,7 +173,7 @@ describe("PatronAuthServiceEditForm – error display", () => {
|
|
|
160
173
|
it("passes error prop down to PatronBlockingRulesEditor", async () => {
|
|
161
174
|
const user = userEvent.setup();
|
|
162
175
|
const error = { status: 400, response: "Validation failed", url: "" };
|
|
163
|
-
|
|
176
|
+
renderForm(
|
|
164
177
|
<PatronAuthServiceEditForm
|
|
165
178
|
{...baseProps}
|
|
166
179
|
item={buildSIP2Item([{ name: "", rule: "expr", message: null }])}
|
|
@@ -180,7 +193,9 @@ describe("PatronAuthServiceEditForm – error display", () => {
|
|
|
180
193
|
describe("PatronAuthServiceEditForm – serialization", () => {
|
|
181
194
|
it("includes patron_blocking_rules in the library state when editLibrary is called", async () => {
|
|
182
195
|
const user = userEvent.setup();
|
|
183
|
-
|
|
196
|
+
renderForm(
|
|
197
|
+
<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />
|
|
198
|
+
);
|
|
184
199
|
|
|
185
200
|
await expandLibrariesPanel(user);
|
|
186
201
|
const editButton = screen.getByRole("button", { name: /Edit/i });
|
|
@@ -215,7 +230,7 @@ describe("PatronAuthServiceEditForm – serialization", () => {
|
|
|
215
230
|
settings: {},
|
|
216
231
|
libraries: [],
|
|
217
232
|
};
|
|
218
|
-
|
|
233
|
+
renderForm(<PatronAuthServiceEditForm {...baseProps} item={item} />);
|
|
219
234
|
|
|
220
235
|
await expandLibrariesPanel(user);
|
|
221
236
|
// Select the library from the dropdown
|
|
@@ -246,7 +261,9 @@ describe("PatronAuthServiceEditForm – serialization", () => {
|
|
|
246
261
|
describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
247
262
|
it("disables the per-library Save button immediately when a new rule is added", async () => {
|
|
248
263
|
const user = userEvent.setup();
|
|
249
|
-
|
|
264
|
+
renderForm(
|
|
265
|
+
<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />
|
|
266
|
+
);
|
|
250
267
|
|
|
251
268
|
await expandLibrariesPanel(user);
|
|
252
269
|
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
@@ -258,7 +275,9 @@ describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
|
258
275
|
|
|
259
276
|
it("re-enables the per-library Save button once validation succeeds", async () => {
|
|
260
277
|
const user = userEvent.setup();
|
|
261
|
-
|
|
278
|
+
renderForm(
|
|
279
|
+
<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />
|
|
280
|
+
);
|
|
262
281
|
|
|
263
282
|
await expandLibrariesPanel(user);
|
|
264
283
|
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
@@ -280,7 +299,9 @@ describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
|
280
299
|
body: { detail: "Unknown placeholder" },
|
|
281
300
|
});
|
|
282
301
|
|
|
283
|
-
|
|
302
|
+
renderForm(
|
|
303
|
+
<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />
|
|
304
|
+
);
|
|
284
305
|
|
|
285
306
|
await expandLibrariesPanel(user);
|
|
286
307
|
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
@@ -302,7 +323,7 @@ describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
|
302
323
|
settings: {},
|
|
303
324
|
libraries: [],
|
|
304
325
|
};
|
|
305
|
-
|
|
326
|
+
renderForm(<PatronAuthServiceEditForm {...baseProps} item={item} />);
|
|
306
327
|
|
|
307
328
|
await expandLibrariesPanel(user);
|
|
308
329
|
const select = screen.getByRole("combobox", { name: /Add Library/i });
|
|
@@ -324,7 +345,7 @@ describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
|
324
345
|
{ name: "Rule A", rule: "expr_a" },
|
|
325
346
|
{ name: "Rule B", rule: "expr_b" },
|
|
326
347
|
];
|
|
327
|
-
|
|
348
|
+
renderForm(
|
|
328
349
|
<PatronAuthServiceEditForm
|
|
329
350
|
{...baseProps}
|
|
330
351
|
item={buildSIP2Item(existingRules)}
|
|
@@ -354,7 +375,7 @@ describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
|
354
375
|
{ name: "Rule A", rule: "expr_a" },
|
|
355
376
|
{ name: "Rule A", rule: "expr_b" }, // starts as duplicate
|
|
356
377
|
];
|
|
357
|
-
|
|
378
|
+
renderForm(
|
|
358
379
|
<PatronAuthServiceEditForm
|
|
359
380
|
{...baseProps}
|
|
360
381
|
item={buildSIP2Item(existingRules)}
|
|
@@ -380,7 +401,7 @@ describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
|
380
401
|
it("re-blocks and then re-enables the Save button when an existing rule's expression is edited and re-validated", async () => {
|
|
381
402
|
const user = userEvent.setup();
|
|
382
403
|
const existingRules = [{ name: "Rule A", rule: "expr_a" }];
|
|
383
|
-
|
|
404
|
+
renderForm(
|
|
384
405
|
<PatronAuthServiceEditForm
|
|
385
406
|
{...baseProps}
|
|
386
407
|
item={buildSIP2Item(existingRules)}
|
|
@@ -411,7 +432,7 @@ describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
|
411
432
|
describe("PatronAuthServiceEditForm – csrfToken / serviceId wiring", () => {
|
|
412
433
|
it("passes the csrfToken from additionalData to the validation API request", async () => {
|
|
413
434
|
const user = userEvent.setup();
|
|
414
|
-
|
|
435
|
+
renderForm(
|
|
415
436
|
<PatronAuthServiceEditForm
|
|
416
437
|
{...baseProps}
|
|
417
438
|
item={buildSIP2Item()}
|
|
@@ -441,7 +462,9 @@ describe("PatronAuthServiceEditForm – csrfToken / serviceId wiring", () => {
|
|
|
441
462
|
it("passes the item id as service_id in the validation API request body", async () => {
|
|
442
463
|
const user = userEvent.setup();
|
|
443
464
|
// item.id = 1 (from buildSIP2Item)
|
|
444
|
-
|
|
465
|
+
renderForm(
|
|
466
|
+
<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />
|
|
467
|
+
);
|
|
445
468
|
|
|
446
469
|
await expandLibrariesPanel(user);
|
|
447
470
|
await user.click(screen.getByRole("button", { name: /Edit/i }));
|