@thepalaceproject/circulation-admin 1.46.0-post.2 → 1.46.0-post.3
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 +1 -1
- package/dist/circulation-admin.js +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/PatronAuthServices.test.tsx +15 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +85 -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,
|
|
@@ -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",
|
|
@@ -484,6 +484,91 @@ describe("ServiceEditForm", () => {
|
|
|
484
484
|
expect(container.querySelector(".with-edit-button a")).toBeNull();
|
|
485
485
|
});
|
|
486
486
|
|
|
487
|
+
it("shows a loading indicator in the Libraries panel until allLibraries arrives", () => {
|
|
488
|
+
// Undefined allLibraries means the library list is still loading.
|
|
489
|
+
const dataStillLoading = Object.assign({}, servicesData, {
|
|
490
|
+
allLibraries: undefined,
|
|
491
|
+
});
|
|
492
|
+
const { container, rerender } = renderForm({
|
|
493
|
+
data: dataStillLoading,
|
|
494
|
+
item: serviceData,
|
|
495
|
+
});
|
|
496
|
+
expect(container.querySelector(".update-libraries")).toBeNull();
|
|
497
|
+
expect(container.querySelector('[role="status"]')).toHaveTextContent(
|
|
498
|
+
"Loading libraries..."
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
rerenderForm(rerender, { item: serviceData });
|
|
502
|
+
// The status line stays mounted and announces completion; the
|
|
503
|
+
// completion text is visually hidden.
|
|
504
|
+
const status = container.querySelector('[role="status"]');
|
|
505
|
+
expect(status).toHaveTextContent("Libraries loaded.");
|
|
506
|
+
expect(status).toHaveClass("visuallyHidden");
|
|
507
|
+
const editable = container.querySelectorAll(".with-edit-button");
|
|
508
|
+
expect(editable).toHaveLength(1);
|
|
509
|
+
expect(editable[0]).toHaveTextContent("New York Public Library - nypl");
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("explains a failed library list load in the Libraries panel", () => {
|
|
513
|
+
// On failure allLibraries settles to [] and allLibrariesError is set.
|
|
514
|
+
const dataWithError = Object.assign({}, servicesData, {
|
|
515
|
+
allLibraries: [],
|
|
516
|
+
allLibrariesError: {
|
|
517
|
+
status: 500,
|
|
518
|
+
response: "nope",
|
|
519
|
+
url: "/admin/libraries",
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
const { container } = renderForm({
|
|
523
|
+
data: dataWithError,
|
|
524
|
+
item: serviceData,
|
|
525
|
+
});
|
|
526
|
+
expect(container.querySelector(".alert-danger")).toHaveTextContent(
|
|
527
|
+
"The library list failed to load"
|
|
528
|
+
);
|
|
529
|
+
// The role="alert" danger Alert announces the failure itself, so the
|
|
530
|
+
// status region empties rather than duplicating the announcement.
|
|
531
|
+
expect(container.querySelector('[role="status"]')).toBeEmptyDOMElement();
|
|
532
|
+
// The associated library still renders, by short name.
|
|
533
|
+
const editable = container.querySelectorAll(".with-edit-button");
|
|
534
|
+
expect(editable).toHaveLength(1);
|
|
535
|
+
expect(editable[0]).toHaveTextContent("nypl");
|
|
536
|
+
// A removal could not be undone in-session, so it is disabled too.
|
|
537
|
+
expect(container.querySelector("button.remove-btn")).toBeDisabled();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("warns when the library list could not be refreshed", () => {
|
|
541
|
+
const staleData = Object.assign({}, servicesData, {
|
|
542
|
+
allLibrariesRefreshError: {
|
|
543
|
+
status: 500,
|
|
544
|
+
response: "nope",
|
|
545
|
+
url: "/admin/libraries",
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
const { container } = renderForm({ data: staleData, item: serviceData });
|
|
549
|
+
expect(container.querySelector(".alert-warning")).toHaveTextContent(
|
|
550
|
+
"The library list could not be refreshed"
|
|
551
|
+
);
|
|
552
|
+
// The role="alert" warning announces the refresh failure itself, so
|
|
553
|
+
// the status region empties rather than duplicating the announcement.
|
|
554
|
+
expect(container.querySelector('[role="status"]')).toBeEmptyDOMElement();
|
|
555
|
+
// The stale list still renders in full, links included, and the
|
|
556
|
+
// panel stays editable; only the blocking error disables removal.
|
|
557
|
+
const editable = container.querySelectorAll(".with-edit-button");
|
|
558
|
+
expect(editable).toHaveLength(1);
|
|
559
|
+
expect(editable[0]).toHaveTextContent("New York Public Library - nypl");
|
|
560
|
+
expect(container.querySelector(".alert-danger")).toBeNull();
|
|
561
|
+
expect(container.querySelector("button.remove-btn")).toBeEnabled();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("says when no libraries are configured", () => {
|
|
565
|
+
const emptyData = Object.assign({}, servicesData, { allLibraries: [] });
|
|
566
|
+
const { container } = renderForm({ data: emptyData });
|
|
567
|
+
expect(container.querySelector(".update-libraries")).toHaveTextContent(
|
|
568
|
+
"No libraries are configured."
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
|
|
487
572
|
it("renders removable and editable libraries", () => {
|
|
488
573
|
const { container, unmount } = renderForm();
|
|
489
574
|
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(0);
|
|
@@ -6,13 +6,16 @@ import SetupPage from "../../../src/components/SetupPage";
|
|
|
6
6
|
|
|
7
7
|
describe("SetupPage", () => {
|
|
8
8
|
beforeEach(() => {
|
|
9
|
-
// SetupPage renders the connected IndividualAdmins list, which fetches
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
// SetupPage renders the connected IndividualAdmins list, which fetches on
|
|
10
|
+
// mount. Stub fetch so mounting does not hit the network; a Response body
|
|
11
|
+
// can only be read once, so build one per call.
|
|
12
|
+
jest.spyOn(globalThis, "fetch").mockImplementation(() =>
|
|
13
|
+
Promise.resolve(
|
|
14
|
+
new Response(JSON.stringify({ individualAdmins: [] }), {
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
})
|
|
18
|
+
)
|
|
16
19
|
);
|
|
17
20
|
});
|
|
18
21
|
|
|
@@ -37,5 +40,19 @@ describe("SetupPage", () => {
|
|
|
37
40
|
name: "Set up your system admin account",
|
|
38
41
|
})
|
|
39
42
|
).toBeInTheDocument();
|
|
43
|
+
|
|
44
|
+
// settingUp skips the libraries request, which cannot succeed before an
|
|
45
|
+
// admin exists. The positive check keeps the negative one honest.
|
|
46
|
+
const urls = (globalThis.fetch as jest.Mock).mock.calls.map((call) =>
|
|
47
|
+
String(call[0])
|
|
48
|
+
);
|
|
49
|
+
expect(urls).toEqual(
|
|
50
|
+
expect.arrayContaining([
|
|
51
|
+
expect.stringContaining("/admin/individual_admins"),
|
|
52
|
+
])
|
|
53
|
+
);
|
|
54
|
+
expect(urls).not.toEqual(
|
|
55
|
+
expect.arrayContaining([expect.stringContaining("/admin/libraries")])
|
|
56
|
+
);
|
|
40
57
|
});
|
|
41
58
|
});
|