@thepalaceproject/circulation-admin 1.46.0 → 1.47.0-post.12
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 +2 -2
- package/dist/circulation-admin.js +1 -1
- package/dist/circulation-admin.js.LICENSE.txt +1 -1
- package/package.json +1 -1
- package/tests/jest/components/DiscoveryServices.test.tsx +36 -0
- package/tests/jest/components/EditableConfigList.test.tsx +75 -1
- package/tests/jest/components/IndividualAdminEditForm.test.tsx +98 -0
- package/tests/jest/components/IndividualAdmins.test.tsx +92 -4
- package/tests/jest/components/LibraryRegistration.test.tsx +18 -0
- package/tests/jest/components/PatronAuthServiceEditForm.test.tsx +97 -3
- package/tests/jest/components/PatronAuthServices.test.tsx +15 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +138 -0
- package/tests/jest/components/SetupPage.test.tsx +24 -7
- package/tests/jest/reducers/libraries.test.ts +171 -0
- package/tests/jest/utils/allLibraries.test.ts +235 -0
package/package.json
CHANGED
|
@@ -52,6 +52,42 @@ describe("DiscoveryServices - registered library disclosure", () => {
|
|
|
52
52
|
|
|
53
53
|
// ── Toggle visibility ─────────────────────────────────────────────────────
|
|
54
54
|
|
|
55
|
+
it("names registration data in the failed-library-list alert", () => {
|
|
56
|
+
// This tab's entries fall back to registration records, so the generic
|
|
57
|
+
// "shown by short name only" message would be wrong here.
|
|
58
|
+
const { container } = renderServices({
|
|
59
|
+
discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
|
|
60
|
+
allLibraries: [],
|
|
61
|
+
allLibrariesError: {
|
|
62
|
+
status: 500,
|
|
63
|
+
response: "nope",
|
|
64
|
+
url: "/admin/libraries",
|
|
65
|
+
},
|
|
66
|
+
libraryRegistrations: [
|
|
67
|
+
{ id: 1, libraries: [{ short_name: "alpha", status: "success" }] },
|
|
68
|
+
] as any,
|
|
69
|
+
});
|
|
70
|
+
expect(container.querySelector(".alert-danger")).toHaveTextContent(
|
|
71
|
+
"Registered libraries are shown using registration data"
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("shows no toggle while allLibraries has not settled", () => {
|
|
76
|
+
// Labels must render once, in final linked form, not flash bare short
|
|
77
|
+
// names that get rewritten when the sitewide list arrives.
|
|
78
|
+
const { container } = renderServices({
|
|
79
|
+
discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
|
|
80
|
+
allLibraries: undefined,
|
|
81
|
+
libraryRegistrations: [
|
|
82
|
+
{
|
|
83
|
+
id: 1,
|
|
84
|
+
libraries: [{ short_name: "alpha", status: "success" }],
|
|
85
|
+
},
|
|
86
|
+
] as any,
|
|
87
|
+
});
|
|
88
|
+
expect(container.querySelector(".association-toggle")).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
55
91
|
it("shows no toggle when libraryRegistrations data has not yet loaded", () => {
|
|
56
92
|
const { container } = renderServices({
|
|
57
93
|
discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
|
|
@@ -23,6 +23,12 @@ describe("EditableConfigList - library association disclosure", () => {
|
|
|
23
23
|
interface ServicesData {
|
|
24
24
|
services: ServiceItem[];
|
|
25
25
|
allLibraries?: Array<{ short_name: string; name?: string; uuid?: string }>;
|
|
26
|
+
allLibrariesError?: { status: number; response: string; url: string };
|
|
27
|
+
allLibrariesRefreshError?: {
|
|
28
|
+
status: number;
|
|
29
|
+
response: string;
|
|
30
|
+
url: string;
|
|
31
|
+
};
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
class TestEditForm extends React.Component<
|
|
@@ -36,6 +42,7 @@ describe("EditableConfigList - library association disclosure", () => {
|
|
|
36
42
|
class TestServiceList extends EditableConfigList<ServicesData, ServiceItem> {
|
|
37
43
|
EditForm = TestEditForm;
|
|
38
44
|
listDataKey = "services";
|
|
45
|
+
usesLibraryList = true;
|
|
39
46
|
itemTypeName = "service";
|
|
40
47
|
urlBase = "/admin/services/";
|
|
41
48
|
identifierKey = "id";
|
|
@@ -377,13 +384,19 @@ describe("EditableConfigList - library association disclosure", () => {
|
|
|
377
384
|
expect(items[1].textContent).toBe("Beta Library - beta");
|
|
378
385
|
});
|
|
379
386
|
|
|
380
|
-
it("
|
|
387
|
+
it("shows an alert and bare short names when the library list failed to load", () => {
|
|
381
388
|
const { container } = renderWithContext(
|
|
382
389
|
<TestServiceList
|
|
383
390
|
data={{
|
|
384
391
|
services: [
|
|
385
392
|
{ id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] },
|
|
386
393
|
],
|
|
394
|
+
allLibraries: [],
|
|
395
|
+
allLibrariesError: {
|
|
396
|
+
status: 500,
|
|
397
|
+
response: "nope",
|
|
398
|
+
url: "/admin/libraries",
|
|
399
|
+
},
|
|
387
400
|
}}
|
|
388
401
|
fetchData={jest.fn()}
|
|
389
402
|
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
@@ -393,11 +406,72 @@ describe("EditableConfigList - library association disclosure", () => {
|
|
|
393
406
|
/>,
|
|
394
407
|
config
|
|
395
408
|
);
|
|
409
|
+
expect(container.querySelector(".alert-danger")).toHaveTextContent(
|
|
410
|
+
"The library list failed to load"
|
|
411
|
+
);
|
|
412
|
+
// The associations still render, by short name, below the alert.
|
|
396
413
|
fireEvent.click(container.querySelector(".association-toggle"));
|
|
397
414
|
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
398
415
|
"nypl"
|
|
399
416
|
);
|
|
400
417
|
});
|
|
418
|
+
|
|
419
|
+
it("shows a warning when the library list is stale", () => {
|
|
420
|
+
const { container } = renderWithContext(
|
|
421
|
+
<TestServiceList
|
|
422
|
+
data={{
|
|
423
|
+
services: [
|
|
424
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] },
|
|
425
|
+
],
|
|
426
|
+
allLibraries: [{ short_name: "nypl", name: "NYPL" }],
|
|
427
|
+
allLibrariesRefreshError: {
|
|
428
|
+
status: 500,
|
|
429
|
+
response: "nope",
|
|
430
|
+
url: "/admin/libraries",
|
|
431
|
+
},
|
|
432
|
+
}}
|
|
433
|
+
fetchData={jest.fn()}
|
|
434
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
435
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
436
|
+
csrfToken="token"
|
|
437
|
+
isFetching={false}
|
|
438
|
+
/>,
|
|
439
|
+
config
|
|
440
|
+
);
|
|
441
|
+
expect(container.querySelector(".alert-warning")).toHaveTextContent(
|
|
442
|
+
"The library list could not be refreshed"
|
|
443
|
+
);
|
|
444
|
+
// The associations still render, from the retained list.
|
|
445
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
446
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
447
|
+
"NYPL - nypl"
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("holds the association panel until allLibraries settles", () => {
|
|
452
|
+
const { container } = renderWithContext(
|
|
453
|
+
<TestServiceList
|
|
454
|
+
data={{
|
|
455
|
+
services: [
|
|
456
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] },
|
|
457
|
+
],
|
|
458
|
+
}}
|
|
459
|
+
fetchData={jest.fn()}
|
|
460
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
461
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
462
|
+
csrfToken="token"
|
|
463
|
+
isFetching={false}
|
|
464
|
+
/>,
|
|
465
|
+
config
|
|
466
|
+
);
|
|
467
|
+
// With the sitewide list unsettled, no toggle renders at all, so the
|
|
468
|
+
// panel cannot flash bare short names that get rewritten when the
|
|
469
|
+
// list arrives. The status line explains and announces the gap.
|
|
470
|
+
expect(container.querySelector(".association-toggle")).toBeNull();
|
|
471
|
+
expect(container.querySelector('[role="status"]')).toHaveTextContent(
|
|
472
|
+
"Loading libraries..."
|
|
473
|
+
);
|
|
474
|
+
});
|
|
401
475
|
});
|
|
402
476
|
});
|
|
403
477
|
|
|
@@ -202,6 +202,104 @@ describe("IndividualAdminEditForm - rendered inputs and role changes", () => {
|
|
|
202
202
|
const roleCheckbox = (role: string) =>
|
|
203
203
|
screen.getByRole("checkbox", { name: roleNames[role] });
|
|
204
204
|
|
|
205
|
+
const failureData = {
|
|
206
|
+
individualAdmins: [adminData],
|
|
207
|
+
allLibraries: [],
|
|
208
|
+
allLibrariesError: {
|
|
209
|
+
status: 500,
|
|
210
|
+
response: "nope",
|
|
211
|
+
url: "/admin/libraries",
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
it("waits for the library list before showing per-library roles", () => {
|
|
216
|
+
const { container } = renderForm({
|
|
217
|
+
item: adminData,
|
|
218
|
+
data: { individualAdmins: [adminData] },
|
|
219
|
+
});
|
|
220
|
+
expect(container.querySelector("table.library-admin-roles")).toBeNull();
|
|
221
|
+
expect(container.querySelector('[role="status"]')).toHaveTextContent(
|
|
222
|
+
"Loading libraries..."
|
|
223
|
+
);
|
|
224
|
+
// Sitewide toggles rewrite an existing admin's hidden per-library roles
|
|
225
|
+
// wholesale, so role edits are disabled until the list is available.
|
|
226
|
+
expect(roleCheckbox("system")).toBeDisabled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("explains a failed library list load when editing an existing admin", () => {
|
|
230
|
+
// On failure allLibraries settles to [] and allLibrariesError is set.
|
|
231
|
+
const { container } = renderForm({ item: adminData, data: failureData });
|
|
232
|
+
expect(
|
|
233
|
+
screen.getByText(
|
|
234
|
+
/library roles cannot be shown, and no roles can be changed/
|
|
235
|
+
)
|
|
236
|
+
).toBeInTheDocument();
|
|
237
|
+
// The role="alert" danger Alert announces the failure itself, so the
|
|
238
|
+
// status region empties rather than duplicating the announcement.
|
|
239
|
+
expect(container.querySelector('[role="status"]')).toBeEmptyDOMElement();
|
|
240
|
+
expect(roleCheckbox("system")).toBeDisabled();
|
|
241
|
+
// All role edits are disabled, so the empty table shell is dropped.
|
|
242
|
+
expect(container.querySelector("table.library-admin-roles")).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("still allows sitewide roles for a new admin when the library list failed", () => {
|
|
246
|
+
// A new admin has no hidden per-library roles to clobber.
|
|
247
|
+
const { container } = renderForm({ data: failureData });
|
|
248
|
+
expect(
|
|
249
|
+
screen.getByText(/Sitewide roles can still be assigned/)
|
|
250
|
+
).toBeInTheDocument();
|
|
251
|
+
expect(roleCheckbox("system")).toBeEnabled();
|
|
252
|
+
expect(roleCheckbox("manager-all")).toBeEnabled();
|
|
253
|
+
// No misleading "no libraries" row under a failure.
|
|
254
|
+
expect(container.querySelector("tbody")).toBeEmptyDOMElement();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const staleData = {
|
|
258
|
+
individualAdmins: [adminData],
|
|
259
|
+
allLibraries,
|
|
260
|
+
allLibrariesRefreshError: {
|
|
261
|
+
status: 500,
|
|
262
|
+
response: "nope",
|
|
263
|
+
url: "/admin/libraries",
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
it("locks only per-library roles for an existing admin while the list is stale", () => {
|
|
268
|
+
const { container } = renderForm({ item: adminData, data: staleData });
|
|
269
|
+
expect(container.querySelector(".alert-warning")).toHaveTextContent(
|
|
270
|
+
"Per-library roles cannot be changed until the list can be refreshed."
|
|
271
|
+
);
|
|
272
|
+
// The role="alert" warning announces the refresh failure itself, so
|
|
273
|
+
// the status region empties rather than duplicating the announcement.
|
|
274
|
+
expect(container.querySelector('[role="status"]')).toBeEmptyDOMElement();
|
|
275
|
+
// Only the per-library un-check branches expand sitewide roles from
|
|
276
|
+
// the (stale) list; sitewide toggles are list-independent and stay
|
|
277
|
+
// assignable, so submitting cannot be forced into a roleless admin.
|
|
278
|
+
expect(roleCheckbox("system")).toBeEnabled();
|
|
279
|
+
expect(roleCheckbox("manager-all")).toBeEnabled();
|
|
280
|
+
expect(roleCheckbox("manager-nypl")).toBeDisabled();
|
|
281
|
+
expect(roleCheckbox("librarian-nypl")).toBeDisabled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("locks only per-library roles for a new admin while the list is stale", () => {
|
|
285
|
+
const { container } = renderForm({ data: staleData });
|
|
286
|
+
expect(container.querySelector(".alert-warning")).toHaveTextContent(
|
|
287
|
+
"Per-library roles cannot be changed"
|
|
288
|
+
);
|
|
289
|
+
expect(roleCheckbox("system")).toBeEnabled();
|
|
290
|
+
expect(roleCheckbox("manager-all")).toBeEnabled();
|
|
291
|
+
expect(roleCheckbox("manager-nypl")).toBeDisabled();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("says when no libraries are configured", () => {
|
|
295
|
+
renderForm({
|
|
296
|
+
data: { individualAdmins: [adminData], allLibraries: [] },
|
|
297
|
+
});
|
|
298
|
+
expect(
|
|
299
|
+
screen.getByText("No libraries are configured.")
|
|
300
|
+
).toBeInTheDocument();
|
|
301
|
+
});
|
|
302
|
+
|
|
205
303
|
const expectRoles = (expected: string[]) => {
|
|
206
304
|
for (const role of allRoles) {
|
|
207
305
|
if (expected.includes(role)) {
|
|
@@ -46,6 +46,84 @@ describe("IndividualAdmins - role association disclosure", () => {
|
|
|
46
46
|
|
|
47
47
|
// ── Toggle visibility ─────────────────────────────────────────────────────
|
|
48
48
|
|
|
49
|
+
it("shows no toggle while allLibraries has not settled", () => {
|
|
50
|
+
// Labels must render once, in final linked form, not flash bare short
|
|
51
|
+
// names that get rewritten when the sitewide list arrives.
|
|
52
|
+
const { container } = renderWithContext(
|
|
53
|
+
<IndividualAdmins
|
|
54
|
+
data={{
|
|
55
|
+
individualAdmins: [
|
|
56
|
+
{
|
|
57
|
+
email: "admin@example.org",
|
|
58
|
+
roles: [{ role: "manager", library: "alpha" }],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
}}
|
|
62
|
+
fetchData={jest.fn()}
|
|
63
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
64
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
65
|
+
csrfToken="token"
|
|
66
|
+
isFetching={false}
|
|
67
|
+
/>,
|
|
68
|
+
sysAdminConfig
|
|
69
|
+
);
|
|
70
|
+
expect(container.querySelector(".association-toggle")).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("shows the sysadmin entry before allLibraries settles", () => {
|
|
74
|
+
// The synthetic sysadmin entry never consults the library list and can
|
|
75
|
+
// never be rewritten by it, so it is not held back.
|
|
76
|
+
const { container } = renderWithContext(
|
|
77
|
+
<IndividualAdmins
|
|
78
|
+
data={{
|
|
79
|
+
individualAdmins: [
|
|
80
|
+
{ email: "root@example.org", roles: [{ role: "system" }] },
|
|
81
|
+
],
|
|
82
|
+
}}
|
|
83
|
+
fetchData={jest.fn()}
|
|
84
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
85
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
86
|
+
csrfToken="token"
|
|
87
|
+
isFetching={false}
|
|
88
|
+
/>,
|
|
89
|
+
sysAdminConfig
|
|
90
|
+
);
|
|
91
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
92
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
93
|
+
"sysadmin"
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("names roles, not libraries, when the library list failed to load", () => {
|
|
98
|
+
const { container } = renderWithContext(
|
|
99
|
+
<IndividualAdmins
|
|
100
|
+
data={{
|
|
101
|
+
individualAdmins: [
|
|
102
|
+
{
|
|
103
|
+
email: "admin@example.org",
|
|
104
|
+
roles: [{ role: "manager", library: "alpha" }],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
allLibraries: [],
|
|
108
|
+
allLibrariesError: {
|
|
109
|
+
status: 500,
|
|
110
|
+
response: "nope",
|
|
111
|
+
url: "/admin/libraries",
|
|
112
|
+
},
|
|
113
|
+
}}
|
|
114
|
+
fetchData={jest.fn()}
|
|
115
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
116
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
117
|
+
csrfToken="token"
|
|
118
|
+
isFetching={false}
|
|
119
|
+
/>,
|
|
120
|
+
sysAdminConfig
|
|
121
|
+
);
|
|
122
|
+
expect(container.querySelector(".alert-danger")).toHaveTextContent(
|
|
123
|
+
"Roles are shown by library short name only."
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
49
127
|
it("shows no toggle for an admin with no roles field", () => {
|
|
50
128
|
const { container } = renderAdmins([{ email: "noroles@example.com" }]);
|
|
51
129
|
expect(container.querySelector(".association-toggle")).toBeNull();
|
|
@@ -498,10 +576,13 @@ describe("IndividualAdmins - connect wiring", () => {
|
|
|
498
576
|
afterEach(() => jest.restoreAllMocks());
|
|
499
577
|
|
|
500
578
|
it("renders the connected default export, fetching on mount", async () => {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
579
|
+
// A Response body can only be read once, so build one per fetch call.
|
|
580
|
+
const fetchSpy = jest.spyOn(globalThis, "fetch").mockImplementation(() =>
|
|
581
|
+
Promise.resolve(
|
|
582
|
+
new Response(JSON.stringify({ individualAdmins: [] }), {
|
|
583
|
+
headers: { "Content-Type": "application/json" },
|
|
584
|
+
})
|
|
585
|
+
)
|
|
505
586
|
);
|
|
506
587
|
|
|
507
588
|
renderWithProviders(
|
|
@@ -517,5 +598,12 @@ describe("IndividualAdmins - connect wiring", () => {
|
|
|
517
598
|
expect(
|
|
518
599
|
await screen.findByText("Create new individual admin")
|
|
519
600
|
).toBeInTheDocument();
|
|
601
|
+
|
|
602
|
+
// Outside setup mode, fetchData also requests the libraries list (the
|
|
603
|
+
// settingUp half of that guard is pinned in SetupPage.test.tsx).
|
|
604
|
+
const urls = fetchSpy.mock.calls.map((call) => String(call[0]));
|
|
605
|
+
expect(urls).toEqual(
|
|
606
|
+
expect.arrayContaining([expect.stringContaining("/admin/libraries")])
|
|
607
|
+
);
|
|
520
608
|
});
|
|
521
609
|
});
|
|
@@ -87,6 +87,24 @@ describe("LibraryRegistration", () => {
|
|
|
87
87
|
expect(libraries(container)).toHaveLength(0);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("explains instead of vanishing when the library list failed to load", () => {
|
|
91
|
+
const { container } = renderReg({
|
|
92
|
+
item: serviceData,
|
|
93
|
+
data: makeData({
|
|
94
|
+
allLibraries: [],
|
|
95
|
+
allLibrariesError: {
|
|
96
|
+
status: 500,
|
|
97
|
+
response: "nope",
|
|
98
|
+
url: "/admin/libraries",
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
expect(libraries(container)).toHaveLength(0);
|
|
103
|
+
expect(container.querySelector(".alert-warning")).toHaveTextContent(
|
|
104
|
+
"Libraries cannot be registered: the library list is unavailable."
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
90
108
|
it("doesn't render libraries in edit form if protocol doesn't support registration", () => {
|
|
91
109
|
const { container } = renderReg({
|
|
92
110
|
item: serviceData,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
import * as fetchMock from "fetch-mock-jest";
|
|
5
5
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
@@ -217,8 +217,11 @@ describe("PatronAuthServiceEditForm – serialization", () => {
|
|
|
217
217
|
|
|
218
218
|
await user.click(saveButton);
|
|
219
219
|
|
|
220
|
-
//
|
|
220
|
+
// Saving collapses the library. Reopening it shows the rule was stored in
|
|
221
|
+
// library state, not just read from the editor.
|
|
221
222
|
expect(screen.queryByRole("button", { name: /Add Rule/i })).toBeNull();
|
|
223
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
224
|
+
expect(screen.getByLabelText(/Rule Name/i)).toHaveValue("Test Rule");
|
|
222
225
|
});
|
|
223
226
|
|
|
224
227
|
it("includes patron_blocking_rules in payload when adding a new library", async () => {
|
|
@@ -253,8 +256,99 @@ describe("PatronAuthServiceEditForm – serialization", () => {
|
|
|
253
256
|
|
|
254
257
|
await user.click(addButton);
|
|
255
258
|
|
|
256
|
-
//
|
|
259
|
+
// The new-library editor is gone. Opening the added library shows the rule
|
|
260
|
+
// was stored in library state.
|
|
257
261
|
expect(screen.queryByLabelText(/Rule Name/i)).toBeNull();
|
|
262
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
263
|
+
expect(screen.getByLabelText(/Rule Name/i)).toHaveValue("New Rule");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("PatronAuthServiceEditForm – per-library settings", () => {
|
|
268
|
+
const LIB_SETTING_KEY = "lib_setting";
|
|
269
|
+
const protocolWithLibrarySettings = {
|
|
270
|
+
...SIP2_PROTOCOL_DATA,
|
|
271
|
+
library_settings: [{ key: LIB_SETTING_KEY, label: "Library setting" }],
|
|
272
|
+
};
|
|
273
|
+
const dataWithLibrarySettings: PatronAuthServicesData = {
|
|
274
|
+
...servicesData,
|
|
275
|
+
protocols: [protocolWithLibrarySettings, OTHER_PROTOCOL_DATA],
|
|
276
|
+
};
|
|
277
|
+
const settingInput = (root: HTMLElement) =>
|
|
278
|
+
within(root).getByRole("textbox", {
|
|
279
|
+
name: /Library setting/i,
|
|
280
|
+
}) as HTMLInputElement;
|
|
281
|
+
|
|
282
|
+
it("keeps a library setting entered when adding a new library", async () => {
|
|
283
|
+
const user = userEvent.setup();
|
|
284
|
+
const item = {
|
|
285
|
+
id: 3,
|
|
286
|
+
protocol: SIP2_PROTOCOL,
|
|
287
|
+
settings: {},
|
|
288
|
+
libraries: [],
|
|
289
|
+
};
|
|
290
|
+
const { container } = renderForm(
|
|
291
|
+
<PatronAuthServiceEditForm
|
|
292
|
+
{...baseProps}
|
|
293
|
+
data={dataWithLibrarySettings}
|
|
294
|
+
item={item}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await expandLibrariesPanel(user);
|
|
299
|
+
await user.selectOptions(
|
|
300
|
+
screen.getByRole("combobox", { name: /Add Library/i }),
|
|
301
|
+
LIBRARY_SHORT_NAME
|
|
302
|
+
);
|
|
303
|
+
await user.type(settingInput(container), "added value");
|
|
304
|
+
await user.click(screen.getByRole("button", { name: /Add Library/i }));
|
|
305
|
+
|
|
306
|
+
// Reopen the new library's settings; the value entered on add is there.
|
|
307
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
308
|
+
const editSection = container.querySelector(
|
|
309
|
+
".edit-library-settings"
|
|
310
|
+
) as HTMLElement;
|
|
311
|
+
expect(settingInput(editSection).value).toBe("added value");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("keeps a library setting changed when editing an existing library", async () => {
|
|
315
|
+
const user = userEvent.setup();
|
|
316
|
+
const item = {
|
|
317
|
+
...buildSIP2Item(),
|
|
318
|
+
libraries: [
|
|
319
|
+
{ short_name: LIBRARY_SHORT_NAME, [LIB_SETTING_KEY]: "old value" },
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
const { container } = renderForm(
|
|
323
|
+
<PatronAuthServiceEditForm
|
|
324
|
+
{...baseProps}
|
|
325
|
+
data={dataWithLibrarySettings}
|
|
326
|
+
item={item}
|
|
327
|
+
/>
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
await expandLibrariesPanel(user);
|
|
331
|
+
const clickEdit = () =>
|
|
332
|
+
user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
333
|
+
await clickEdit();
|
|
334
|
+
let editSection = container.querySelector(
|
|
335
|
+
".edit-library-settings"
|
|
336
|
+
) as HTMLElement;
|
|
337
|
+
expect(settingInput(editSection).value).toBe("old value");
|
|
338
|
+
|
|
339
|
+
await user.clear(settingInput(editSection));
|
|
340
|
+
await user.type(settingInput(editSection), "new value");
|
|
341
|
+
await user.click(
|
|
342
|
+
editSection.querySelector("button.edit-library") as HTMLElement
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Saving collapses the library; reopening shows the saved value.
|
|
346
|
+
expect(container.querySelector(".edit-library-settings")).toBeNull();
|
|
347
|
+
await clickEdit();
|
|
348
|
+
editSection = container.querySelector(
|
|
349
|
+
".edit-library-settings"
|
|
350
|
+
) as HTMLElement;
|
|
351
|
+
expect(settingInput(editSection).value).toBe("new value");
|
|
258
352
|
});
|
|
259
353
|
});
|
|
260
354
|
|
|
@@ -77,6 +77,21 @@ describe("PatronAuthServices", () => {
|
|
|
77
77
|
).not.toBeInTheDocument();
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
it("fetches the libraries list along with the services on mount", async () => {
|
|
81
|
+
const fetchSpy = stubFetch(listData);
|
|
82
|
+
renderConnected(<PatronAuthServices csrfToken="token" />);
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
const urls = fetchSpy.mock.calls.map((call) => String(call[0]));
|
|
86
|
+
expect(urls).toEqual(
|
|
87
|
+
expect.arrayContaining([
|
|
88
|
+
expect.stringContaining("/admin/patron_auth_services"),
|
|
89
|
+
expect.stringContaining("/admin/libraries"),
|
|
90
|
+
])
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
80
95
|
it("shows the neighborhood analytics panel when creating a service whose protocol has a neighborhood_mode setting", async () => {
|
|
81
96
|
const neighborhoodSetting = {
|
|
82
97
|
key: "neighborhood_mode",
|