@thepalaceproject/circulation-admin 1.39.0 → 1.40.0

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.
@@ -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
+ });