@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.
- package/dist/circulation-admin.css +1 -1
- package/dist/circulation-admin.js +1 -1
- package/package.json +2 -2
- package/tests/jest/components/Collections.test.tsx +220 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +545 -0
- package/tests/jest/components/EditableConfigList.test.tsx +399 -0
- package/tests/jest/components/IndividualAdmins.test.tsx +390 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { fireEvent } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
EditableConfigList,
|
|
5
|
+
EditFormProps,
|
|
6
|
+
} from "../../../src/components/EditableConfigList";
|
|
7
|
+
import renderWithContext from "../testUtils/renderWithContext";
|
|
8
|
+
import { ConfigurationSettings } 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__/EditableConfigList-test.tsx`.
|
|
13
|
+
//
|
|
14
|
+
// Those tests should eventually be migrated here and
|
|
15
|
+
// adapted to the Jest/React Testing Library paradigm.
|
|
16
|
+
|
|
17
|
+
describe("EditableConfigList - library association disclosure", () => {
|
|
18
|
+
// ── Test doubles ──────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
interface ServiceItem {
|
|
21
|
+
id: number;
|
|
22
|
+
name: string;
|
|
23
|
+
libraries?: Array<{ short_name: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ServicesData {
|
|
27
|
+
services: ServiceItem[];
|
|
28
|
+
allLibraries?: Array<{ short_name: string; name?: string; uuid?: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class TestEditForm extends React.Component<
|
|
32
|
+
EditFormProps<ServicesData, ServiceItem>
|
|
33
|
+
> {
|
|
34
|
+
render() {
|
|
35
|
+
return <div />;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class TestServiceList extends EditableConfigList<ServicesData, ServiceItem> {
|
|
40
|
+
EditForm = TestEditForm;
|
|
41
|
+
listDataKey = "services";
|
|
42
|
+
itemTypeName = "service";
|
|
43
|
+
urlBase = "/admin/services/";
|
|
44
|
+
identifierKey = "id";
|
|
45
|
+
labelKey = "name";
|
|
46
|
+
canCreate() {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
canDelete() {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Shared fixtures ───────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const allLibraries = [
|
|
57
|
+
{ short_name: "gamma", name: "Gamma Library", uuid: "uuid-gamma" },
|
|
58
|
+
{ short_name: "alpha", name: "Alpha Library", uuid: "uuid-alpha" },
|
|
59
|
+
{ short_name: "beta", name: "Beta Library", uuid: "uuid-beta" },
|
|
60
|
+
{ short_name: "delta", name: "Delta Library" }, // no uuid
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const config: Partial<ConfigurationSettings> = {
|
|
64
|
+
csrfToken: "",
|
|
65
|
+
featureFlags: defaultFeatureFlags,
|
|
66
|
+
roles: [{ role: "system" }],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const renderList = (items: ServiceItem[]) =>
|
|
70
|
+
renderWithContext(
|
|
71
|
+
<TestServiceList
|
|
72
|
+
data={{ services: items, allLibraries }}
|
|
73
|
+
fetchData={jest.fn()}
|
|
74
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
75
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
76
|
+
csrfToken="token"
|
|
77
|
+
isFetching={false}
|
|
78
|
+
/>,
|
|
79
|
+
config
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// ── Toggle visibility ─────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
it("shows no toggle for an item without a libraries field", () => {
|
|
85
|
+
const { container } = renderList([{ id: 1, name: "Service A" }]);
|
|
86
|
+
expect(container.querySelector(".association-toggle")).toBeNull();
|
|
87
|
+
expect(container.querySelector(".library-count")).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("shows a disabled toggle and 'no libraries' for an item with an empty libraries array", () => {
|
|
91
|
+
const { container } = renderList([
|
|
92
|
+
{ id: 1, name: "Service A", libraries: [] },
|
|
93
|
+
]);
|
|
94
|
+
const toggle = container.querySelector<HTMLButtonElement>(
|
|
95
|
+
".association-toggle"
|
|
96
|
+
);
|
|
97
|
+
expect(toggle).not.toBeNull();
|
|
98
|
+
expect(toggle.disabled).toBe(true);
|
|
99
|
+
// aria-expanded is omitted on a permanently-disabled toggle (it can never change state).
|
|
100
|
+
expect(toggle.getAttribute("aria-expanded")).toBeNull();
|
|
101
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
102
|
+
" (no libraries)"
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("shows an enabled toggle and '1 library' for an item with one library", () => {
|
|
107
|
+
const { container } = renderList([
|
|
108
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "alpha" }] },
|
|
109
|
+
]);
|
|
110
|
+
const toggle = container.querySelector<HTMLButtonElement>(
|
|
111
|
+
".association-toggle"
|
|
112
|
+
);
|
|
113
|
+
expect(toggle.disabled).toBe(false);
|
|
114
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
115
|
+
" (1 library)"
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("shows 'N libraries' for an item with multiple libraries", () => {
|
|
120
|
+
const { container } = renderList([
|
|
121
|
+
{
|
|
122
|
+
id: 1,
|
|
123
|
+
name: "Service A",
|
|
124
|
+
libraries: [
|
|
125
|
+
{ short_name: "alpha" },
|
|
126
|
+
{ short_name: "beta" },
|
|
127
|
+
{ short_name: "gamma" },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
132
|
+
" (3 libraries)"
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── Expand / collapse ─────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
it("expands the library list on toggle click and collapses on a second click", () => {
|
|
139
|
+
const { container } = renderList([
|
|
140
|
+
{
|
|
141
|
+
id: 1,
|
|
142
|
+
name: "Service A",
|
|
143
|
+
libraries: [{ short_name: "alpha" }, { short_name: "beta" }],
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
const toggle = container.querySelector(".association-toggle");
|
|
147
|
+
|
|
148
|
+
expect(container.querySelector(".associated-items")).toBeNull();
|
|
149
|
+
fireEvent.click(toggle);
|
|
150
|
+
expect(container.querySelector(".associated-items")).not.toBeNull();
|
|
151
|
+
fireEvent.click(toggle);
|
|
152
|
+
expect(container.querySelector(".associated-items")).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("sets aria-expanded correctly as the list is toggled", () => {
|
|
156
|
+
const { container } = renderList([
|
|
157
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "alpha" }] },
|
|
158
|
+
]);
|
|
159
|
+
const toggle = container.querySelector(".association-toggle");
|
|
160
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
|
161
|
+
fireEvent.click(toggle);
|
|
162
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── Expand all / Collapse all buttons ────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
it("shows no expand/collapse controls when no items have libraries", () => {
|
|
168
|
+
const { container } = renderList([
|
|
169
|
+
{ id: 1, name: "A", libraries: [] },
|
|
170
|
+
{ id: 2, name: "B" },
|
|
171
|
+
]);
|
|
172
|
+
expect(container.querySelector(".expand-collapse-controls")).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("shows expand/collapse controls when at least one item has libraries", () => {
|
|
176
|
+
const { container } = renderList([
|
|
177
|
+
{ id: 1, name: "A", libraries: [{ short_name: "alpha" }] },
|
|
178
|
+
]);
|
|
179
|
+
const controlSets = container.querySelectorAll(".expand-collapse-controls");
|
|
180
|
+
// One set above the list and one below (visual duplicate).
|
|
181
|
+
expect(controlSets).toHaveLength(2);
|
|
182
|
+
// Top set: functional, in tab order, not hidden from accessibility tree.
|
|
183
|
+
const topSet = controlSets[0];
|
|
184
|
+
expect(topSet.closest("[aria-hidden]")).toBeNull();
|
|
185
|
+
expect(
|
|
186
|
+
topSet.querySelector<HTMLButtonElement>(".expand-all").tabIndex
|
|
187
|
+
).not.toBe(-1);
|
|
188
|
+
expect(
|
|
189
|
+
topSet.querySelector<HTMLButtonElement>(".expand-all").disabled
|
|
190
|
+
).toBe(false);
|
|
191
|
+
expect(
|
|
192
|
+
topSet.querySelector<HTMLButtonElement>(".collapse-all").disabled
|
|
193
|
+
).toBe(true);
|
|
194
|
+
// Bottom set: hidden from accessibility tree and removed from tab order.
|
|
195
|
+
const bottomSet = controlSets[1];
|
|
196
|
+
expect(bottomSet.closest("[aria-hidden='true']")).not.toBeNull();
|
|
197
|
+
expect(
|
|
198
|
+
bottomSet.querySelector<HTMLButtonElement>(".expand-all").tabIndex
|
|
199
|
+
).toBe(-1);
|
|
200
|
+
expect(
|
|
201
|
+
bottomSet.querySelector<HTMLButtonElement>(".collapse-all").tabIndex
|
|
202
|
+
).toBe(-1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("Expand all expands all items that have libraries", () => {
|
|
206
|
+
const { container } = renderList([
|
|
207
|
+
{ id: 1, name: "A", libraries: [{ short_name: "alpha" }] },
|
|
208
|
+
{ id: 2, name: "B", libraries: [] }, // no libraries → toggle disabled
|
|
209
|
+
{ id: 3, name: "C", libraries: [{ short_name: "beta" }] },
|
|
210
|
+
{ id: 4, name: "D" }, // no libraries field → no toggle
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
fireEvent.click(container.querySelector(".expand-all"));
|
|
214
|
+
|
|
215
|
+
const lists = container.querySelectorAll(".associated-items");
|
|
216
|
+
// Items 1 and 3 have libraries; item 2 is empty so no list shown even when expanded.
|
|
217
|
+
expect(lists).toHaveLength(2);
|
|
218
|
+
expect(
|
|
219
|
+
container.querySelector<HTMLButtonElement>(".expand-all").disabled
|
|
220
|
+
).toBe(true);
|
|
221
|
+
expect(
|
|
222
|
+
container.querySelector<HTMLButtonElement>(".collapse-all").disabled
|
|
223
|
+
).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("Collapse all collapses all expanded items", () => {
|
|
227
|
+
const { container } = renderList([
|
|
228
|
+
{ id: 1, name: "A", libraries: [{ short_name: "alpha" }] },
|
|
229
|
+
{ id: 2, name: "B", libraries: [{ short_name: "beta" }] },
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
fireEvent.click(container.querySelector(".expand-all"));
|
|
233
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
|
|
234
|
+
|
|
235
|
+
fireEvent.click(container.querySelector(".collapse-all"));
|
|
236
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(0);
|
|
237
|
+
expect(
|
|
238
|
+
container.querySelector<HTMLButtonElement>(".collapse-all").disabled
|
|
239
|
+
).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── Alt+click toggle-all ──────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
it("alt+click on any toggle expands all items that have libraries", () => {
|
|
245
|
+
const { container } = renderList([
|
|
246
|
+
{ id: 1, name: "A", libraries: [{ short_name: "alpha" }] },
|
|
247
|
+
{ id: 2, name: "B", libraries: [] }, // no libraries → toggle disabled
|
|
248
|
+
{ id: 3, name: "C", libraries: [{ short_name: "beta" }] },
|
|
249
|
+
{ id: 4, name: "D" }, // no libraries field → no toggle
|
|
250
|
+
]);
|
|
251
|
+
const toggles = container.querySelectorAll(".association-toggle");
|
|
252
|
+
// Items 1, 2 and 3 have a libraries array → 3 toggles; item 4 has none.
|
|
253
|
+
expect(toggles).toHaveLength(3);
|
|
254
|
+
|
|
255
|
+
fireEvent.click(toggles[0], { altKey: true });
|
|
256
|
+
|
|
257
|
+
const lists = container.querySelectorAll(".associated-items");
|
|
258
|
+
// Items 1 and 3 have libraries; item 2 is empty so no list shown even when "expanded".
|
|
259
|
+
expect(lists).toHaveLength(2);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("alt+click collapses all items when all expandable items are already expanded", () => {
|
|
263
|
+
const { container } = renderList([
|
|
264
|
+
{ id: 1, name: "A", libraries: [{ short_name: "alpha" }] },
|
|
265
|
+
{ id: 2, name: "B", libraries: [{ short_name: "beta" }] },
|
|
266
|
+
]);
|
|
267
|
+
const toggles = container.querySelectorAll(".association-toggle");
|
|
268
|
+
|
|
269
|
+
// Expand all first.
|
|
270
|
+
fireEvent.click(toggles[0], { altKey: true });
|
|
271
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(2);
|
|
272
|
+
|
|
273
|
+
// Alt+click again should collapse all.
|
|
274
|
+
fireEvent.click(toggles[0], { altKey: true });
|
|
275
|
+
expect(container.querySelectorAll(".associated-items")).toHaveLength(0);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ── Sorted library list ───────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
it("renders associated libraries sorted alphabetically by display name", () => {
|
|
281
|
+
const { container } = renderList([
|
|
282
|
+
{
|
|
283
|
+
id: 1,
|
|
284
|
+
name: "Service A",
|
|
285
|
+
libraries: [
|
|
286
|
+
{ short_name: "gamma" },
|
|
287
|
+
{ short_name: "alpha" },
|
|
288
|
+
{ short_name: "beta" },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
293
|
+
|
|
294
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
295
|
+
expect(items).toHaveLength(3);
|
|
296
|
+
expect(items[0].textContent).toBe("Alpha Library");
|
|
297
|
+
expect(items[1].textContent).toBe("Beta Library");
|
|
298
|
+
expect(items[2].textContent).toBe("Gamma Library");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("renders a library with a uuid as a link to its config page", () => {
|
|
302
|
+
const { container } = renderList([
|
|
303
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "alpha" }] },
|
|
304
|
+
]);
|
|
305
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
306
|
+
|
|
307
|
+
const link = container.querySelector<HTMLAnchorElement>(
|
|
308
|
+
".associated-items a"
|
|
309
|
+
);
|
|
310
|
+
expect(link).not.toBeNull();
|
|
311
|
+
expect(link.textContent).toBe("Alpha Library");
|
|
312
|
+
expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-alpha");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("renders a library without a uuid as plain text (no link)", () => {
|
|
316
|
+
const { container } = renderList([
|
|
317
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "delta" }] },
|
|
318
|
+
]);
|
|
319
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
320
|
+
|
|
321
|
+
expect(container.querySelector(".associated-items a")).toBeNull();
|
|
322
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
323
|
+
"Delta Library"
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("falls back to short_name when no display name is available in allLibraries", () => {
|
|
328
|
+
const { container } = renderList([
|
|
329
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "unknown" }] },
|
|
330
|
+
]);
|
|
331
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
332
|
+
|
|
333
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
334
|
+
"unknown"
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("renders multiple libraries that are not in allLibraries, each falling back to its short_name", () => {
|
|
339
|
+
// Neither library is in allLibraries, so both fall back to their short_name
|
|
340
|
+
// as the display label. Distinct short_names → distinct labels → no key collision.
|
|
341
|
+
const { container } = renderList([
|
|
342
|
+
{
|
|
343
|
+
id: 1,
|
|
344
|
+
name: "Service A",
|
|
345
|
+
libraries: [{ short_name: "dup-a" }, { short_name: "dup-b" }],
|
|
346
|
+
},
|
|
347
|
+
]);
|
|
348
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
349
|
+
|
|
350
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
351
|
+
expect(items).toHaveLength(2);
|
|
352
|
+
expect(items[0].textContent).toBe("dup-a");
|
|
353
|
+
expect(items[1].textContent).toBe("dup-b");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("renderAssociatedLibraries", () => {
|
|
357
|
+
it("renders no associated-items panel when the item has no libraries property", () => {
|
|
358
|
+
const { container } = renderList([{ id: 1, name: "A" }]);
|
|
359
|
+
expect(container.querySelector(".associated-items")).toBeNull();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("renders no associated-items panel when the item has an empty libraries array", () => {
|
|
363
|
+
const { container } = renderList([{ id: 1, name: "A", libraries: [] }]);
|
|
364
|
+
expect(container.querySelector(".associated-items")).toBeNull();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("renders library names resolved from allLibraries on expand", () => {
|
|
368
|
+
const { container } = renderList([
|
|
369
|
+
{
|
|
370
|
+
id: 1,
|
|
371
|
+
name: "Service A",
|
|
372
|
+
libraries: [{ short_name: "alpha" }, { short_name: "beta" }],
|
|
373
|
+
},
|
|
374
|
+
]);
|
|
375
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
376
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
377
|
+
expect(items).toHaveLength(2);
|
|
378
|
+
// Sorted alphabetically by resolved display name.
|
|
379
|
+
expect(items[0].textContent).toBe("Alpha Library");
|
|
380
|
+
expect(items[1].textContent).toBe("Beta Library");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("falls back to short_name when allLibraries is absent from the data", () => {
|
|
384
|
+
const { container } = renderWithContext(
|
|
385
|
+
<TestServiceList
|
|
386
|
+
data={{ services: [{ id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] }] }}
|
|
387
|
+
fetchData={jest.fn()}
|
|
388
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
389
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
390
|
+
csrfToken="token"
|
|
391
|
+
isFetching={false}
|
|
392
|
+
/>,
|
|
393
|
+
config
|
|
394
|
+
);
|
|
395
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
396
|
+
expect(container.querySelector(".associated-items li").textContent).toBe("nypl");
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|