@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,545 @@
1
+ import * as React from "react";
2
+ import { fireEvent } from "@testing-library/react";
3
+ import { DiscoveryServices } from "../../../src/components/DiscoveryServices";
4
+ import renderWithContext from "../testUtils/renderWithContext";
5
+ import {
6
+ ConfigurationSettings,
7
+ DiscoveryServicesData,
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__/DiscoveryServices-test.tsx`.
13
+ //
14
+ // Those tests should eventually be migrated here and
15
+ // adapted to the Jest/React Testing Library paradigm.
16
+
17
+ describe("DiscoveryServices - registered library 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 renderServices = (data: Partial<DiscoveryServicesData>) =>
34
+ renderWithContext(
35
+ <DiscoveryServices
36
+ data={
37
+ {
38
+ discovery_services: [],
39
+ allLibraries,
40
+ ...data,
41
+ } as DiscoveryServicesData
42
+ }
43
+ fetchData={jest.fn()}
44
+ editItem={jest.fn().mockResolvedValue(undefined)}
45
+ deleteItem={jest.fn().mockResolvedValue(undefined)}
46
+ registerLibrary={jest.fn().mockResolvedValue(undefined)}
47
+ fetchLibraryRegistrations={jest.fn().mockResolvedValue(undefined)}
48
+ csrfToken="token"
49
+ isFetching={false}
50
+ />,
51
+ sysAdminConfig
52
+ );
53
+
54
+ // ── Toggle visibility ─────────────────────────────────────────────────────
55
+
56
+ it("shows no toggle when libraryRegistrations data has not yet loaded", () => {
57
+ const { container } = renderServices({
58
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
59
+ // libraryRegistrations omitted → undefined
60
+ });
61
+ expect(container.querySelector(".association-toggle")).toBeNull();
62
+ expect(container.querySelector(".library-count")).toBeNull();
63
+ });
64
+
65
+ it("shows a disabled toggle when libraryRegistrations is loaded but the service has no entry in it", () => {
66
+ // libraryRegistrations is present (so getAssociatedEntries returns [] not
67
+ // undefined), but service id=99 has no corresponding record in the array.
68
+ // Result: disabled toggle + "no registered libraries", not "no toggle".
69
+ const { container } = renderServices({
70
+ discovery_services: [{ id: 99, protocol: "p", name: "Service X" } as any],
71
+ libraryRegistrations: [
72
+ {
73
+ id: 1,
74
+ libraries: [
75
+ {
76
+ short_name: "alpha",
77
+ status: "success",
78
+ stage: "production",
79
+ } as any,
80
+ ],
81
+ },
82
+ ],
83
+ });
84
+ const toggle = container.querySelector<HTMLButtonElement>(
85
+ ".association-toggle"
86
+ );
87
+ expect(toggle).not.toBeNull();
88
+ expect(toggle.disabled).toBe(true);
89
+ expect(container.querySelector(".library-count").textContent).toBe(
90
+ " (no registered libraries)"
91
+ );
92
+ });
93
+
94
+ it("shows a disabled toggle and 'no registered libraries' when none are registered", () => {
95
+ const { container } = renderServices({
96
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
97
+ libraryRegistrations: [
98
+ {
99
+ id: 1,
100
+ libraries: [
101
+ {
102
+ short_name: "alpha",
103
+ status: "warning",
104
+ } as any,
105
+ {
106
+ short_name: "beta",
107
+ status: "failure",
108
+ } as any,
109
+ ],
110
+ },
111
+ ],
112
+ });
113
+ const toggle = container.querySelector<HTMLButtonElement>(
114
+ ".association-toggle"
115
+ );
116
+ expect(toggle).not.toBeNull();
117
+ expect(toggle.disabled).toBe(true);
118
+ expect(container.querySelector(".library-count").textContent).toBe(
119
+ " (no registered libraries)"
120
+ );
121
+ });
122
+
123
+ it("shows '1 registered library' when exactly one library is registered", () => {
124
+ const { container } = renderServices({
125
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
126
+ libraryRegistrations: [
127
+ {
128
+ id: 1,
129
+ libraries: [
130
+ {
131
+ short_name: "alpha",
132
+ status: "success",
133
+ stage: "production",
134
+ } as any,
135
+ ],
136
+ },
137
+ ],
138
+ });
139
+ const toggle = container.querySelector<HTMLButtonElement>(
140
+ ".association-toggle"
141
+ );
142
+ expect(toggle.disabled).toBe(false);
143
+ expect(container.querySelector(".library-count").textContent).toBe(
144
+ " (1 registered library)"
145
+ );
146
+ });
147
+
148
+ it("shows 'N registered libraries' when multiple are registered", () => {
149
+ const { container } = renderServices({
150
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
151
+ libraryRegistrations: [
152
+ {
153
+ id: 1,
154
+ libraries: [
155
+ {
156
+ short_name: "alpha",
157
+ status: "success",
158
+ stage: "production",
159
+ } as any,
160
+ { short_name: "beta", status: "success", stage: "testing" } as any,
161
+ { short_name: "gamma", status: "warning" } as any,
162
+ ],
163
+ },
164
+ ],
165
+ });
166
+ expect(container.querySelector(".library-count").textContent).toBe(
167
+ " (2 registered libraries)"
168
+ );
169
+ });
170
+
171
+ // ── Content filtering ─────────────────────────────────────────────────────
172
+
173
+ it("shows only registered (status=success) libraries in the expanded list", () => {
174
+ const { container } = renderServices({
175
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
176
+ libraryRegistrations: [
177
+ {
178
+ id: 1,
179
+ libraries: [
180
+ {
181
+ short_name: "alpha",
182
+ status: "success",
183
+ stage: "production",
184
+ } as any,
185
+ { short_name: "beta", status: "warning", stage: "testing" } as any,
186
+ { short_name: "gamma", status: "failure" } as any,
187
+ ],
188
+ },
189
+ ],
190
+ });
191
+ fireEvent.click(container.querySelector(".association-toggle"));
192
+
193
+ const items = container.querySelectorAll(".associated-items li");
194
+ expect(items).toHaveLength(1);
195
+ expect(items[0].textContent).toContain("Alpha Library");
196
+ });
197
+
198
+ // ── Stage suffix ──────────────────────────────────────────────────────────
199
+
200
+ it("shows the registration stage in the suffix", () => {
201
+ const { container } = renderServices({
202
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
203
+ libraryRegistrations: [
204
+ {
205
+ id: 1,
206
+ libraries: [
207
+ {
208
+ short_name: "alpha",
209
+ status: "success",
210
+ stage: "production",
211
+ } as any,
212
+ ],
213
+ },
214
+ ],
215
+ });
216
+ fireEvent.click(container.querySelector(".association-toggle"));
217
+
218
+ const item = container.querySelector(".associated-items li");
219
+ expect(item.textContent).toBe("Alpha Library - registered - production");
220
+ });
221
+
222
+ it("shows '- registered' without a stage when stage is absent", () => {
223
+ const { container } = renderServices({
224
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
225
+ libraryRegistrations: [
226
+ {
227
+ id: 1,
228
+ libraries: [{ short_name: "alpha", status: "success" } as any],
229
+ },
230
+ ],
231
+ });
232
+ fireEvent.click(container.querySelector(".association-toggle"));
233
+
234
+ const item = container.querySelector(".associated-items li");
235
+ expect(item.textContent).toBe("Alpha Library - registered");
236
+ });
237
+
238
+ // ── Sorting ───────────────────────────────────────────────────────────────
239
+
240
+ it("sorts registered libraries alphabetically by display name", () => {
241
+ const { container } = renderServices({
242
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
243
+ libraryRegistrations: [
244
+ {
245
+ id: 1,
246
+ libraries: [
247
+ {
248
+ short_name: "gamma",
249
+ status: "success",
250
+ stage: "production",
251
+ } as any,
252
+ { short_name: "alpha", status: "success", stage: "testing" } as any,
253
+ {
254
+ short_name: "beta",
255
+ status: "success",
256
+ stage: "production",
257
+ } as any,
258
+ ],
259
+ },
260
+ ],
261
+ });
262
+ fireEvent.click(container.querySelector(".association-toggle"));
263
+
264
+ const items = container.querySelectorAll(".associated-items li");
265
+ expect(items[0].textContent).toContain("Alpha Library");
266
+ expect(items[1].textContent).toContain("Beta Library");
267
+ expect(items[2].textContent).toContain("Gamma Library");
268
+ });
269
+
270
+ // ── Links ─────────────────────────────────────────────────────────────────
271
+
272
+ it("links the library name to its config page when a uuid is available", () => {
273
+ const { container } = renderServices({
274
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
275
+ libraryRegistrations: [
276
+ {
277
+ id: 1,
278
+ libraries: [
279
+ {
280
+ short_name: "alpha",
281
+ status: "success",
282
+ stage: "production",
283
+ } as any,
284
+ ],
285
+ },
286
+ ],
287
+ });
288
+ fireEvent.click(container.querySelector(".association-toggle"));
289
+
290
+ const link = container.querySelector<HTMLAnchorElement>(
291
+ ".associated-items a"
292
+ );
293
+ expect(link).not.toBeNull();
294
+ expect(link.textContent).toBe("Alpha Library");
295
+ expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-alpha");
296
+ // The suffix should not be inside the link.
297
+ expect(link.nextSibling.textContent).toBe(" - registered - production");
298
+ });
299
+
300
+ it("renders the library name as plain text when no uuid is available", () => {
301
+ const { container } = renderServices({
302
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
303
+ libraryRegistrations: [
304
+ {
305
+ id: 1,
306
+ libraries: [
307
+ { short_name: "delta", status: "success", stage: "testing" } as any,
308
+ ],
309
+ },
310
+ ],
311
+ });
312
+ fireEvent.click(container.querySelector(".association-toggle"));
313
+
314
+ expect(container.querySelector(".associated-items a")).toBeNull();
315
+ expect(container.querySelector(".associated-items li").textContent).toBe(
316
+ "Delta Library - registered - testing"
317
+ );
318
+ });
319
+
320
+ // ── Per-service isolation ─────────────────────────────────────────────────
321
+
322
+ it("shows each service's own registered libraries independently", () => {
323
+ const { container } = renderServices({
324
+ discovery_services: [
325
+ { id: 1, protocol: "p", name: "Service A" } as any,
326
+ { id: 2, protocol: "p", name: "Service B" } as any,
327
+ ],
328
+ libraryRegistrations: [
329
+ {
330
+ id: 1,
331
+ libraries: [
332
+ {
333
+ short_name: "alpha",
334
+ status: "success",
335
+ stage: "production",
336
+ } as any,
337
+ ],
338
+ },
339
+ {
340
+ id: 2,
341
+ libraries: [
342
+ { short_name: "beta", status: "success", stage: "testing" } as any,
343
+ {
344
+ short_name: "gamma",
345
+ status: "success",
346
+ stage: "production",
347
+ } as any,
348
+ ],
349
+ },
350
+ ],
351
+ });
352
+
353
+ expect(container.querySelector(".library-count").textContent).toBe(
354
+ " (1 registered library)"
355
+ );
356
+
357
+ const toggles = container.querySelectorAll<HTMLButtonElement>(
358
+ ".association-toggle"
359
+ );
360
+ expect(toggles).toHaveLength(2);
361
+ expect(
362
+ toggles[1].closest("li").querySelector(".library-count").textContent
363
+ ).toBe(" (2 registered libraries)");
364
+ });
365
+
366
+ // ── Expand all / Collapse all buttons ────────────────────────────────────
367
+
368
+ it("Expand all expands all services that have registered libraries", () => {
369
+ const { container } = renderServices({
370
+ discovery_services: [
371
+ { id: 1, protocol: "p", name: "Service A" } as any,
372
+ { id: 2, protocol: "p", name: "Service B" } as any,
373
+ { id: 3, protocol: "p", name: "Service C" } as any,
374
+ ],
375
+ libraryRegistrations: [
376
+ {
377
+ id: 1,
378
+ libraries: [
379
+ {
380
+ short_name: "alpha",
381
+ status: "success",
382
+ stage: "production",
383
+ } as any,
384
+ ],
385
+ },
386
+ {
387
+ id: 2,
388
+ libraries: [], // no registrations → disabled toggle
389
+ },
390
+ {
391
+ id: 3,
392
+ libraries: [
393
+ { short_name: "beta", status: "success", stage: "testing" } as any,
394
+ ],
395
+ },
396
+ ],
397
+ });
398
+
399
+ fireEvent.click(container.querySelector(".expand-all"));
400
+
401
+ // Services 1 and 3 have registered libraries; service 2 has none.
402
+ expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
403
+ });
404
+
405
+ it("shows no expand/collapse controls when libraryRegistrations has not yet loaded", () => {
406
+ const { container } = renderServices({
407
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
408
+ // libraryRegistrations omitted → getAssociatedEntries returns undefined
409
+ // for every item, so expandableItems() is empty and no controls render.
410
+ });
411
+ expect(container.querySelector(".expand-collapse-controls")).toBeNull();
412
+ });
413
+
414
+ it("shows no expand/collapse controls when no services have successfully registered libraries", () => {
415
+ const { container } = renderServices({
416
+ discovery_services: [
417
+ { id: 1, protocol: "p", name: "Service A" } as any,
418
+ { id: 2, protocol: "p", name: "Service B" } as any,
419
+ ],
420
+ libraryRegistrations: [
421
+ {
422
+ id: 1,
423
+ libraries: [{ short_name: "alpha", status: "warning" } as any],
424
+ },
425
+ {
426
+ id: 2,
427
+ libraries: [],
428
+ },
429
+ ],
430
+ });
431
+ expect(container.querySelector(".expand-collapse-controls")).toBeNull();
432
+ });
433
+
434
+ it("Collapse all collapses all expanded services", () => {
435
+ const { container } = renderServices({
436
+ discovery_services: [
437
+ { id: 1, protocol: "p", name: "Service A" } as any,
438
+ { id: 2, protocol: "p", name: "Service B" } as any,
439
+ ],
440
+ libraryRegistrations: [
441
+ {
442
+ id: 1,
443
+ libraries: [
444
+ {
445
+ short_name: "alpha",
446
+ status: "success",
447
+ stage: "production",
448
+ } as any,
449
+ ],
450
+ },
451
+ {
452
+ id: 2,
453
+ libraries: [
454
+ { short_name: "beta", status: "success", stage: "testing" } as any,
455
+ ],
456
+ },
457
+ ],
458
+ });
459
+
460
+ fireEvent.click(container.querySelector(".expand-all"));
461
+ expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
462
+
463
+ fireEvent.click(container.querySelector(".collapse-all"));
464
+ expect(container.querySelectorAll(".associated-items")).toHaveLength(0);
465
+ expect(
466
+ container.querySelector<HTMLButtonElement>(".collapse-all").disabled
467
+ ).toBe(true);
468
+ });
469
+
470
+ // ── Alt+click toggle-all ──────────────────────────────────────────────────
471
+
472
+ it("alt+click expands all services that have registered libraries", () => {
473
+ const { container } = renderServices({
474
+ discovery_services: [
475
+ { id: 1, protocol: "p", name: "Service A" } as any,
476
+ { id: 2, protocol: "p", name: "Service B" } as any,
477
+ { id: 3, protocol: "p", name: "Service C" } as any,
478
+ ],
479
+ libraryRegistrations: [
480
+ {
481
+ id: 1,
482
+ libraries: [
483
+ {
484
+ short_name: "alpha",
485
+ status: "success",
486
+ stage: "production",
487
+ } as any,
488
+ ],
489
+ },
490
+ {
491
+ id: 2,
492
+ libraries: [], // no registrations → disabled toggle
493
+ },
494
+ {
495
+ id: 3,
496
+ libraries: [
497
+ { short_name: "beta", status: "success", stage: "testing" } as any,
498
+ ],
499
+ },
500
+ ],
501
+ });
502
+
503
+ const toggles = container.querySelectorAll(".association-toggle");
504
+ fireEvent.click(toggles[0], { altKey: true });
505
+
506
+ // Services 1 and 3 have registered libraries; service 2 has none.
507
+ expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
508
+ });
509
+
510
+ it("alt+click collapses all when all services are already expanded", () => {
511
+ const { container } = renderServices({
512
+ discovery_services: [
513
+ { id: 1, protocol: "p", name: "Service A" } as any,
514
+ { id: 2, protocol: "p", name: "Service B" } as any,
515
+ ],
516
+ libraryRegistrations: [
517
+ {
518
+ id: 1,
519
+ libraries: [
520
+ {
521
+ short_name: "alpha",
522
+ status: "success",
523
+ stage: "production",
524
+ } as any,
525
+ ],
526
+ },
527
+ {
528
+ id: 2,
529
+ libraries: [
530
+ { short_name: "beta", status: "success", stage: "testing" } as any,
531
+ ],
532
+ },
533
+ ],
534
+ });
535
+ const toggles = container.querySelectorAll(".association-toggle");
536
+
537
+ // Expand all first.
538
+ fireEvent.click(toggles[0], { altKey: true });
539
+ expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
540
+
541
+ // Alt+click again should collapse all.
542
+ fireEvent.click(toggles[0], { altKey: true });
543
+ expect(container.querySelectorAll(".associated-items")).toHaveLength(0);
544
+ });
545
+ });