@thepalaceproject/circulation-admin 1.43.0-post.14 → 1.43.0-post.16

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.
@@ -1,9 +1,10 @@
1
1
  import * as React from "react";
2
+ import { installFormDataShim } from "../testUtils/formDataShim";
2
3
  import { screen } from "@testing-library/react";
3
4
  import userEvent from "@testing-library/user-event";
4
5
  import IndividualAdminEditForm from "../../../src/components/IndividualAdminEditForm";
5
6
  import renderWithContext from "../testUtils/renderWithContext";
6
- import { ConfigurationSettings } from "../../../src/interfaces";
7
+ import { AdminRoleData, ConfigurationSettings } from "../../../src/interfaces";
7
8
 
8
9
  describe("IndividualAdminEditForm", () => {
9
10
  it("clears the role checkboxes after save", async () => {
@@ -126,3 +127,507 @@ describe("IndividualAdminEditForm", () => {
126
127
  expect(gammaUserCheckbox).not.toBeChecked();
127
128
  });
128
129
  });
130
+
131
+ // Assertions on props (checked/disabled/value) are made against the rendered
132
+ // checkboxes and inputs; role-change behavior is driven with real clicks.
133
+ describe("IndividualAdminEditForm - rendered inputs and role changes", () => {
134
+ // The reusable-components `Form` builds `new FormData(formElement)` on
135
+ // submit, which the unit jsdom env's undici FormData rejects; install the
136
+ // shared shim that reads the form's successful controls.
137
+ installFormDataShim();
138
+ const adminData = { email: "test@nypl.org", password: "password" };
139
+ const allLibraries = [
140
+ { name: "NYPL", short_name: "nypl" },
141
+ { name: "BPL", short_name: "bpl" },
142
+ ];
143
+ const baseData = { individualAdmins: [adminData], allLibraries };
144
+
145
+ const systemAdmin: AdminRoleData[] = [{ role: "system" }];
146
+ const managerAll: AdminRoleData[] = [{ role: "manager-all" }];
147
+ const librarianAll: AdminRoleData[] = [{ role: "librarian-all" }];
148
+ const nyplManager: AdminRoleData[] = [{ role: "manager", library: "nypl" }];
149
+ const bplManager: AdminRoleData[] = [{ role: "manager", library: "bpl" }];
150
+ const nyplLibrarian: AdminRoleData[] = [
151
+ { role: "librarian", library: "nypl" },
152
+ ];
153
+ const bplLibrarian: AdminRoleData[] = [{ role: "librarian", library: "bpl" }];
154
+ const nyplManagerLibrarianAll: AdminRoleData[] = [
155
+ { role: "manager", library: "nypl" },
156
+ { role: "librarian-all" },
157
+ ];
158
+
159
+ const configFor = (
160
+ roles: AdminRoleData[],
161
+ settingUp = false
162
+ ): Partial<ConfigurationSettings> => ({
163
+ csrfToken: "",
164
+ featureFlags: {},
165
+ roles,
166
+ settingUp,
167
+ });
168
+
169
+ const systemConfig = configFor(systemAdmin);
170
+
171
+ let save: jest.Mock;
172
+
173
+ const baseProps = (overrides: Record<string, any> = {}) => ({
174
+ data: baseData,
175
+ disabled: false,
176
+ save,
177
+ urlBase: "url base",
178
+ listDataKey: "admins",
179
+ ...overrides,
180
+ });
181
+
182
+ const renderForm = (
183
+ overrides: Record<string, any> = {},
184
+ config: Partial<ConfigurationSettings> = systemConfig
185
+ ) =>
186
+ renderWithContext(
187
+ <IndividualAdminEditForm {...(baseProps(overrides) as any)} />,
188
+ config
189
+ );
190
+
191
+ const roleNames: Record<string, RegExp> = {
192
+ system: /^system admin$/i,
193
+ "manager-all": /^administrator$/i,
194
+ "librarian-all": /^user$/i,
195
+ "manager-nypl": /administrator of nypl/i,
196
+ "manager-bpl": /administrator of bpl/i,
197
+ "librarian-nypl": /user of nypl/i,
198
+ "librarian-bpl": /user of bpl/i,
199
+ };
200
+ const allRoles = Object.keys(roleNames);
201
+
202
+ const roleCheckbox = (role: string) =>
203
+ screen.getByRole("checkbox", { name: roleNames[role] });
204
+
205
+ const expectRoles = (expected: string[]) => {
206
+ for (const role of allRoles) {
207
+ if (expected.includes(role)) {
208
+ expect(roleCheckbox(role)).toBeChecked();
209
+ } else {
210
+ expect(roleCheckbox(role)).not.toBeChecked();
211
+ }
212
+ }
213
+ };
214
+
215
+ // A fresh item object each time, so the component's
216
+ // UNSAFE_componentWillReceiveProps resets its admin state from the new roles.
217
+ const setRoles = (rerender: (ui: React.ReactElement) => void, roles) =>
218
+ rerender(
219
+ <IndividualAdminEditForm
220
+ {...(baseProps({ item: { ...adminData, roles } }) as any)}
221
+ />
222
+ );
223
+
224
+ const saveButton = (container: HTMLElement) =>
225
+ container.querySelector<HTMLButtonElement>("button:not(.panel-heading)");
226
+
227
+ beforeEach(() => {
228
+ save = jest.fn().mockResolvedValue(undefined);
229
+ });
230
+
231
+ // ── Rendering ──────────────────────────────────────────────────────────────
232
+
233
+ it("renders the email field, read-only with a value when editing an admin", () => {
234
+ const { container, rerender } = renderForm();
235
+ let email = container.querySelector<HTMLInputElement>(
236
+ 'input[name="email"]'
237
+ );
238
+ expect(email).not.toBeNull();
239
+ expect(email.value).toBe("");
240
+
241
+ rerender(
242
+ <IndividualAdminEditForm {...(baseProps({ item: adminData }) as any)} />
243
+ );
244
+ email = container.querySelector<HTMLInputElement>('input[name="email"]');
245
+ expect(email.value).toBe("test@nypl.org");
246
+ expect(email.readOnly).toBe(true);
247
+ });
248
+
249
+ it("requires the email and password fields", () => {
250
+ const { container } = renderForm();
251
+ expect(container.querySelectorAll(".required-field")).toHaveLength(2);
252
+ expect(container.innerHTML).not.toContain("(Optional)");
253
+ });
254
+
255
+ it("shows the password field only when the editor may change it", () => {
256
+ const passwordShownFor = (
257
+ targetRoles: AdminRoleData[] | undefined,
258
+ editingRoles: AdminRoleData[]
259
+ ) => {
260
+ const overrides = targetRoles ? { item: { roles: targetRoles } } : {};
261
+ const { container, unmount } = renderForm(
262
+ overrides,
263
+ configFor(editingRoles)
264
+ );
265
+ const shown = !!container.querySelector('input[name="password"]');
266
+ unmount();
267
+ return shown;
268
+ };
269
+
270
+ // No target admin at all → always editable.
271
+ expect(passwordShownFor(undefined, systemAdmin)).toBe(true);
272
+ // Target has no roles → editable iff the editor manages some library.
273
+ expect(passwordShownFor([], managerAll)).toBe(true);
274
+ expect(passwordShownFor([], librarianAll)).toBe(false);
275
+ // Target is a system admin → editable only by a system admin.
276
+ expect(passwordShownFor(systemAdmin, systemAdmin)).toBe(true);
277
+ expect(passwordShownFor(systemAdmin, managerAll)).toBe(false);
278
+ // Target is a sitewide manager → editable only by a sitewide manager.
279
+ expect(passwordShownFor(managerAll, managerAll)).toBe(true);
280
+ expect(passwordShownFor(managerAll, librarianAll)).toBe(false);
281
+ // Target manages a single library → editable by a manager of that library.
282
+ expect(passwordShownFor(nyplManager, nyplManager)).toBe(true);
283
+ expect(passwordShownFor(nyplManager, bplManager)).toBe(false);
284
+ // Target is a sitewide librarian → its role carries no library, so the
285
+ // per-role check falls to isLibraryManager(undefined): only a sitewide
286
+ // manager qualifies, never a manager of one particular library.
287
+ expect(passwordShownFor(librarianAll, systemAdmin)).toBe(true);
288
+ expect(passwordShownFor(librarianAll, managerAll)).toBe(true);
289
+ expect(passwordShownFor(librarianAll, librarianAll)).toBe(false);
290
+ expect(passwordShownFor(librarianAll, nyplManager)).toBe(false);
291
+ expect(passwordShownFor(librarianAll, nyplLibrarian)).toBe(false);
292
+ expect(passwordShownFor(librarianAll, nyplManagerLibrarianAll)).toBe(false);
293
+ // Target holds roles at two libraries → a manager of either one qualifies.
294
+ expect(
295
+ passwordShownFor([...nyplManager, ...bplLibrarian], bplManager)
296
+ ).toBe(true);
297
+ });
298
+
299
+ it("never pre-fills the existing password", () => {
300
+ const { container } = renderForm({
301
+ item: { ...adminData, roles: nyplManager },
302
+ });
303
+
304
+ const password = container.querySelector<HTMLInputElement>(
305
+ 'input[name="password"]'
306
+ );
307
+ expect(password).not.toBeNull();
308
+ expect(password.value).toBe("");
309
+ });
310
+
311
+ it("has a save button only when a save function is supplied", () => {
312
+ const withSave = renderForm();
313
+ expect(saveButton(withSave.container)).not.toBeNull();
314
+ withSave.unmount();
315
+
316
+ const withoutSave = renderForm({ save: undefined });
317
+ expect(saveButton(withoutSave.container)).toBeNull();
318
+ });
319
+
320
+ it("does not render role checkboxes when setting up the first admin", () => {
321
+ renderForm({}, configFor(systemAdmin, true));
322
+ for (const role of [
323
+ "system",
324
+ "manager-all",
325
+ "librarian-all",
326
+ "manager-nypl",
327
+ "librarian-nypl",
328
+ ]) {
329
+ expect(
330
+ screen.queryByRole("checkbox", { name: roleNames[role] })
331
+ ).toBeNull();
332
+ }
333
+ });
334
+
335
+ // ── Disabled state (isDisabled) ──────────────────────────────────────────────
336
+
337
+ it("disables every role checkbox when the form is disabled", () => {
338
+ renderForm({ disabled: true });
339
+ expect(roleCheckbox("system")).toBeDisabled();
340
+ expect(roleCheckbox("manager-all")).toBeDisabled();
341
+ expect(roleCheckbox("manager-nypl")).toBeDisabled();
342
+ });
343
+
344
+ it("lets a system-admin editor edit every role", () => {
345
+ renderForm();
346
+ for (const role of allRoles) {
347
+ expect(roleCheckbox(role)).toBeEnabled();
348
+ }
349
+ });
350
+
351
+ it("restricts a sitewide-manager editor from the system-admin role", () => {
352
+ renderForm({}, configFor(managerAll));
353
+ expect(roleCheckbox("system")).toBeDisabled();
354
+ expect(roleCheckbox("manager-all")).toBeEnabled();
355
+ expect(roleCheckbox("librarian-all")).toBeEnabled();
356
+ expect(roleCheckbox("manager-nypl")).toBeEnabled();
357
+ });
358
+
359
+ it("restricts a sitewide-librarian editor from the manager roles", () => {
360
+ renderForm({}, configFor(librarianAll));
361
+ expect(roleCheckbox("system")).toBeDisabled();
362
+ expect(roleCheckbox("manager-all")).toBeDisabled();
363
+ expect(roleCheckbox("librarian-all")).toBeDisabled();
364
+ });
365
+
366
+ it("restricts a single-library manager editor to that library", () => {
367
+ renderForm({}, configFor(nyplManager));
368
+ expect(roleCheckbox("manager-all")).toBeDisabled();
369
+ expect(roleCheckbox("manager-nypl")).toBeEnabled();
370
+ expect(roleCheckbox("manager-bpl")).toBeDisabled();
371
+ expect(roleCheckbox("librarian-nypl")).toBeEnabled();
372
+ expect(roleCheckbox("librarian-bpl")).toBeDisabled();
373
+ });
374
+
375
+ it("disables a per-library box when a sitewide role is selected but the editor is not sitewide", () => {
376
+ const { rerender } = renderForm(
377
+ { item: { ...adminData, roles: managerAll } },
378
+ configFor(nyplManager)
379
+ );
380
+ // manager-all is selected on the target; a non-sitewide editor can't touch it.
381
+ expect(roleCheckbox("manager-nypl")).toBeDisabled();
382
+ expect(roleCheckbox("manager-bpl")).toBeDisabled();
383
+
384
+ rerender(
385
+ <IndividualAdminEditForm
386
+ {...(baseProps({ item: { ...adminData, roles: librarianAll } }) as any)}
387
+ />
388
+ );
389
+ expect(roleCheckbox("librarian-nypl")).toBeDisabled();
390
+ expect(roleCheckbox("librarian-bpl")).toBeDisabled();
391
+ });
392
+
393
+ it("also restricts an editor when the target admin is a system admin", () => {
394
+ renderForm(
395
+ { item: { ...adminData, roles: systemAdmin } },
396
+ configFor(managerAll)
397
+ );
398
+ // isSelected("system") is true, so a non-system editor can edit nothing.
399
+ expect(roleCheckbox("manager-all")).toBeDisabled();
400
+ });
401
+
402
+ // ── Role-change behavior (handleRoleChange) ──────────────────────────────────
403
+
404
+ it("changes the system admin role", async () => {
405
+ const user = userEvent.setup();
406
+ renderForm({ item: { ...adminData, roles: nyplManagerLibrarianAll } });
407
+
408
+ await user.click(roleCheckbox("system"));
409
+ expectRoles(allRoles);
410
+
411
+ await user.click(roleCheckbox("system"));
412
+ expectRoles([]);
413
+ });
414
+
415
+ it("changes the manager-all role", async () => {
416
+ const user = userEvent.setup();
417
+ const { rerender } = renderForm();
418
+
419
+ await user.click(roleCheckbox("manager-all"));
420
+ expectRoles([
421
+ "manager-all",
422
+ "librarian-all",
423
+ "manager-nypl",
424
+ "manager-bpl",
425
+ "librarian-nypl",
426
+ "librarian-bpl",
427
+ ]);
428
+
429
+ await user.click(roleCheckbox("manager-all"));
430
+ expectRoles(["librarian-all", "librarian-nypl", "librarian-bpl"]);
431
+
432
+ setRoles(rerender, systemAdmin);
433
+ await user.click(roleCheckbox("manager-all"));
434
+ expectRoles(["librarian-all", "librarian-nypl", "librarian-bpl"]);
435
+
436
+ setRoles(rerender, nyplManagerLibrarianAll);
437
+ await user.click(roleCheckbox("manager-all"));
438
+ expectRoles([
439
+ "manager-all",
440
+ "librarian-all",
441
+ "manager-nypl",
442
+ "manager-bpl",
443
+ "librarian-nypl",
444
+ "librarian-bpl",
445
+ ]);
446
+ });
447
+
448
+ it("changes the librarian-all role", async () => {
449
+ const user = userEvent.setup();
450
+ const { rerender } = renderForm();
451
+
452
+ await user.click(roleCheckbox("librarian-all"));
453
+ expectRoles(["librarian-all", "librarian-nypl", "librarian-bpl"]);
454
+
455
+ await user.click(roleCheckbox("librarian-all"));
456
+ expectRoles([]);
457
+
458
+ setRoles(rerender, systemAdmin);
459
+ await user.click(roleCheckbox("librarian-all"));
460
+ expectRoles([]);
461
+
462
+ setRoles(rerender, nyplManagerLibrarianAll);
463
+ await user.click(roleCheckbox("librarian-all"));
464
+ expectRoles(["manager-nypl", "librarian-nypl"]);
465
+
466
+ setRoles(rerender, nyplLibrarian);
467
+ await user.click(roleCheckbox("librarian-all"));
468
+ expectRoles(["librarian-all", "librarian-nypl", "librarian-bpl"]);
469
+ });
470
+
471
+ it("changes the manager role for each library", async () => {
472
+ const user = userEvent.setup();
473
+ const { rerender } = renderForm();
474
+
475
+ await user.click(roleCheckbox("manager-nypl"));
476
+ expectRoles(["manager-nypl", "librarian-nypl"]);
477
+
478
+ await user.click(roleCheckbox("manager-nypl"));
479
+ expectRoles(["librarian-nypl"]);
480
+
481
+ setRoles(rerender, systemAdmin);
482
+ await user.click(roleCheckbox("manager-nypl"));
483
+ expectRoles([
484
+ "librarian-all",
485
+ "manager-bpl",
486
+ "librarian-nypl",
487
+ "librarian-bpl",
488
+ ]);
489
+
490
+ setRoles(rerender, managerAll);
491
+ await user.click(roleCheckbox("manager-nypl"));
492
+ expectRoles([
493
+ "librarian-all",
494
+ "manager-bpl",
495
+ "librarian-nypl",
496
+ "librarian-bpl",
497
+ ]);
498
+
499
+ setRoles(rerender, nyplManagerLibrarianAll);
500
+ await user.click(roleCheckbox("manager-nypl"));
501
+ expectRoles(["librarian-all", "librarian-nypl", "librarian-bpl"]);
502
+
503
+ setRoles(rerender, nyplLibrarian);
504
+ await user.click(roleCheckbox("manager-nypl"));
505
+ expectRoles(["manager-nypl", "librarian-nypl"]);
506
+
507
+ setRoles(rerender, bplLibrarian);
508
+ await user.click(roleCheckbox("manager-nypl"));
509
+ expectRoles(["manager-nypl", "librarian-nypl", "librarian-bpl"]);
510
+ });
511
+
512
+ it("changes the librarian role for each library", async () => {
513
+ const user = userEvent.setup();
514
+ const { rerender } = renderForm();
515
+
516
+ await user.click(roleCheckbox("librarian-nypl"));
517
+ expectRoles(["librarian-nypl"]);
518
+
519
+ await user.click(roleCheckbox("librarian-nypl"));
520
+ expectRoles([]);
521
+
522
+ setRoles(rerender, systemAdmin);
523
+ await user.click(roleCheckbox("librarian-nypl"));
524
+ expectRoles(["manager-bpl", "librarian-bpl"]);
525
+
526
+ setRoles(rerender, managerAll);
527
+ await user.click(roleCheckbox("librarian-nypl"));
528
+ expectRoles(["manager-bpl", "librarian-bpl"]);
529
+
530
+ setRoles(rerender, nyplManagerLibrarianAll);
531
+ await user.click(roleCheckbox("librarian-nypl"));
532
+ expectRoles(["librarian-bpl"]);
533
+
534
+ setRoles(rerender, bplLibrarian);
535
+ await user.click(roleCheckbox("librarian-nypl"));
536
+ expectRoles(["librarian-nypl", "librarian-bpl"]);
537
+ });
538
+
539
+ // ── Submission (submit / handleData) ─────────────────────────────────────────
540
+
541
+ it("calls save when the save button is clicked", async () => {
542
+ const user = userEvent.setup();
543
+ const { container } = renderForm();
544
+ await user.click(saveButton(container));
545
+ expect(save).toHaveBeenCalledTimes(1);
546
+ });
547
+
548
+ it("submits the entered email, password, and selected roles", async () => {
549
+ const user = userEvent.setup();
550
+ const { container } = renderForm();
551
+
552
+ await user.type(
553
+ container.querySelector<HTMLInputElement>('input[name="email"]'),
554
+ "newEmail"
555
+ );
556
+ await user.type(
557
+ container.querySelector<HTMLInputElement>('input[name="password"]'),
558
+ "newPassword"
559
+ );
560
+ await user.click(roleCheckbox("librarian-all"));
561
+ await user.click(roleCheckbox("manager-nypl"));
562
+
563
+ await user.click(saveButton(container));
564
+
565
+ expect(save).toHaveBeenCalledTimes(1);
566
+ const formData = save.mock.calls[0][0];
567
+ expect(formData.get("email")).toBe("newEmail");
568
+ expect(formData.get("password")).toBe("newPassword");
569
+ expect(formData.get("roles")).toBe(
570
+ JSON.stringify([
571
+ { role: "librarian-all" },
572
+ { role: "manager", library: "nypl" },
573
+ ])
574
+ );
575
+ });
576
+
577
+ it("submits only a system-admin role when setting up the first admin", async () => {
578
+ const user = userEvent.setup();
579
+ const { container } = renderForm({}, configFor(systemAdmin, true));
580
+
581
+ await user.type(
582
+ container.querySelector<HTMLInputElement>('input[name="email"]'),
583
+ "newEmail"
584
+ );
585
+ await user.type(
586
+ container.querySelector<HTMLInputElement>('input[name="password"]'),
587
+ "newPassword"
588
+ );
589
+
590
+ await user.click(saveButton(container));
591
+
592
+ expect(save).toHaveBeenCalledTimes(1);
593
+ const formData = save.mock.calls[0][0];
594
+ expect(formData.get("email")).toBe("newEmail");
595
+ expect(formData.get("password")).toBe("newPassword");
596
+ expect(formData.get("roles")).toBe(JSON.stringify([{ role: "system" }]));
597
+ });
598
+
599
+ it("clears the form after a successful save", async () => {
600
+ const user = userEvent.setup();
601
+ const { container, rerender } = renderForm();
602
+ const email = () =>
603
+ container.querySelector<HTMLInputElement>('input[name="email"]');
604
+
605
+ await user.type(email(), "newEmail");
606
+ expect(email().value).toBe("newEmail");
607
+
608
+ // The presence of responseBody (and no error) signals a successful save.
609
+ rerender(
610
+ <IndividualAdminEditForm
611
+ {...(baseProps({ responseBody: "new admin" }) as any)}
612
+ />
613
+ );
614
+ expect(email().value).toBe("");
615
+ });
616
+
617
+ it("does not clear the form when there is an error", async () => {
618
+ const user = userEvent.setup();
619
+ const { container, rerender } = renderForm();
620
+ const email = () =>
621
+ container.querySelector<HTMLInputElement>('input[name="email"]');
622
+
623
+ await user.type(email(), "newEmail");
624
+ expect(email().value).toBe("newEmail");
625
+
626
+ rerender(
627
+ <IndividualAdminEditForm
628
+ {...(baseProps({ fetchError: "ERROR" }) as any)}
629
+ />
630
+ );
631
+ expect(email().value).toBe("newEmail");
632
+ });
633
+ });
@@ -1,19 +1,17 @@
1
1
  import * as React from "react";
2
- import { fireEvent } from "@testing-library/react";
3
- import { IndividualAdmins } from "../../../src/components/IndividualAdmins";
2
+ import { fireEvent, screen } from "@testing-library/react";
3
+ import ConnectedIndividualAdmins, {
4
+ IndividualAdmins,
5
+ } from "../../../src/components/IndividualAdmins";
4
6
  import renderWithContext from "../testUtils/renderWithContext";
7
+ import { renderWithProviders } from "../testUtils/withProviders";
5
8
  import {
9
+ AdminRoleData,
6
10
  ConfigurationSettings,
7
11
  IndividualAdminsData,
8
12
  } from "../../../src/interfaces";
9
13
  import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
10
14
 
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
15
  describe("IndividualAdmins - role association disclosure", () => {
18
16
  // ── Shared fixtures ───────────────────────────────────────────────────────
19
17
 
@@ -388,3 +386,121 @@ describe("IndividualAdmins - role association disclosure", () => {
388
386
  expect(container.querySelectorAll(".associated-items")).toHaveLength(0);
389
387
  });
390
388
  });
389
+
390
+ // The settingUp headers ("Welcome!" / "Set up your system admin account") are
391
+ // covered by SetupPage.test.tsx, which exercises the real prop wiring. Only the
392
+ // non-setup branch needs covering here.
393
+ describe("IndividualAdmins - headers", () => {
394
+ it("shows the configuration messages when not setting up", () => {
395
+ renderWithContext(
396
+ <IndividualAdmins
397
+ settingUp={false}
398
+ editOrCreate="create"
399
+ csrfToken="token"
400
+ data={{ individualAdmins: [] }}
401
+ fetchData={jest.fn()}
402
+ editItem={jest.fn().mockResolvedValue(undefined)}
403
+ deleteItem={jest.fn().mockResolvedValue(undefined)}
404
+ isFetching={false}
405
+ />,
406
+ { csrfToken: "", featureFlags: defaultFeatureFlags, roles: [] }
407
+ );
408
+
409
+ expect(
410
+ screen.getByRole("heading", {
411
+ level: 2,
412
+ name: "Individual admin configuration",
413
+ })
414
+ ).toBeInTheDocument();
415
+ expect(
416
+ screen.getByRole("heading", {
417
+ level: 3,
418
+ name: "Create a new individual admin",
419
+ })
420
+ ).toBeInTheDocument();
421
+ });
422
+ });
423
+
424
+ describe("IndividualAdmins - access control", () => {
425
+ const renderAs = (roles: AdminRoleData[]) =>
426
+ renderWithContext(
427
+ <IndividualAdmins
428
+ data={{
429
+ individualAdmins: [{ email: "someone@example.com", roles: [] }],
430
+ }}
431
+ fetchData={jest.fn()}
432
+ editItem={jest.fn().mockResolvedValue(undefined)}
433
+ deleteItem={jest.fn().mockResolvedValue(undefined)}
434
+ csrfToken="token"
435
+ isFetching={false}
436
+ />,
437
+ { csrfToken: "", featureFlags: defaultFeatureFlags, roles }
438
+ );
439
+
440
+ const createLink = () => screen.queryByText("Create new individual admin");
441
+ const deleteButton = () => screen.queryByRole("button", { name: /Delete/ });
442
+
443
+ it("lets library managers create, edit and view, but not delete", () => {
444
+ renderAs([{ role: "manager", library: "alpha" }]);
445
+ expect(createLink()).toBeInTheDocument();
446
+ expect(screen.getByText("Edit")).toBeInTheDocument();
447
+ expect(deleteButton()).toBeNull();
448
+ });
449
+
450
+ it("lets sitewide library managers create and edit", () => {
451
+ renderAs([{ role: "manager-all" }]);
452
+ expect(createLink()).toBeInTheDocument();
453
+ expect(screen.getByText("Edit")).toBeInTheDocument();
454
+ });
455
+
456
+ it("lets system admins create, edit and delete", () => {
457
+ renderAs([{ role: "system" }]);
458
+ expect(createLink()).toBeInTheDocument();
459
+ expect(screen.getByText("Edit")).toBeInTheDocument();
460
+ expect(deleteButton()).toBeInTheDocument();
461
+ });
462
+
463
+ it("does not let librarians create, edit or delete", () => {
464
+ renderAs([{ role: "librarian", library: "alpha" }]);
465
+ expect(createLink()).toBeNull();
466
+ expect(screen.queryByText("Edit")).toBeNull();
467
+ // Non-editors get a read-only View link in place of Edit.
468
+ expect(screen.getByText("View")).toBeInTheDocument();
469
+ expect(deleteButton()).toBeNull();
470
+ });
471
+
472
+ it("does not let sitewide librarians create, edit or delete", () => {
473
+ renderAs([{ role: "librarian-all" }]);
474
+ expect(createLink()).toBeNull();
475
+ expect(screen.getByText("View")).toBeInTheDocument();
476
+ expect(deleteButton()).toBeNull();
477
+ });
478
+ });
479
+
480
+ // Exercises the connected default export so mapStateToProps/mapDispatchToProps
481
+ // are covered.
482
+ describe("IndividualAdmins - connect wiring", () => {
483
+ afterEach(() => jest.restoreAllMocks());
484
+
485
+ it("renders the connected default export, fetching on mount", async () => {
486
+ jest.spyOn(globalThis, "fetch").mockResolvedValue(
487
+ new Response(JSON.stringify({ individualAdmins: [] }), {
488
+ headers: { "Content-Type": "application/json" },
489
+ })
490
+ );
491
+
492
+ renderWithProviders(
493
+ <ConnectedIndividualAdmins
494
+ csrfToken="token"
495
+ editOrCreate={undefined}
496
+ identifier={undefined}
497
+ />,
498
+ { appConfigSettings: { roles: [{ role: "system" }] } }
499
+ );
500
+
501
+ // A system admin sees the create link once the mount fetch settles.
502
+ expect(
503
+ await screen.findByText("Create new individual admin")
504
+ ).toBeInTheDocument();
505
+ });
506
+ });