@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.
@@ -0,0 +1,354 @@
1
+ import * as React from "react";
2
+ import { installFormDataShim } from "../testUtils/formDataShim";
3
+ import { render } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+
6
+ import LibraryRegistration from "../../../src/components/LibraryRegistration";
7
+ // The reusable-components `Form` builds `new FormData(formElement)` on
8
+ // submit, which the unit jsdom env's undici FormData rejects; install the
9
+ // shared shim that reads the form's successful controls.
10
+ installFormDataShim();
11
+ const protocol = "protocol 1";
12
+ const serviceData = { id: 1, protocol };
13
+
14
+ const makeProtocols = (overrides: Record<string, unknown> = {}) => [
15
+ {
16
+ name: protocol,
17
+ label: "protocol 1 label",
18
+ supports_registration: true,
19
+ supports_staging: true,
20
+ settings: [],
21
+ library_settings: [],
22
+ ...overrides,
23
+ },
24
+ ];
25
+
26
+ const allLibraries = [
27
+ { short_name: "nypl", name: "New York Public Library", uuid: "1" },
28
+ { short_name: "bpl", name: "Brooklyn Public Library", uuid: "2" },
29
+ { short_name: "qpl", name: "Queens Public Library", uuid: "3" },
30
+ ];
31
+
32
+ const libraryRegistrations = [
33
+ {
34
+ id: 1,
35
+ libraries: [
36
+ { short_name: "nypl", status: "success", stage: "production" },
37
+ { short_name: "bpl", status: "failure", stage: "testing" },
38
+ ],
39
+ },
40
+ ];
41
+
42
+ const makeData = (overrides: Record<string, unknown> = {}) => ({
43
+ discovery_services: [serviceData],
44
+ protocols: makeProtocols(),
45
+ allLibraries,
46
+ libraryRegistrations,
47
+ ...overrides,
48
+ });
49
+
50
+ const renderReg = ({
51
+ data,
52
+ registerLibrary,
53
+ ...props
54
+ }: Record<string, any> = {}) => {
55
+ const register = registerLibrary ?? jest.fn();
56
+ const result = render(
57
+ <LibraryRegistration
58
+ disabled={false}
59
+ data={(data ?? makeData()) as any}
60
+ save={jest.fn()}
61
+ urlBase="url base"
62
+ listDataKey="discovery_services"
63
+ registerLibrary={register}
64
+ protocol={protocol}
65
+ {...props}
66
+ />
67
+ );
68
+ return { ...result, registerLibrary: register };
69
+ };
70
+
71
+ const libraries = (container: HTMLElement) =>
72
+ Array.from(
73
+ container.querySelectorAll<HTMLElement>(
74
+ ".service-with-registrations-library"
75
+ )
76
+ );
77
+ const registrationInfos = (container: HTMLElement) =>
78
+ Array.from(
79
+ container.querySelectorAll<HTMLElement>(".library-registration-info")
80
+ );
81
+
82
+ describe("LibraryRegistration", () => {
83
+ describe("rendering", () => {
84
+ it("doesn't render libraries in create form", () => {
85
+ // No `item` prop => nothing rendered.
86
+ const { container } = renderReg();
87
+ expect(libraries(container)).toHaveLength(0);
88
+ });
89
+
90
+ it("doesn't render libraries in edit form if protocol doesn't support registration", () => {
91
+ const { container } = renderReg({
92
+ item: serviceData,
93
+ data: makeData({
94
+ protocols: makeProtocols({ supports_registration: false }),
95
+ }),
96
+ });
97
+ expect(libraries(container)).toHaveLength(0);
98
+ });
99
+
100
+ it("treats a protocol that is absent from the list as unsupported", () => {
101
+ // protocolSupportsType falls through to `false` when the selected protocol
102
+ // matches no entry in data.protocols, so no registration UI renders.
103
+ const { container } = renderReg({
104
+ item: serviceData,
105
+ protocol: "protocol-not-in-the-list",
106
+ });
107
+ expect(libraries(container)).toHaveLength(0);
108
+ });
109
+
110
+ it("renders all libraries in edit form, with registration status", () => {
111
+ const { container } = renderReg({ item: serviceData });
112
+ const libs = libraries(container);
113
+ expect(libs).toHaveLength(3);
114
+
115
+ expect(libs[0]).toHaveTextContent("New York Public Library");
116
+ expect(libs[0]).toHaveTextContent("Registered");
117
+ expect(libs[1]).toHaveTextContent("Brooklyn Public Library");
118
+ expect(libs[1]).toHaveTextContent("Registration failed");
119
+ expect(libs[2]).toHaveTextContent("Queens Public Library");
120
+ expect(libs[2]).toHaveTextContent("Not registered");
121
+ });
122
+
123
+ it("provides links to the libraries' edit forms", () => {
124
+ const { container } = renderReg({ item: serviceData });
125
+ const libs = libraries(container);
126
+ expect(libs).toHaveLength(3);
127
+
128
+ expect(libs[0].querySelector("a")).toHaveAttribute(
129
+ "href",
130
+ "/admin/web/config/libraries/edit/1"
131
+ );
132
+ expect(libs[1].querySelector("a")).toHaveAttribute(
133
+ "href",
134
+ "/admin/web/config/libraries/edit/2"
135
+ );
136
+ expect(libs[2].querySelector("a")).toHaveAttribute(
137
+ "href",
138
+ "/admin/web/config/libraries/edit/3"
139
+ );
140
+ });
141
+
142
+ it("should not render the staging dropdown if it is not supported", () => {
143
+ const { container } = renderReg({
144
+ item: serviceData,
145
+ data: makeData({
146
+ protocols: makeProtocols({ supports_staging: false }),
147
+ }),
148
+ });
149
+ const infos = registrationInfos(container);
150
+ expect(infos).toHaveLength(3);
151
+
152
+ // No current-stage section and no stage dropdown for any library.
153
+ infos.forEach((info) => {
154
+ expect(info.querySelector(".current-stage")).not.toBeInTheDocument();
155
+ expect(info.querySelector("select")).not.toBeInTheDocument();
156
+ });
157
+
158
+ // Registration status and button still appear. The first span in each
159
+ // info is the status span.
160
+ const [nypl, bpl, qpl] = infos;
161
+ expect(nypl.querySelector("span")).toHaveTextContent("Registered");
162
+ expect(nypl.querySelector("span")).toHaveClass("bg-success");
163
+ expect(bpl.querySelector("span")).toHaveTextContent(
164
+ "Registration failed"
165
+ );
166
+ expect(bpl.querySelector("span")).toHaveClass("bg-danger");
167
+ expect(qpl.querySelector("span")).toHaveTextContent("Not registered");
168
+ expect(qpl.querySelector("span")).toHaveClass("bg-warning");
169
+
170
+ expect(nypl.querySelector("button")).toHaveTextContent(
171
+ "Update registration"
172
+ );
173
+ expect(bpl.querySelector("button")).toHaveTextContent(
174
+ "Retry registration"
175
+ );
176
+ expect(qpl.querySelector("button")).toHaveTextContent("Register");
177
+ });
178
+
179
+ it("should render the current registration stage", () => {
180
+ // An additional library shows the current stage when it is in the
181
+ // "testing" stage and its registration was successful.
182
+ const libraryRegistrationsWBoston = [
183
+ {
184
+ id: 1,
185
+ libraries: [
186
+ { short_name: "nypl", status: "success", stage: "production" },
187
+ { short_name: "bpl", status: "failure", stage: "testing" },
188
+ { short_name: "boston", status: "success", stage: "testing" },
189
+ ],
190
+ },
191
+ ];
192
+ const allLibrariesWBoston = [
193
+ ...allLibraries,
194
+ { short_name: "boston", name: "Boston Public Library", uuid: "4" },
195
+ ];
196
+ const { container } = renderReg({
197
+ item: serviceData,
198
+ data: makeData({
199
+ allLibraries: allLibrariesWBoston,
200
+ libraryRegistrations: libraryRegistrationsWBoston,
201
+ }),
202
+ });
203
+ const [nypl, bpl, qpl, boston] = registrationInfos(container);
204
+
205
+ expect(nypl.querySelector(".current-stage span")).toHaveTextContent(
206
+ "Current Stage: production"
207
+ );
208
+ expect(bpl.querySelector(".current-stage span")).toHaveTextContent(
209
+ "No current stage"
210
+ );
211
+ expect(qpl.querySelector(".current-stage span")).toHaveTextContent(
212
+ "No current stage"
213
+ );
214
+ expect(boston.querySelector(".current-stage span")).toHaveTextContent(
215
+ "Current Stage: testing"
216
+ );
217
+ });
218
+
219
+ it("should render a registration stage type dropdown with two options", () => {
220
+ const { container } = renderReg({ item: serviceData });
221
+ const bpl = registrationInfos(container)[1];
222
+ const options = bpl.querySelectorAll("option");
223
+ expect(options).toHaveLength(2);
224
+ expect(options[0]).toHaveTextContent("Testing");
225
+ expect(options[1]).toHaveTextContent("Production");
226
+ });
227
+
228
+ it("should render a registration stage select if the library is not in production", () => {
229
+ const { container } = renderReg({ item: serviceData });
230
+ const [nypl, bpl, qpl] = registrationInfos(container);
231
+
232
+ // Once a library is registered in production, the dropdown is removed.
233
+ expect(nypl.querySelectorAll("select")).toHaveLength(0);
234
+ expect(bpl.querySelectorAll("select")).toHaveLength(1);
235
+ expect(qpl.querySelectorAll("select")).toHaveLength(1);
236
+ });
237
+
238
+ it("should render a form", () => {
239
+ // The status of each library determines its button text.
240
+ const { container } = renderReg({ item: serviceData });
241
+ const libs = libraries(container);
242
+
243
+ expect(libs[0]).toHaveTextContent("New York Public Library");
244
+ expect(libs[0].querySelector("button")).toHaveTextContent(
245
+ "Update registration"
246
+ );
247
+ expect(libs[1]).toHaveTextContent("Brooklyn Public Library");
248
+ expect(libs[1].querySelector("button")).toHaveTextContent(
249
+ "Retry registration"
250
+ );
251
+ expect(libs[2]).toHaveTextContent("Queens Public Library");
252
+ expect(libs[2].querySelector("button")).toHaveTextContent("Register");
253
+ });
254
+
255
+ // Each form's `checked` prop is derived from the library's status. It only
256
+ // reaches the DOM when the registration carries terms of service, since
257
+ // that is what makes LibraryRegistrationForm render the checkbox at all.
258
+ //
259
+ // Each case renders a single library, because libraryRegistrationItem
260
+ // resolves the registration data as `libraryRegistrations[idx]` — indexing
261
+ // the per-service array by the library's position — so only a library at
262
+ // index 0 finds the terms.
263
+ const renderWithTerms = (library, status: string) =>
264
+ renderReg({
265
+ item: serviceData,
266
+ data: makeData({
267
+ allLibraries: [library],
268
+ libraryRegistrations: [
269
+ {
270
+ id: 1,
271
+ terms_of_service_html: "<p>Terms</p>",
272
+ libraries: [{ short_name: library.short_name, status }],
273
+ },
274
+ ],
275
+ }),
276
+ });
277
+
278
+ const termsCheckbox = (container: HTMLElement) =>
279
+ container.querySelector<HTMLInputElement>('input[type="checkbox"]');
280
+
281
+ it("pre-checks the terms-of-service box for an already-registered library", () => {
282
+ const { container } = renderWithTerms(allLibraries[0], "success");
283
+ expect(termsCheckbox(container)).toBeChecked();
284
+ });
285
+
286
+ it("leaves the terms-of-service box unchecked when registration failed", () => {
287
+ const { container } = renderWithTerms(allLibraries[1], "failure");
288
+ expect(termsCheckbox(container)).not.toBeChecked();
289
+ });
290
+ });
291
+
292
+ describe("behavior", () => {
293
+ it("registers a library", async () => {
294
+ const user = userEvent.setup();
295
+ const registerLibrary = jest.fn();
296
+ const { container } = renderReg({ item: serviceData, registerLibrary });
297
+ const libs = libraries(container);
298
+ expect(registerLibrary).not.toHaveBeenCalled();
299
+
300
+ await user.click(libs[0].querySelector("button"));
301
+ expect(registerLibrary).toHaveBeenCalledTimes(1);
302
+ // nypl is registered in production, so it always registers in production.
303
+ expect(registerLibrary.mock.calls[0][0]).toMatchObject({
304
+ short_name: "nypl",
305
+ });
306
+ expect(registerLibrary.mock.calls[0][1]).toBe("production");
307
+
308
+ await user.click(libs[1].querySelector("button"));
309
+ expect(registerLibrary).toHaveBeenCalledTimes(2);
310
+ // bpl defaults to the "testing" stage.
311
+ expect(registerLibrary.mock.calls[1][0]).toMatchObject({
312
+ short_name: "bpl",
313
+ });
314
+ expect(registerLibrary.mock.calls[1][1]).toBe("testing");
315
+
316
+ await user.click(libs[2].querySelector("button"));
317
+ expect(registerLibrary).toHaveBeenCalledTimes(3);
318
+ expect(registerLibrary.mock.calls[2][0]).toMatchObject({
319
+ short_name: "qpl",
320
+ });
321
+ expect(registerLibrary.mock.calls[2][1]).toBe("testing");
322
+ });
323
+
324
+ it("registers each library with the stage selected from its own dropdown", async () => {
325
+ const user = userEvent.setup();
326
+ const registerLibrary = jest.fn();
327
+ const { container } = renderReg({ item: serviceData, registerLibrary });
328
+ const [, bpl, qpl] = registrationInfos(container);
329
+
330
+ // Choosing "production" for bpl registers bpl in production...
331
+ await user.selectOptions(bpl.querySelector("select"), "production");
332
+ await user.click(bpl.querySelector("button"));
333
+ expect(registerLibrary.mock.calls[0][0]).toMatchObject({
334
+ short_name: "bpl",
335
+ });
336
+ expect(registerLibrary.mock.calls[0][1]).toBe("production");
337
+
338
+ // ...without affecting qpl, which still defaults to "testing".
339
+ await user.click(qpl.querySelector("button"));
340
+ expect(registerLibrary.mock.calls[1][0]).toMatchObject({
341
+ short_name: "qpl",
342
+ });
343
+ expect(registerLibrary.mock.calls[1][1]).toBe("testing");
344
+
345
+ // Each library's dropdown updates its own stage independently.
346
+ await user.selectOptions(qpl.querySelector("select"), "production");
347
+ await user.click(qpl.querySelector("button"));
348
+ expect(registerLibrary.mock.calls[2][0]).toMatchObject({
349
+ short_name: "qpl",
350
+ });
351
+ expect(registerLibrary.mock.calls[2][1]).toBe("production");
352
+ });
353
+ });
354
+ });
@@ -0,0 +1,167 @@
1
+ import * as React from "react";
2
+ import { render } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import NeighborhoodAnalyticsForm from "../../../src/components/NeighborhoodAnalyticsForm";
6
+ import { SettingData } from "../../../src/interfaces";
7
+
8
+ const patronAuthSetting: SettingData = {
9
+ key: "neighborhood_mode",
10
+ label: "Patron Auth",
11
+ options: [
12
+ { key: "disabled", label: "Off" },
13
+ { key: "choice1", label: "Choice 1" },
14
+ { key: "choice2", label: "Choice 2" },
15
+ ],
16
+ };
17
+
18
+ const analyticsSetting: SettingData = {
19
+ key: "location_source",
20
+ label: "Analytics",
21
+ options: [
22
+ { key: "disabled", label: "Off" },
23
+ { key: "choice1", label: "Choice 1" },
24
+ { key: "choice2", label: "Choice 2" },
25
+ ],
26
+ };
27
+
28
+ // The Panel renders its body collapsed and `aria-hidden` by default, so the
29
+ // form is present in the DOM but hidden from the accessibility tree; query it
30
+ // through the container.
31
+ const getSelect = (container: HTMLElement) =>
32
+ container.querySelector("select") as HTMLSelectElement;
33
+ const panelTitle = (container: HTMLElement) =>
34
+ container.querySelector(".panel-title") as HTMLElement;
35
+ const warning = (container: HTMLElement) =>
36
+ container.querySelector(".bg-warning") as HTMLElement | null;
37
+
38
+ describe("NeighborhoodAnalyticsForm", () => {
39
+ it("renders a panel with the feature name", () => {
40
+ const { container } = render(
41
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
42
+ );
43
+ expect(container.querySelector(".panel")).toBeInTheDocument();
44
+ expect(panelTitle(container)).toHaveTextContent(
45
+ "Patron Neighborhood Analytics: Disabled"
46
+ );
47
+ });
48
+
49
+ it("renders a select whose options reflect the setting", () => {
50
+ const { container } = render(
51
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
52
+ );
53
+ const options = Array.from(getSelect(container).querySelectorAll("option"));
54
+ expect(options).toHaveLength(patronAuthSetting.options.length);
55
+ options.forEach((option, idx) => {
56
+ expect(option).toHaveValue(patronAuthSetting.options[idx].key);
57
+ expect(option).toHaveTextContent(patronAuthSetting.options[idx].label);
58
+ });
59
+ });
60
+
61
+ it("renders a warning message pointing at the paired service once enabled", async () => {
62
+ const user = userEvent.setup();
63
+ const { container, rerender } = render(
64
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
65
+ );
66
+
67
+ // No warning while the feature is off.
68
+ expect(warning(container)).not.toBeInTheDocument();
69
+ await user.selectOptions(getSelect(container), "disabled");
70
+ expect(warning(container)).not.toBeInTheDocument();
71
+
72
+ // Enabling it surfaces a warning that links to the analytics config.
73
+ await user.selectOptions(getSelect(container), "choice1");
74
+ expect(warning(container)).toBeInTheDocument();
75
+ expect(warning(container)).toHaveTextContent(
76
+ "This feature will work only if it is also enabled in your local analytics service configuration settings."
77
+ );
78
+ expect(warning(container).querySelector("a")).toHaveAttribute(
79
+ "href",
80
+ "/admin/web/config/analytics"
81
+ );
82
+
83
+ // Switching the setting flips the paired service to patron auth.
84
+ rerender(<NeighborhoodAnalyticsForm setting={analyticsSetting} />);
85
+ expect(warning(container)).toHaveTextContent(
86
+ "This feature will work only if it is also enabled in your patron authentication service configuration settings."
87
+ );
88
+ expect(warning(container).querySelector("a")).toHaveAttribute(
89
+ "href",
90
+ "/admin/web/config/patronAuth"
91
+ );
92
+ });
93
+
94
+ it("updates the header when an option is chosen", async () => {
95
+ const user = userEvent.setup();
96
+ const { container } = render(
97
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
98
+ );
99
+ expect(panelTitle(container)).toHaveTextContent(
100
+ "Patron Neighborhood Analytics: Disabled"
101
+ );
102
+
103
+ await user.selectOptions(getSelect(container), "choice1");
104
+ expect(panelTitle(container)).toHaveTextContent(
105
+ "Patron Neighborhood Analytics: Enabled"
106
+ );
107
+ });
108
+
109
+ it("reflects the currentValue prop as the initial selection", () => {
110
+ const { container } = render(
111
+ <NeighborhoodAnalyticsForm
112
+ setting={patronAuthSetting}
113
+ currentValue="choice1"
114
+ />
115
+ );
116
+ // A non-"disabled" currentValue means the feature starts enabled.
117
+ expect(panelTitle(container)).toHaveTextContent(
118
+ "Patron Neighborhood Analytics: Enabled"
119
+ );
120
+ expect(getSelect(container)).toHaveValue("choice1");
121
+ });
122
+
123
+ it("tells the user whether the feature is currently enabled", async () => {
124
+ const user = userEvent.setup();
125
+ const { container } = render(
126
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
127
+ );
128
+
129
+ expect(panelTitle(container)).toHaveTextContent("Disabled");
130
+ await user.selectOptions(getSelect(container), "choice1");
131
+ expect(panelTitle(container)).toHaveTextContent("Enabled");
132
+ await user.selectOptions(getSelect(container), "choice2");
133
+ expect(panelTitle(container)).toHaveTextContent("Enabled");
134
+ await user.selectOptions(getSelect(container), "disabled");
135
+ expect(panelTitle(container)).toHaveTextContent("Disabled");
136
+ });
137
+
138
+ it("shows the warning only for enabled selections", async () => {
139
+ const user = userEvent.setup();
140
+ const { container } = render(
141
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
142
+ );
143
+
144
+ expect(warning(container)).not.toBeInTheDocument();
145
+ await user.selectOptions(getSelect(container), "choice1");
146
+ expect(warning(container)).toBeInTheDocument();
147
+ await user.selectOptions(getSelect(container), "choice2");
148
+ expect(warning(container)).toBeInTheDocument();
149
+ await user.selectOptions(getSelect(container), "disabled");
150
+ expect(warning(container)).not.toBeInTheDocument();
151
+ });
152
+
153
+ it("exposes the selected value through its getValue ref API", async () => {
154
+ // ServiceEditForm reads the chosen value via this imperative ref API.
155
+ const user = userEvent.setup();
156
+ const ref = React.createRef<NeighborhoodAnalyticsForm>();
157
+ const { container } = render(
158
+ <NeighborhoodAnalyticsForm ref={ref} setting={patronAuthSetting} />
159
+ );
160
+
161
+ expect(ref.current?.getValue()).toBe("");
162
+ await user.selectOptions(getSelect(container), "choice1");
163
+ expect(ref.current?.getValue()).toBe("choice1");
164
+ await user.selectOptions(getSelect(container), "choice2");
165
+ expect(ref.current?.getValue()).toBe("choice2");
166
+ });
167
+ });