@thepalaceproject/circulation-admin 1.38.0 → 1.39.0-post.1
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/README.md +42 -0
- package/dist/circulation-admin.css +1 -1
- package/dist/circulation-admin.js +1 -1
- package/jest.config.js +1 -0
- package/package.json +4 -2
- package/scripts/syncPatronBlockingDocs.js +43 -0
- package/tests/jest/api/patronBlockingRules.test.ts +28 -6
- package/tests/jest/components/CollectionImportButton.test.tsx +145 -7
- 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
- package/tests/jest/components/PatronAuthServiceEditForm.test.tsx +39 -16
- package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +234 -46
- package/tests/jest/components/PatronBlockingRulesHelpModal.test.tsx +148 -0
- package/webpack.common.js +4 -0
package/jest.config.js
CHANGED
|
@@ -4,6 +4,7 @@ module.exports = {
|
|
|
4
4
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
|
5
5
|
"<rootDir>/tests/__mocks__/fileMock.js",
|
|
6
6
|
"\\.(css|less)$": "<rootDir>/tests/__mocks__/styleMock.js",
|
|
7
|
+
"\\.md$": "<rootDir>/tests/__mocks__/fileMock.js",
|
|
7
8
|
},
|
|
8
9
|
preset: "ts-jest",
|
|
9
10
|
testEnvironment: "jest-fixed-jsdom",
|
package/package.json
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"dev-server": "dotenv -c -- webpack serve --progress --hot --config webpack.dev-server.config",
|
|
33
33
|
"dev-test-axe": "TEST_AXE=true npm run dev",
|
|
34
34
|
"prod": "webpack --progress --config webpack.prod.config",
|
|
35
|
-
"build-docs": "typedoc --tsconfig tsconfig.json src"
|
|
35
|
+
"build-docs": "typedoc --tsconfig tsconfig.json src",
|
|
36
|
+
"sync-patron-blocking-docs": "node scripts/syncPatronBlockingDocs.js"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@nypl/dgx-svg-icons": "0.3.4",
|
|
@@ -113,6 +114,7 @@
|
|
|
113
114
|
"jsdom": "^20.0.3",
|
|
114
115
|
"json-loader": "^0.5.4",
|
|
115
116
|
"lint-staged": "^10.4.0",
|
|
117
|
+
"marked": "^17.0.6",
|
|
116
118
|
"mini-css-extract-plugin": "1.6.0",
|
|
117
119
|
"mocha": "^10.2.0",
|
|
118
120
|
"msw": "^2.7.3",
|
|
@@ -152,5 +154,5 @@
|
|
|
152
154
|
"*.{js,jsx,ts,tsx,css,md}": "prettier --write",
|
|
153
155
|
"*.{js,css,md}": "prettier --write"
|
|
154
156
|
},
|
|
155
|
-
"version": "1.
|
|
157
|
+
"version": "1.39.0-post.1"
|
|
156
158
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
3
|
+
/**
|
|
4
|
+
* Reads ../circulation/docs/FUNCTIONS.md, converts it to HTML, and writes
|
|
5
|
+
* src/content/patronBlockingFunctionsHtml.ts — a TypeScript module that
|
|
6
|
+
* exports the HTML as a string constant.
|
|
7
|
+
*
|
|
8
|
+
* Run via: npm run sync-patron-blocking-docs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const { marked } = require("marked");
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Main
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const srcMd = path.resolve(__dirname, "../../circulation/docs/FUNCTIONS.md");
|
|
21
|
+
const destTs = path.resolve(
|
|
22
|
+
__dirname,
|
|
23
|
+
"../src/content/patronBlockingFunctionsHtml.ts"
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(srcMd)) {
|
|
27
|
+
console.error(`Source not found: ${srcMd}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const markdown = fs.readFileSync(srcMd, "utf8");
|
|
32
|
+
const html = marked(markdown);
|
|
33
|
+
|
|
34
|
+
const tsContent = `// AUTO-GENERATED — do not edit by hand.
|
|
35
|
+
// Run \`npm run sync-patron-blocking-docs\` to regenerate from
|
|
36
|
+
// circulation/docs/FUNCTIONS.md.
|
|
37
|
+
const patronBlockingFunctionsHtml = ${JSON.stringify(html)};
|
|
38
|
+
export default patronBlockingFunctionsHtml;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
fs.mkdirSync(path.dirname(destTs), { recursive: true });
|
|
42
|
+
fs.writeFileSync(destTs, tsContent, "utf8");
|
|
43
|
+
console.log(`Written: ${destTs}`);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as fetchMock from "fetch-mock-jest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
validatePatronBlockingRuleExpression,
|
|
4
|
+
ValidationResult,
|
|
5
|
+
} from "../../../src/api/patronBlockingRules";
|
|
3
6
|
import { PatronBlockingRule } from "../../../src/interfaces";
|
|
4
7
|
|
|
5
8
|
const VALIDATE_URL = "/admin/patron_auth_service_validate_patron_blocking_rule";
|
|
@@ -21,7 +24,24 @@ describe("validatePatronBlockingRuleExpression", () => {
|
|
|
21
24
|
sampleRule,
|
|
22
25
|
"test-token"
|
|
23
26
|
);
|
|
24
|
-
expect(result).toBeNull();
|
|
27
|
+
expect(result.error).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns available_fields from a 200 response with a body", async () => {
|
|
31
|
+
fetchMock.post(VALIDATE_URL, {
|
|
32
|
+
status: 200,
|
|
33
|
+
body: { available_fields: { fines: 0, patron_identifier: "X123" } },
|
|
34
|
+
});
|
|
35
|
+
const result: ValidationResult = await validatePatronBlockingRuleExpression(
|
|
36
|
+
42,
|
|
37
|
+
sampleRule,
|
|
38
|
+
"test-token"
|
|
39
|
+
);
|
|
40
|
+
expect(result.error).toBeNull();
|
|
41
|
+
expect(result.availableFields).toEqual({
|
|
42
|
+
fines: 0,
|
|
43
|
+
patron_identifier: "X123",
|
|
44
|
+
});
|
|
25
45
|
});
|
|
26
46
|
|
|
27
47
|
it("returns the detail string from a 400 response", async () => {
|
|
@@ -34,7 +54,8 @@ describe("validatePatronBlockingRuleExpression", () => {
|
|
|
34
54
|
sampleRule,
|
|
35
55
|
"test-token"
|
|
36
56
|
);
|
|
37
|
-
expect(result).toBe("Unknown placeholder: {unknown_field}");
|
|
57
|
+
expect(result.error).toBe("Unknown placeholder: {unknown_field}");
|
|
58
|
+
expect(result.availableFields).toBeNull();
|
|
38
59
|
});
|
|
39
60
|
|
|
40
61
|
it("returns a fallback string when a 400 response body has no detail", async () => {
|
|
@@ -44,8 +65,9 @@ describe("validatePatronBlockingRuleExpression", () => {
|
|
|
44
65
|
sampleRule,
|
|
45
66
|
"test-token"
|
|
46
67
|
);
|
|
47
|
-
expect(result).not.toBeNull();
|
|
48
|
-
expect(typeof result).toBe("string");
|
|
68
|
+
expect(result.error).not.toBeNull();
|
|
69
|
+
expect(typeof result.error).toBe("string");
|
|
70
|
+
expect(result.availableFields).toBeNull();
|
|
49
71
|
});
|
|
50
72
|
|
|
51
73
|
it("sends the correct URL, method, and CSRF header", async () => {
|
|
@@ -73,6 +95,6 @@ describe("validatePatronBlockingRuleExpression", () => {
|
|
|
73
95
|
expect.objectContaining({ method: "POST" })
|
|
74
96
|
);
|
|
75
97
|
// Server would return an error detail in a real call; here it returns 200 (mocked)
|
|
76
|
-
expect(result).toBeNull();
|
|
98
|
+
expect(result.error).toBeNull();
|
|
77
99
|
});
|
|
78
100
|
});
|
|
@@ -79,7 +79,56 @@ describe("CollectionImportButton", () => {
|
|
|
79
79
|
screen.getByRole("button", { name: "Queue Import" })
|
|
80
80
|
).toBeInTheDocument();
|
|
81
81
|
expect(screen.getByRole("checkbox")).toBeInTheDocument();
|
|
82
|
-
expect(screen.
|
|
82
|
+
expect(screen.getByLabelText("Force full re-import")).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("shows compact summary by default; detailed docs are hidden", async () => {
|
|
86
|
+
const user = userEvent.setup();
|
|
87
|
+
renderButton();
|
|
88
|
+
await expandPanel(user);
|
|
89
|
+
expect(
|
|
90
|
+
screen.getByText(/queue import picks up new and changed items/i)
|
|
91
|
+
).toBeInTheDocument();
|
|
92
|
+
expect(
|
|
93
|
+
screen.getByText(/schedules a background import job/i)
|
|
94
|
+
).not.toBeVisible();
|
|
95
|
+
expect(
|
|
96
|
+
screen.getByText(/the import job re-processes every item/i)
|
|
97
|
+
).not.toBeVisible();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("clicking 'More details' reveals the detailed docs", async () => {
|
|
101
|
+
const user = userEvent.setup();
|
|
102
|
+
renderButton();
|
|
103
|
+
await expandPanel(user);
|
|
104
|
+
|
|
105
|
+
const details = screen.getByText("More details").closest("details");
|
|
106
|
+
expect(details).not.toHaveAttribute("open");
|
|
107
|
+
|
|
108
|
+
await user.click(screen.getByText("More details"));
|
|
109
|
+
|
|
110
|
+
expect(details).toHaveAttribute("open");
|
|
111
|
+
expect(
|
|
112
|
+
screen.getByText(/schedules a background import job/i)
|
|
113
|
+
).toBeVisible();
|
|
114
|
+
expect(
|
|
115
|
+
screen.getByText(/the import job re-processes every item/i)
|
|
116
|
+
).toBeVisible();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("clicking 'More details' again hides the detailed docs", async () => {
|
|
120
|
+
const user = userEvent.setup();
|
|
121
|
+
renderButton();
|
|
122
|
+
await expandPanel(user);
|
|
123
|
+
|
|
124
|
+
await user.click(screen.getByText("More details"));
|
|
125
|
+
expect(
|
|
126
|
+
screen.getByText(/schedules a background import job/i)
|
|
127
|
+
).toBeVisible();
|
|
128
|
+
|
|
129
|
+
await user.click(screen.getByText("More details"));
|
|
130
|
+
const details = screen.getByText("More details").closest("details");
|
|
131
|
+
expect(details).not.toHaveAttribute("open");
|
|
83
132
|
});
|
|
84
133
|
|
|
85
134
|
it("checkbox toggles force state", async () => {
|
|
@@ -94,6 +143,41 @@ describe("CollectionImportButton", () => {
|
|
|
94
143
|
expect(checkbox).not.toBeChecked();
|
|
95
144
|
});
|
|
96
145
|
|
|
146
|
+
it("button text changes to 'Queue Full Re-import' when force is checked", async () => {
|
|
147
|
+
const user = userEvent.setup();
|
|
148
|
+
renderButton();
|
|
149
|
+
await expandPanel(user);
|
|
150
|
+
|
|
151
|
+
expect(
|
|
152
|
+
screen.getByRole("button", { name: "Queue Import" })
|
|
153
|
+
).toBeInTheDocument();
|
|
154
|
+
|
|
155
|
+
await user.click(screen.getByRole("checkbox"));
|
|
156
|
+
|
|
157
|
+
expect(
|
|
158
|
+
screen.getByRole("button", { name: "Queue Full Re-import" })
|
|
159
|
+
).toBeInTheDocument();
|
|
160
|
+
expect(
|
|
161
|
+
screen.queryByRole("button", { name: "Queue Import" })
|
|
162
|
+
).not.toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("button uses force class when force is checked", async () => {
|
|
166
|
+
const user = userEvent.setup();
|
|
167
|
+
renderButton();
|
|
168
|
+
await expandPanel(user);
|
|
169
|
+
|
|
170
|
+
const button = screen.getByRole("button", { name: "Queue Import" });
|
|
171
|
+
expect(button).not.toHaveClass("force");
|
|
172
|
+
|
|
173
|
+
await user.click(screen.getByRole("checkbox"));
|
|
174
|
+
|
|
175
|
+
const forceButton = screen.getByRole("button", {
|
|
176
|
+
name: "Queue Full Re-import",
|
|
177
|
+
});
|
|
178
|
+
expect(forceButton).toHaveClass("force");
|
|
179
|
+
});
|
|
180
|
+
|
|
97
181
|
it("button triggers import with correct args (force=false)", async () => {
|
|
98
182
|
const user = userEvent.setup();
|
|
99
183
|
const { importCollection } = renderButton();
|
|
@@ -109,18 +193,39 @@ describe("CollectionImportButton", () => {
|
|
|
109
193
|
await expandPanel(user);
|
|
110
194
|
const checkbox = screen.getByRole("checkbox");
|
|
111
195
|
await user.click(checkbox);
|
|
112
|
-
const button = screen.getByRole("button", {
|
|
196
|
+
const button = screen.getByRole("button", {
|
|
197
|
+
name: "Queue Full Re-import",
|
|
198
|
+
});
|
|
113
199
|
await user.click(button);
|
|
114
200
|
expect(importCollection).toHaveBeenCalledWith(42, true);
|
|
115
201
|
});
|
|
116
202
|
|
|
117
|
-
it("shows success feedback
|
|
203
|
+
it("shows success feedback for regular import", async () => {
|
|
118
204
|
const user = userEvent.setup();
|
|
119
205
|
renderButton();
|
|
120
206
|
await expandPanel(user);
|
|
121
207
|
await user.click(screen.getByRole("button", { name: "Queue Import" }));
|
|
122
208
|
await waitFor(() => {
|
|
123
|
-
const feedback = screen.getByText(
|
|
209
|
+
const feedback = screen.getByText(
|
|
210
|
+
/import task queued\. new and updated items will appear/i
|
|
211
|
+
);
|
|
212
|
+
expect(feedback).toBeInTheDocument();
|
|
213
|
+
expect(feedback).toHaveClass("alert", "alert-success");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("shows success feedback for force re-import", async () => {
|
|
218
|
+
const user = userEvent.setup();
|
|
219
|
+
renderButton();
|
|
220
|
+
await expandPanel(user);
|
|
221
|
+
await user.click(screen.getByRole("checkbox"));
|
|
222
|
+
await user.click(
|
|
223
|
+
screen.getByRole("button", { name: "Queue Full Re-import" })
|
|
224
|
+
);
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
const feedback = screen.getByText(
|
|
227
|
+
/full re-import task queued\. all items will be re-processed/i
|
|
228
|
+
);
|
|
124
229
|
expect(feedback).toBeInTheDocument();
|
|
125
230
|
expect(feedback).toHaveClass("alert", "alert-success");
|
|
126
231
|
});
|
|
@@ -150,9 +255,13 @@ describe("CollectionImportButton", () => {
|
|
|
150
255
|
await user.click(checkbox);
|
|
151
256
|
expect(checkbox).toBeChecked();
|
|
152
257
|
|
|
153
|
-
await user.click(
|
|
258
|
+
await user.click(
|
|
259
|
+
screen.getByRole("button", { name: "Queue Full Re-import" })
|
|
260
|
+
);
|
|
154
261
|
await waitFor(() => {
|
|
155
|
-
expect(
|
|
262
|
+
expect(
|
|
263
|
+
screen.getByText(/full re-import task queued/i)
|
|
264
|
+
).toBeInTheDocument();
|
|
156
265
|
});
|
|
157
266
|
|
|
158
267
|
const nextCollection: CollectionData = {
|
|
@@ -171,7 +280,9 @@ describe("CollectionImportButton", () => {
|
|
|
171
280
|
|
|
172
281
|
await waitFor(() => {
|
|
173
282
|
expect(screen.getByRole("checkbox")).not.toBeChecked();
|
|
174
|
-
expect(
|
|
283
|
+
expect(
|
|
284
|
+
screen.queryByText(/full re-import task queued/i)
|
|
285
|
+
).not.toBeInTheDocument();
|
|
175
286
|
});
|
|
176
287
|
});
|
|
177
288
|
|
|
@@ -204,4 +315,31 @@ describe("CollectionImportButton", () => {
|
|
|
204
315
|
).toBeEnabled();
|
|
205
316
|
});
|
|
206
317
|
});
|
|
318
|
+
|
|
319
|
+
it("shows 'Queuing Full Re-import...' while importing with force", async () => {
|
|
320
|
+
const user = userEvent.setup();
|
|
321
|
+
let resolveImport: () => void;
|
|
322
|
+
const pendingImport = new Promise<void>((resolve) => {
|
|
323
|
+
resolveImport = resolve;
|
|
324
|
+
});
|
|
325
|
+
const mockImport = jest.fn().mockReturnValue(pendingImport);
|
|
326
|
+
renderButton({ importCollection: mockImport });
|
|
327
|
+
await expandPanel(user);
|
|
328
|
+
|
|
329
|
+
await user.click(screen.getByRole("checkbox"));
|
|
330
|
+
await user.click(
|
|
331
|
+
screen.getByRole("button", { name: "Queue Full Re-import" })
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(
|
|
335
|
+
screen.getByRole("button", { name: "Queuing Full Re-import..." })
|
|
336
|
+
).toBeDisabled();
|
|
337
|
+
|
|
338
|
+
resolveImport();
|
|
339
|
+
await waitFor(() => {
|
|
340
|
+
expect(
|
|
341
|
+
screen.getByRole("button", { name: "Queue Full Re-import" })
|
|
342
|
+
).toBeEnabled();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
207
345
|
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { fireEvent } from "@testing-library/react";
|
|
3
|
+
import { Collections } from "../../../src/components/Collections";
|
|
4
|
+
import renderWithContext from "../testUtils/renderWithContext";
|
|
5
|
+
import {
|
|
6
|
+
CollectionsData,
|
|
7
|
+
ConfigurationSettings,
|
|
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__/Collections-test.tsx`.
|
|
13
|
+
//
|
|
14
|
+
// Those tests should eventually be migrated here and
|
|
15
|
+
// adapted to the Jest/React Testing Library paradigm.
|
|
16
|
+
|
|
17
|
+
describe("Collections - associated 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 renderCollections = (data: Partial<CollectionsData>) =>
|
|
34
|
+
renderWithContext(
|
|
35
|
+
<Collections
|
|
36
|
+
data={
|
|
37
|
+
{
|
|
38
|
+
collections: [],
|
|
39
|
+
protocols: [],
|
|
40
|
+
allLibraries,
|
|
41
|
+
...data,
|
|
42
|
+
} as CollectionsData
|
|
43
|
+
}
|
|
44
|
+
fetchData={jest.fn()}
|
|
45
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
46
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
47
|
+
registerLibrary={jest.fn().mockResolvedValue(undefined)}
|
|
48
|
+
importCollection={jest.fn().mockResolvedValue(undefined)}
|
|
49
|
+
csrfToken="token"
|
|
50
|
+
isFetching={false}
|
|
51
|
+
/>,
|
|
52
|
+
sysAdminConfig
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// ── Toggle visibility ─────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
it("shows no toggle for a collection without a libraries field", () => {
|
|
58
|
+
const { container } = renderCollections({
|
|
59
|
+
collections: [{ id: 1, protocol: "p", name: "My Collection" } as any],
|
|
60
|
+
});
|
|
61
|
+
expect(container.querySelector(".association-toggle")).toBeNull();
|
|
62
|
+
expect(container.querySelector(".library-count")).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("shows a disabled toggle and 'no libraries' for a collection with an empty libraries array", () => {
|
|
66
|
+
const { container } = renderCollections({
|
|
67
|
+
collections: [
|
|
68
|
+
{ id: 1, protocol: "p", name: "My Collection", libraries: [] } as any,
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
const toggle = container.querySelector<HTMLButtonElement>(
|
|
72
|
+
".association-toggle"
|
|
73
|
+
);
|
|
74
|
+
expect(toggle).not.toBeNull();
|
|
75
|
+
expect(toggle.disabled).toBe(true);
|
|
76
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
77
|
+
" (no libraries)"
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("shows '1 library' for a collection with one associated library", () => {
|
|
82
|
+
const { container } = renderCollections({
|
|
83
|
+
collections: [
|
|
84
|
+
{
|
|
85
|
+
id: 1,
|
|
86
|
+
protocol: "p",
|
|
87
|
+
name: "My Collection",
|
|
88
|
+
libraries: [{ short_name: "alpha" }],
|
|
89
|
+
} as any,
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
const toggle = container.querySelector<HTMLButtonElement>(
|
|
93
|
+
".association-toggle"
|
|
94
|
+
);
|
|
95
|
+
expect(toggle.disabled).toBe(false);
|
|
96
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
97
|
+
" (1 library)"
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("shows 'N libraries' for a collection with multiple associated libraries", () => {
|
|
102
|
+
const { container } = renderCollections({
|
|
103
|
+
collections: [
|
|
104
|
+
{
|
|
105
|
+
id: 1,
|
|
106
|
+
protocol: "p",
|
|
107
|
+
name: "My Collection",
|
|
108
|
+
libraries: [
|
|
109
|
+
{ short_name: "alpha" },
|
|
110
|
+
{ short_name: "beta" },
|
|
111
|
+
{ short_name: "gamma" },
|
|
112
|
+
],
|
|
113
|
+
} as any,
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
expect(container.querySelector(".library-count").textContent).toBe(
|
|
117
|
+
" (3 libraries)"
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── Expand / collapse ─────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
it("expands the library list on toggle click and collapses on a second click", () => {
|
|
124
|
+
const { container } = renderCollections({
|
|
125
|
+
collections: [
|
|
126
|
+
{
|
|
127
|
+
id: 1,
|
|
128
|
+
protocol: "p",
|
|
129
|
+
name: "My Collection",
|
|
130
|
+
libraries: [{ short_name: "alpha" }, { short_name: "beta" }],
|
|
131
|
+
} as any,
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
const toggle = container.querySelector(".association-toggle");
|
|
135
|
+
expect(container.querySelector(".associated-items")).toBeNull();
|
|
136
|
+
fireEvent.click(toggle);
|
|
137
|
+
expect(container.querySelector(".associated-items")).not.toBeNull();
|
|
138
|
+
fireEvent.click(toggle);
|
|
139
|
+
expect(container.querySelector(".associated-items")).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── allLibraries injection from mapStateToProps ───────────────────────────
|
|
143
|
+
|
|
144
|
+
it("resolves library display names from allLibraries", () => {
|
|
145
|
+
const { container } = renderCollections({
|
|
146
|
+
collections: [
|
|
147
|
+
{
|
|
148
|
+
id: 1,
|
|
149
|
+
protocol: "p",
|
|
150
|
+
name: "My Collection",
|
|
151
|
+
libraries: [{ short_name: "alpha" }, { short_name: "beta" }],
|
|
152
|
+
} as any,
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
156
|
+
|
|
157
|
+
const items = container.querySelectorAll(".associated-items li");
|
|
158
|
+
// Sorted alphabetically: Alpha, Beta
|
|
159
|
+
expect(items[0].textContent).toBe("Alpha Library");
|
|
160
|
+
expect(items[1].textContent).toBe("Beta Library");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("links a library name to its config page when a uuid is available", () => {
|
|
164
|
+
const { container } = renderCollections({
|
|
165
|
+
collections: [
|
|
166
|
+
{
|
|
167
|
+
id: 1,
|
|
168
|
+
protocol: "p",
|
|
169
|
+
name: "My Collection",
|
|
170
|
+
libraries: [{ short_name: "alpha" }],
|
|
171
|
+
} as any,
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
175
|
+
|
|
176
|
+
const link = container.querySelector<HTMLAnchorElement>(
|
|
177
|
+
".associated-items a"
|
|
178
|
+
);
|
|
179
|
+
expect(link).not.toBeNull();
|
|
180
|
+
expect(link.textContent).toBe("Alpha Library");
|
|
181
|
+
expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-alpha");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("renders a library without a uuid as plain text", () => {
|
|
185
|
+
const { container } = renderCollections({
|
|
186
|
+
collections: [
|
|
187
|
+
{
|
|
188
|
+
id: 1,
|
|
189
|
+
protocol: "p",
|
|
190
|
+
name: "My Collection",
|
|
191
|
+
libraries: [{ short_name: "delta" }],
|
|
192
|
+
} as any,
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
196
|
+
|
|
197
|
+
expect(container.querySelector(".associated-items a")).toBeNull();
|
|
198
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
199
|
+
"Delta Library"
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("falls back to short_name when the library is not in allLibraries", () => {
|
|
204
|
+
const { container } = renderCollections({
|
|
205
|
+
collections: [
|
|
206
|
+
{
|
|
207
|
+
id: 1,
|
|
208
|
+
protocol: "p",
|
|
209
|
+
name: "My Collection",
|
|
210
|
+
libraries: [{ short_name: "unknown" }],
|
|
211
|
+
} as any,
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
fireEvent.click(container.querySelector(".association-toggle"));
|
|
215
|
+
|
|
216
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
217
|
+
"unknown"
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
});
|