@thepalaceproject/circulation-admin 1.43.0-post.9 → 1.44.0-post.10
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/CLAUDE.md +22 -10
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/jest.config.js +11 -44
- package/package.json +1 -5
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +111 -0
- package/tests/jest/components/Announcement.test.tsx +76 -0
- package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
- package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
- package/tests/jest/components/Autocomplete.test.tsx +67 -0
- package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
- package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/BookEditor.test.tsx +20 -21
- package/tests/jest/components/CatalogPage.test.tsx +158 -0
- package/tests/jest/components/CatalogServices.test.tsx +38 -1
- package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
- package/tests/jest/components/Classifications.test.tsx +166 -0
- package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
- package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
- package/tests/jest/components/Collections.test.tsx +335 -7
- package/tests/jest/components/ColorPicker.test.tsx +58 -0
- package/tests/jest/components/ComplaintForm.test.tsx +171 -0
- package/tests/jest/components/Complaints.test.tsx +259 -0
- package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/Contributors.test.tsx +157 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
- package/tests/jest/components/CustomLists.test.tsx +621 -4
- package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
- package/tests/jest/components/EditableConfigList.test.tsx +598 -7
- package/tests/jest/components/EditableInput.test.tsx +250 -1
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/GenreForm.test.tsx +152 -0
- package/tests/jest/components/Header.test.tsx +421 -0
- package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
- package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/Lane.test.tsx +132 -10
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +377 -9
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/Libraries.test.tsx +48 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/MetadataServices.test.tsx +37 -1
- package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/ProtocolFormField.test.tsx +443 -7
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +0 -7
- package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
- package/tests/jest/components/ToolTip.test.tsx +46 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
- package/tests/jest/features/book.test.ts +75 -80
- package/tests/jest/fixtures/classificationsData.ts +45 -0
- package/tests/jest/fixtures/genreData.ts +811 -0
- package/tests/jest/README.md +0 -6
- package/tests/jest/legacy-setup.ts +0 -13
- package/tests/jest/legacyJsdomEnvironment.js +0 -49
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { installFormDataShim, FormDataShim } from "../testUtils/formDataShim";
|
|
3
|
+
import { render, within } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import ServiceEditForm, {
|
|
7
|
+
ServiceEditFormProps,
|
|
8
|
+
ServiceEditFormState,
|
|
9
|
+
} from "../../../src/components/ServiceEditForm";
|
|
10
|
+
import NeighborhoodAnalyticsForm from "../../../src/components/NeighborhoodAnalyticsForm";
|
|
11
|
+
import { ServicesData } from "../../../src/interfaces";
|
|
12
|
+
// The reusable-components `Form` builds `new FormData(formElement)` on
|
|
13
|
+
// submit, which the unit jsdom env's undici FormData rejects; install the
|
|
14
|
+
// shared shim that reads the form's successful controls.
|
|
15
|
+
installFormDataShim();
|
|
16
|
+
describe("ServiceEditForm", () => {
|
|
17
|
+
// ServiceEditForm is a generic class; expose a concrete constructor type so
|
|
18
|
+
// JSX works without generic inference.
|
|
19
|
+
const TestServiceEditForm: new (
|
|
20
|
+
props: ServiceEditFormProps<ServicesData>
|
|
21
|
+
) => React.Component<
|
|
22
|
+
ServiceEditFormProps<ServicesData>,
|
|
23
|
+
ServiceEditFormState
|
|
24
|
+
> = ServiceEditForm;
|
|
25
|
+
|
|
26
|
+
let save: jest.Mock;
|
|
27
|
+
const urlBase = "/services";
|
|
28
|
+
|
|
29
|
+
const serviceData = {
|
|
30
|
+
id: 2,
|
|
31
|
+
protocol: "protocol 1",
|
|
32
|
+
settings: {
|
|
33
|
+
text_setting: "text setting",
|
|
34
|
+
select_setting: "option2",
|
|
35
|
+
},
|
|
36
|
+
libraries: [
|
|
37
|
+
{
|
|
38
|
+
short_name: "nypl",
|
|
39
|
+
library_text_setting: "library text setting",
|
|
40
|
+
library_select_setting: "option4",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
const parentProtocol = {
|
|
45
|
+
name: "protocol 3",
|
|
46
|
+
label: "protocol 3 label",
|
|
47
|
+
settings: [{ key: "parent_setting", label: "parent label" }],
|
|
48
|
+
child_settings: [{ key: "child_setting", label: "child label" }],
|
|
49
|
+
library_settings: [],
|
|
50
|
+
};
|
|
51
|
+
const protocolsData = [
|
|
52
|
+
{
|
|
53
|
+
name: "protocol 1",
|
|
54
|
+
label: "protocol 1 label",
|
|
55
|
+
description: "protocol 1 description",
|
|
56
|
+
sitewide: false,
|
|
57
|
+
settings: [
|
|
58
|
+
{ key: "text_setting", label: "text label", optional: true },
|
|
59
|
+
{
|
|
60
|
+
key: "select_setting",
|
|
61
|
+
label: "select label",
|
|
62
|
+
type: "select",
|
|
63
|
+
options: [
|
|
64
|
+
{ key: "option1", label: "option 1" },
|
|
65
|
+
{ key: "option2", label: "option 2" },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
library_settings: [
|
|
70
|
+
{
|
|
71
|
+
key: "library_text_setting",
|
|
72
|
+
label: "library text label",
|
|
73
|
+
optional: true,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "library_select_setting",
|
|
77
|
+
label: "library select label",
|
|
78
|
+
type: "select",
|
|
79
|
+
options: [
|
|
80
|
+
{ key: "option3", label: "option 3" },
|
|
81
|
+
{ key: "option4", label: "option 4" },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "protocol 2",
|
|
88
|
+
label: "protocol 2 label",
|
|
89
|
+
description: "protocol 2 description",
|
|
90
|
+
sitewide: true,
|
|
91
|
+
settings: [
|
|
92
|
+
{ key: "text_setting", label: "text label" },
|
|
93
|
+
{ key: "protocol2_setting", label: "protocol2 label" },
|
|
94
|
+
],
|
|
95
|
+
library_settings: [],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "protocol with instructions",
|
|
99
|
+
label: "instructions label",
|
|
100
|
+
description: "click for instructions",
|
|
101
|
+
instructions: "Instructions!",
|
|
102
|
+
sitewide: false,
|
|
103
|
+
settings: [],
|
|
104
|
+
library_settings: [],
|
|
105
|
+
},
|
|
106
|
+
parentProtocol,
|
|
107
|
+
];
|
|
108
|
+
const allLibraries = [
|
|
109
|
+
{ short_name: "nypl", name: "New York Public Library" },
|
|
110
|
+
{ short_name: "bpl", name: "Brooklyn Public Library" },
|
|
111
|
+
];
|
|
112
|
+
const servicesData = {
|
|
113
|
+
services: [serviceData],
|
|
114
|
+
protocols: protocolsData,
|
|
115
|
+
allLibraries: allLibraries,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Renders the form with the standard set of props; `props` overrides them.
|
|
119
|
+
const renderForm = (
|
|
120
|
+
props: Partial<ServiceEditFormProps<ServicesData>> = {}
|
|
121
|
+
) =>
|
|
122
|
+
render(
|
|
123
|
+
<TestServiceEditForm
|
|
124
|
+
disabled={false}
|
|
125
|
+
data={servicesData}
|
|
126
|
+
save={save}
|
|
127
|
+
urlBase={urlBase}
|
|
128
|
+
listDataKey="services"
|
|
129
|
+
{...props}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Re-renders (keeping the same instance) with the standard props + overrides.
|
|
134
|
+
const rerenderForm = (
|
|
135
|
+
rerender: (el: React.ReactElement) => void,
|
|
136
|
+
props: Partial<ServiceEditFormProps<ServicesData>> = {}
|
|
137
|
+
) =>
|
|
138
|
+
rerender(
|
|
139
|
+
<TestServiceEditForm
|
|
140
|
+
disabled={false}
|
|
141
|
+
data={servicesData}
|
|
142
|
+
save={save}
|
|
143
|
+
urlBase={urlBase}
|
|
144
|
+
listDataKey="services"
|
|
145
|
+
{...props}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
save = jest.fn();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("rendering", () => {
|
|
154
|
+
it("renders hidden id", () => {
|
|
155
|
+
const { container, rerender } = renderForm();
|
|
156
|
+
expect(container.querySelector('input[name="id"]')).toBeNull();
|
|
157
|
+
|
|
158
|
+
rerenderForm(rerender, { item: serviceData });
|
|
159
|
+
const input = container.querySelector(
|
|
160
|
+
'input[name="id"]'
|
|
161
|
+
) as HTMLInputElement;
|
|
162
|
+
expect(input.type).toBe("hidden");
|
|
163
|
+
expect(input.value).toBe("2");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("renders protocol", () => {
|
|
167
|
+
// Starts with the first protocol in the list, and is editable (all
|
|
168
|
+
// protocols available as options).
|
|
169
|
+
const { container, unmount } = renderForm();
|
|
170
|
+
let select = container.querySelector(
|
|
171
|
+
'select[name="protocol"]'
|
|
172
|
+
) as HTMLSelectElement;
|
|
173
|
+
expect(select.value).toBe("protocol 1");
|
|
174
|
+
let options = select.querySelectorAll("option");
|
|
175
|
+
expect(options).toHaveLength(4);
|
|
176
|
+
expect(options[0]).toHaveTextContent("protocol 1 label");
|
|
177
|
+
expect(options[1]).toHaveTextContent("protocol 2 label");
|
|
178
|
+
expect(options[2]).toHaveTextContent("instructions label");
|
|
179
|
+
expect(
|
|
180
|
+
within(container).getByText("protocol 1 description")
|
|
181
|
+
).toBeInTheDocument();
|
|
182
|
+
|
|
183
|
+
// When editing an existing service, the protocol can't be changed, so
|
|
184
|
+
// there is only a single option.
|
|
185
|
+
unmount();
|
|
186
|
+
const { container: c2 } = renderForm({ item: serviceData });
|
|
187
|
+
select = c2.querySelector('select[name="protocol"]') as HTMLSelectElement;
|
|
188
|
+
expect(select.value).toBe("protocol 1");
|
|
189
|
+
options = select.querySelectorAll("option");
|
|
190
|
+
expect(options).toHaveLength(1);
|
|
191
|
+
expect(options[0]).toHaveTextContent("protocol 1 label");
|
|
192
|
+
expect(
|
|
193
|
+
within(c2).getByText("protocol 1 description")
|
|
194
|
+
).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("renders protocol fields", () => {
|
|
198
|
+
const { container, unmount } = renderForm();
|
|
199
|
+
expect(
|
|
200
|
+
(
|
|
201
|
+
container.querySelector(
|
|
202
|
+
'input[name="text_setting"]'
|
|
203
|
+
) as HTMLInputElement
|
|
204
|
+
).value
|
|
205
|
+
).toBe("");
|
|
206
|
+
expect(
|
|
207
|
+
container.querySelector('select[name="select_setting"]')
|
|
208
|
+
).toBeInTheDocument();
|
|
209
|
+
expect(container.querySelector('[name="protocol2_setting"]')).toBeNull();
|
|
210
|
+
|
|
211
|
+
unmount();
|
|
212
|
+
const { container: c2 } = renderForm({ item: serviceData });
|
|
213
|
+
expect(
|
|
214
|
+
(c2.querySelector('input[name="text_setting"]') as HTMLInputElement)
|
|
215
|
+
.value
|
|
216
|
+
).toBe("text setting");
|
|
217
|
+
expect(
|
|
218
|
+
(c2.querySelector('select[name="select_setting"]') as HTMLSelectElement)
|
|
219
|
+
.value
|
|
220
|
+
).toBe("option2");
|
|
221
|
+
expect(c2.querySelector('[name="protocol2_setting"]')).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("renders a collapsible component", async () => {
|
|
225
|
+
const user = userEvent.setup();
|
|
226
|
+
const { container } = renderForm();
|
|
227
|
+
await user.selectOptions(
|
|
228
|
+
container.querySelector('select[name="protocol"]') as HTMLSelectElement,
|
|
229
|
+
"protocol with instructions"
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const panels = container.querySelectorAll(".panel");
|
|
233
|
+
expect(panels).toHaveLength(3);
|
|
234
|
+
expect(panels[0].querySelector(".panel-title")).toHaveTextContent(
|
|
235
|
+
"click for instructions"
|
|
236
|
+
);
|
|
237
|
+
expect(panels[0].querySelector(".panel-body")).toHaveTextContent(
|
|
238
|
+
"Instructions!"
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("doesn't render parent dropdown for protocol with no child settings", () => {
|
|
243
|
+
const { container } = renderForm();
|
|
244
|
+
expect(container.querySelector('select[name="parent_id"]')).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("doesn't render parent dropdown when there are no available parents", () => {
|
|
248
|
+
const newService = Object.assign({}, serviceData, {
|
|
249
|
+
protocol: "protocol 3",
|
|
250
|
+
});
|
|
251
|
+
const { container } = renderForm({ item: newService });
|
|
252
|
+
expect(container.querySelector('select[name="parent_id"]')).toBeNull();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("renders parent dropdown for protocol with child settings and available parents", () => {
|
|
256
|
+
const parentService = Object.assign({}, serviceData, {
|
|
257
|
+
protocol: "protocol 3",
|
|
258
|
+
name: "Parent",
|
|
259
|
+
});
|
|
260
|
+
const childService = Object.assign({}, serviceData, {
|
|
261
|
+
protocol: "protocol 3",
|
|
262
|
+
id: 3,
|
|
263
|
+
name: "Child",
|
|
264
|
+
});
|
|
265
|
+
const servicesDataWithParent = Object.assign({}, servicesData, {
|
|
266
|
+
services: [parentService, childService],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const { container, unmount } = renderForm({
|
|
270
|
+
data: servicesDataWithParent,
|
|
271
|
+
item: childService,
|
|
272
|
+
});
|
|
273
|
+
let select = container.querySelector(
|
|
274
|
+
'select[name="parent_id"]'
|
|
275
|
+
) as HTMLSelectElement;
|
|
276
|
+
expect(select).toBeInTheDocument();
|
|
277
|
+
// No parent selected, so the "None" option (value "") is active.
|
|
278
|
+
expect(select.value).toBe("");
|
|
279
|
+
let options = select.querySelectorAll("option");
|
|
280
|
+
expect(options).toHaveLength(2);
|
|
281
|
+
expect(options[0]).toHaveTextContent("None");
|
|
282
|
+
expect(options[1]).toHaveTextContent("Parent");
|
|
283
|
+
|
|
284
|
+
unmount();
|
|
285
|
+
const childWithParent = Object.assign({}, childService, {
|
|
286
|
+
parent_id: parentService.id,
|
|
287
|
+
});
|
|
288
|
+
const { container: c2 } = renderForm({
|
|
289
|
+
data: servicesDataWithParent,
|
|
290
|
+
item: childWithParent,
|
|
291
|
+
});
|
|
292
|
+
select = c2.querySelector(
|
|
293
|
+
'select[name="parent_id"]'
|
|
294
|
+
) as HTMLSelectElement;
|
|
295
|
+
expect(select).toBeInTheDocument();
|
|
296
|
+
expect(select.value).toBe("2");
|
|
297
|
+
options = select.querySelectorAll("option");
|
|
298
|
+
expect(options).toHaveLength(2);
|
|
299
|
+
expect(options[0]).toHaveTextContent("None");
|
|
300
|
+
expect(options[1]).toHaveTextContent("Parent");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("renders protocol fields for child", () => {
|
|
304
|
+
const parentService = Object.assign({}, serviceData, {
|
|
305
|
+
protocol: "protocol 3",
|
|
306
|
+
name: "Parent",
|
|
307
|
+
});
|
|
308
|
+
const childNoParent = Object.assign({}, serviceData, {
|
|
309
|
+
protocol: "protocol 3",
|
|
310
|
+
id: 3,
|
|
311
|
+
name: "Child",
|
|
312
|
+
settings: { ...serviceData.settings },
|
|
313
|
+
});
|
|
314
|
+
const servicesDataWithParent = Object.assign({}, servicesData, {
|
|
315
|
+
services: [parentService, childNoParent],
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Editing a "parent" service (no parent selected) shows the parent
|
|
319
|
+
// settings, not the child settings.
|
|
320
|
+
const { container, rerender, unmount } = renderForm({
|
|
321
|
+
data: servicesDataWithParent,
|
|
322
|
+
item: childNoParent,
|
|
323
|
+
});
|
|
324
|
+
expect(
|
|
325
|
+
(
|
|
326
|
+
container.querySelector(
|
|
327
|
+
'input[name="parent_setting"]'
|
|
328
|
+
) as HTMLInputElement
|
|
329
|
+
).value
|
|
330
|
+
).toBe("");
|
|
331
|
+
expect(container.querySelector('input[name="child_setting"]')).toBeNull();
|
|
332
|
+
|
|
333
|
+
rerenderForm(rerender, {
|
|
334
|
+
data: servicesDataWithParent,
|
|
335
|
+
item: {
|
|
336
|
+
...childNoParent,
|
|
337
|
+
settings: {
|
|
338
|
+
...childNoParent.settings,
|
|
339
|
+
parent_setting: "parent setting",
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
expect(
|
|
344
|
+
(
|
|
345
|
+
container.querySelector(
|
|
346
|
+
'input[name="parent_setting"]'
|
|
347
|
+
) as HTMLInputElement
|
|
348
|
+
).value
|
|
349
|
+
).toBe("parent setting");
|
|
350
|
+
|
|
351
|
+
// Once the service has a parent, only the child settings are shown.
|
|
352
|
+
unmount();
|
|
353
|
+
const childWithParent = {
|
|
354
|
+
...childNoParent,
|
|
355
|
+
parent_id: parentService.id,
|
|
356
|
+
settings: { ...serviceData.settings },
|
|
357
|
+
};
|
|
358
|
+
const { container: c2, rerender: rerender2 } = renderForm({
|
|
359
|
+
data: servicesDataWithParent,
|
|
360
|
+
item: childWithParent,
|
|
361
|
+
});
|
|
362
|
+
expect(c2.querySelector('input[name="parent_setting"]')).toBeNull();
|
|
363
|
+
expect(
|
|
364
|
+
(c2.querySelector('input[name="child_setting"]') as HTMLInputElement)
|
|
365
|
+
.value
|
|
366
|
+
).toBe("");
|
|
367
|
+
|
|
368
|
+
rerenderForm(rerender2, {
|
|
369
|
+
data: servicesDataWithParent,
|
|
370
|
+
item: {
|
|
371
|
+
...childWithParent,
|
|
372
|
+
settings: {
|
|
373
|
+
...childWithParent.settings,
|
|
374
|
+
child_setting: "child setting",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
expect(
|
|
379
|
+
(c2.querySelector('input[name="child_setting"]') as HTMLInputElement)
|
|
380
|
+
.value
|
|
381
|
+
).toBe("child setting");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("doesn't render libraries for sitewide protocol without library settings", () => {
|
|
385
|
+
const servicesDataSitewide = Object.assign({}, servicesData, {
|
|
386
|
+
protocols: [servicesData.protocols[1]],
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const { container, unmount } = renderForm({ data: servicesDataSitewide });
|
|
390
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(0);
|
|
391
|
+
|
|
392
|
+
unmount();
|
|
393
|
+
const serviceDataSitewide = Object.assign({}, servicesData, {
|
|
394
|
+
libraries: [],
|
|
395
|
+
protocol: "",
|
|
396
|
+
});
|
|
397
|
+
const { container: c2 } = renderForm({
|
|
398
|
+
data: servicesDataSitewide,
|
|
399
|
+
item: serviceDataSitewide,
|
|
400
|
+
});
|
|
401
|
+
expect(c2.querySelectorAll(".with-remove-button")).toHaveLength(0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("renders libraries for a sitewide protocol with library settings", () => {
|
|
405
|
+
const servicesDataSitewide = Object.assign({}, servicesData, {
|
|
406
|
+
protocols: [
|
|
407
|
+
Object.assign({}, servicesData.protocols[1], { sitewide: true }),
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const { container } = renderForm({
|
|
412
|
+
data: servicesDataSitewide,
|
|
413
|
+
item: serviceData,
|
|
414
|
+
});
|
|
415
|
+
const libraries = container.querySelectorAll(".with-remove-button");
|
|
416
|
+
expect(libraries).toHaveLength(1);
|
|
417
|
+
expect(libraries[0]).toHaveTextContent("New York Public Library");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("renders removable and editable libraries", () => {
|
|
421
|
+
const { container, unmount } = renderForm();
|
|
422
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(0);
|
|
423
|
+
|
|
424
|
+
unmount();
|
|
425
|
+
const { container: c2 } = renderForm({ item: serviceData });
|
|
426
|
+
const libraries = c2.querySelectorAll(".with-remove-button");
|
|
427
|
+
expect(libraries).toHaveLength(1);
|
|
428
|
+
const editable = c2.querySelectorAll(".with-edit-button");
|
|
429
|
+
expect(editable).toHaveLength(1);
|
|
430
|
+
expect(editable[0]).toHaveTextContent("New York Public Library");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("renders removable but not editable libraries", () => {
|
|
434
|
+
const { container, unmount } = renderForm();
|
|
435
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(0);
|
|
436
|
+
|
|
437
|
+
unmount();
|
|
438
|
+
const newServiceData = Object.assign({}, serviceData, {
|
|
439
|
+
protocol: "protocol 3",
|
|
440
|
+
});
|
|
441
|
+
const { container: c2 } = renderForm({ item: newServiceData });
|
|
442
|
+
const libraries = c2.querySelectorAll(".with-remove-button");
|
|
443
|
+
expect(libraries).toHaveLength(1);
|
|
444
|
+
// protocol 3 has no library settings, so there is no edit button.
|
|
445
|
+
expect(c2.querySelectorAll(".with-edit-button")).toHaveLength(0);
|
|
446
|
+
expect(libraries[0]).toHaveTextContent("New York Public Library");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("doesn't render library add dropdown for sitewide protocol", () => {
|
|
450
|
+
const servicesDataSitewide = Object.assign({}, servicesData, {
|
|
451
|
+
protocols: [servicesData.protocols[1]],
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const { container, unmount } = renderForm({ data: servicesDataSitewide });
|
|
455
|
+
expect(container.querySelector('select[name="add-library"]')).toBeNull();
|
|
456
|
+
|
|
457
|
+
unmount();
|
|
458
|
+
const serviceDataSitewide = Object.assign({}, servicesData, {
|
|
459
|
+
libraries: [],
|
|
460
|
+
protocol: "",
|
|
461
|
+
});
|
|
462
|
+
const { container: c2 } = renderForm({
|
|
463
|
+
data: servicesDataSitewide,
|
|
464
|
+
item: serviceDataSitewide,
|
|
465
|
+
});
|
|
466
|
+
expect(c2.querySelector('select[name="add-library"]')).toBeNull();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("renders library add dropdown for a sitewide protocol with library settings", () => {
|
|
470
|
+
const servicesDataSitewide = Object.assign({}, servicesData, {
|
|
471
|
+
protocols: [
|
|
472
|
+
Object.assign({}, servicesData.protocols[1], { sitewide: true }),
|
|
473
|
+
],
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const { container } = renderForm({
|
|
477
|
+
data: servicesDataSitewide,
|
|
478
|
+
item: serviceData,
|
|
479
|
+
});
|
|
480
|
+
expect(
|
|
481
|
+
container.querySelector('select[name="add-library"]')
|
|
482
|
+
).toBeInTheDocument();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("renders library add dropdown for a non-sitewide protocol", () => {
|
|
486
|
+
const { container, unmount } = renderForm();
|
|
487
|
+
let select = container.querySelector(
|
|
488
|
+
'select[name="add-library"]'
|
|
489
|
+
) as HTMLSelectElement;
|
|
490
|
+
expect(within(container).getByText("Add Library")).toBeInTheDocument();
|
|
491
|
+
|
|
492
|
+
let options = select.querySelectorAll("option");
|
|
493
|
+
expect(options).toHaveLength(3);
|
|
494
|
+
expect((options[0] as HTMLOptionElement).value).toBe("none");
|
|
495
|
+
expect((options[1] as HTMLOptionElement).value).toBe("nypl");
|
|
496
|
+
expect((options[2] as HTMLOptionElement).value).toBe("bpl");
|
|
497
|
+
|
|
498
|
+
// No library selected yet, so no per-library settings are rendered.
|
|
499
|
+
expect(
|
|
500
|
+
container.querySelector('input[name="library_text_setting"]')
|
|
501
|
+
).toBeNull();
|
|
502
|
+
expect(
|
|
503
|
+
container.querySelector('select[name="library_select_setting"]')
|
|
504
|
+
).toBeNull();
|
|
505
|
+
|
|
506
|
+
// When editing an existing service, the already-associated library
|
|
507
|
+
// (nypl) is not offered in the add dropdown.
|
|
508
|
+
unmount();
|
|
509
|
+
const { container: c2 } = renderForm({ item: serviceData });
|
|
510
|
+
select = c2.querySelector(
|
|
511
|
+
'select[name="add-library"]'
|
|
512
|
+
) as HTMLSelectElement;
|
|
513
|
+
expect(within(c2).getByText("Add Library")).toBeInTheDocument();
|
|
514
|
+
options = select.querySelectorAll("option");
|
|
515
|
+
expect(options).toHaveLength(2);
|
|
516
|
+
expect((options[0] as HTMLOptionElement).value).toBe("none");
|
|
517
|
+
expect((options[1] as HTMLOptionElement).value).toBe("bpl");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("renders neighborhood analytics form for a relevant patron auth protocol", () => {
|
|
521
|
+
const { container, rerender } = renderForm();
|
|
522
|
+
expect(
|
|
523
|
+
within(container).queryByText(/Patron Neighborhood Analytics/)
|
|
524
|
+
).not.toBeInTheDocument();
|
|
525
|
+
|
|
526
|
+
const patronAuthProtocol = {
|
|
527
|
+
...protocolsData[0],
|
|
528
|
+
...{ settings: [{ key: "neighborhood_mode", options: [] }] },
|
|
529
|
+
};
|
|
530
|
+
rerenderForm(rerender, {
|
|
531
|
+
extraFormKey: "neighborhood_mode",
|
|
532
|
+
extraFormSection: NeighborhoodAnalyticsForm,
|
|
533
|
+
// The pared-down protocol setting omits `label`, so the data is cast to
|
|
534
|
+
// `any` to bypass the prop type.
|
|
535
|
+
data: {
|
|
536
|
+
...servicesData,
|
|
537
|
+
...{ protocols: [patronAuthProtocol] },
|
|
538
|
+
} as any,
|
|
539
|
+
});
|
|
540
|
+
// The ": Disabled" suffix is unique to the panel title (the form's
|
|
541
|
+
// visually-hidden legend has the bare heading text too).
|
|
542
|
+
expect(
|
|
543
|
+
within(container).getByText(/Patron Neighborhood Analytics: Disabled/)
|
|
544
|
+
).toBeInTheDocument();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("renders neighborhood analytics form for a relevant analytics protocol", () => {
|
|
548
|
+
const { container, rerender } = renderForm();
|
|
549
|
+
expect(
|
|
550
|
+
within(container).queryByText(/Patron Neighborhood Analytics/)
|
|
551
|
+
).not.toBeInTheDocument();
|
|
552
|
+
|
|
553
|
+
const analyticsProtocol = {
|
|
554
|
+
...protocolsData[0],
|
|
555
|
+
...{ settings: [{ key: "location_source", options: [] }] },
|
|
556
|
+
};
|
|
557
|
+
rerenderForm(rerender, {
|
|
558
|
+
extraFormKey: "location_source",
|
|
559
|
+
extraFormSection: NeighborhoodAnalyticsForm,
|
|
560
|
+
// The pared-down protocol setting omits `label`, so the data is cast to
|
|
561
|
+
// `any` to bypass the prop type.
|
|
562
|
+
data: { ...servicesData, ...{ protocols: [analyticsProtocol] } } as any,
|
|
563
|
+
});
|
|
564
|
+
expect(
|
|
565
|
+
within(container).getByText(/Patron Neighborhood Analytics: Disabled/)
|
|
566
|
+
).toBeInTheDocument();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("has a save button", () => {
|
|
570
|
+
const { container } = renderForm();
|
|
571
|
+
// The form's submit button (the collapsible panel headers are also
|
|
572
|
+
// <button>s, so this is the only reusable-components Button).
|
|
573
|
+
expect(
|
|
574
|
+
within(container).getByRole("button", { name: "Submit" })
|
|
575
|
+
).toBeInTheDocument();
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
describe("behavior", () => {
|
|
580
|
+
it("changes fields and description when protocol changes", async () => {
|
|
581
|
+
const user = userEvent.setup();
|
|
582
|
+
const { container } = renderForm();
|
|
583
|
+
|
|
584
|
+
// Select a library so the library settings are shown.
|
|
585
|
+
await user.selectOptions(
|
|
586
|
+
container.querySelector(
|
|
587
|
+
'select[name="add-library"]'
|
|
588
|
+
) as HTMLSelectElement,
|
|
589
|
+
"nypl"
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
expect(
|
|
593
|
+
container.querySelector('input[name="text_setting"]')
|
|
594
|
+
).toBeInTheDocument();
|
|
595
|
+
expect(
|
|
596
|
+
container.querySelector('select[name="select_setting"]')
|
|
597
|
+
).toBeInTheDocument();
|
|
598
|
+
expect(
|
|
599
|
+
container.querySelector('input[name="library_text_setting"]')
|
|
600
|
+
).toBeInTheDocument();
|
|
601
|
+
expect(
|
|
602
|
+
container.querySelector('select[name="library_select_setting"]')
|
|
603
|
+
).toBeInTheDocument();
|
|
604
|
+
expect(container.querySelector('[name="protocol2_setting"]')).toBeNull();
|
|
605
|
+
expect(
|
|
606
|
+
within(container).getByText("protocol 1 description")
|
|
607
|
+
).toBeInTheDocument();
|
|
608
|
+
|
|
609
|
+
// Switch to protocol 2.
|
|
610
|
+
await user.selectOptions(
|
|
611
|
+
container.querySelector('select[name="protocol"]') as HTMLSelectElement,
|
|
612
|
+
"protocol 2"
|
|
613
|
+
);
|
|
614
|
+
expect(
|
|
615
|
+
container.querySelector('input[name="text_setting"]')
|
|
616
|
+
).toBeInTheDocument();
|
|
617
|
+
expect(
|
|
618
|
+
container.querySelector('select[name="select_setting"]')
|
|
619
|
+
).toBeNull();
|
|
620
|
+
// protocol 2 is sitewide with no library settings, so the whole
|
|
621
|
+
// libraries section (and its fields) disappears.
|
|
622
|
+
expect(
|
|
623
|
+
container.querySelector('input[name="library_text_setting"]')
|
|
624
|
+
).toBeNull();
|
|
625
|
+
expect(
|
|
626
|
+
container.querySelector('select[name="library_select_setting"]')
|
|
627
|
+
).toBeNull();
|
|
628
|
+
expect(
|
|
629
|
+
container.querySelector('input[name="protocol2_setting"]')
|
|
630
|
+
).toBeInTheDocument();
|
|
631
|
+
expect(
|
|
632
|
+
within(container).getByText("protocol 2 description")
|
|
633
|
+
).toBeInTheDocument();
|
|
634
|
+
|
|
635
|
+
// Switch back to protocol 1.
|
|
636
|
+
await user.selectOptions(
|
|
637
|
+
container.querySelector('select[name="protocol"]') as HTMLSelectElement,
|
|
638
|
+
"protocol 1"
|
|
639
|
+
);
|
|
640
|
+
expect(
|
|
641
|
+
container.querySelector('input[name="text_setting"]')
|
|
642
|
+
).toBeInTheDocument();
|
|
643
|
+
expect(
|
|
644
|
+
container.querySelector('select[name="select_setting"]')
|
|
645
|
+
).toBeInTheDocument();
|
|
646
|
+
expect(
|
|
647
|
+
container.querySelector('input[name="library_text_setting"]')
|
|
648
|
+
).toBeInTheDocument();
|
|
649
|
+
expect(
|
|
650
|
+
container.querySelector('select[name="library_select_setting"]')
|
|
651
|
+
).toBeInTheDocument();
|
|
652
|
+
expect(container.querySelector('[name="protocol2_setting"]')).toBeNull();
|
|
653
|
+
expect(
|
|
654
|
+
within(container).getByText("protocol 1 description")
|
|
655
|
+
).toBeInTheDocument();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("changes fields when parent changes", async () => {
|
|
659
|
+
const user = userEvent.setup();
|
|
660
|
+
const parentService = Object.assign({}, serviceData, {
|
|
661
|
+
protocol: "protocol 3",
|
|
662
|
+
name: "Parent",
|
|
663
|
+
});
|
|
664
|
+
const servicesDataWithParent = Object.assign({}, servicesData, {
|
|
665
|
+
services: [parentService],
|
|
666
|
+
protocols: [parentProtocol],
|
|
667
|
+
});
|
|
668
|
+
const { container } = renderForm({ data: servicesDataWithParent });
|
|
669
|
+
|
|
670
|
+
expect(
|
|
671
|
+
container.querySelector('input[name="parent_setting"]')
|
|
672
|
+
).toBeInTheDocument();
|
|
673
|
+
expect(container.querySelector('input[name="child_setting"]')).toBeNull();
|
|
674
|
+
|
|
675
|
+
// Selecting a parent switches to the child settings.
|
|
676
|
+
await user.selectOptions(
|
|
677
|
+
container.querySelector(
|
|
678
|
+
'select[name="parent_id"]'
|
|
679
|
+
) as HTMLSelectElement,
|
|
680
|
+
"2"
|
|
681
|
+
);
|
|
682
|
+
expect(
|
|
683
|
+
container.querySelector('input[name="parent_setting"]')
|
|
684
|
+
).toBeNull();
|
|
685
|
+
expect(
|
|
686
|
+
container.querySelector('input[name="child_setting"]')
|
|
687
|
+
).toBeInTheDocument();
|
|
688
|
+
|
|
689
|
+
// Clearing the parent switches back to the parent settings.
|
|
690
|
+
await user.selectOptions(
|
|
691
|
+
container.querySelector(
|
|
692
|
+
'select[name="parent_id"]'
|
|
693
|
+
) as HTMLSelectElement,
|
|
694
|
+
""
|
|
695
|
+
);
|
|
696
|
+
expect(
|
|
697
|
+
container.querySelector('input[name="parent_setting"]')
|
|
698
|
+
).toBeInTheDocument();
|
|
699
|
+
expect(container.querySelector('input[name="child_setting"]')).toBeNull();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("adds a library with settings", async () => {
|
|
703
|
+
const user = userEvent.setup();
|
|
704
|
+
const { container } = renderForm();
|
|
705
|
+
|
|
706
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(0);
|
|
707
|
+
expect(
|
|
708
|
+
container.querySelector('input[name="library_text_setting"]')
|
|
709
|
+
).toBeNull();
|
|
710
|
+
expect(
|
|
711
|
+
container.querySelector('select[name="library_select_setting"]')
|
|
712
|
+
).toBeNull();
|
|
713
|
+
|
|
714
|
+
const addLibrarySelect = () =>
|
|
715
|
+
container.querySelector(
|
|
716
|
+
'select[name="add-library"]'
|
|
717
|
+
) as HTMLSelectElement;
|
|
718
|
+
|
|
719
|
+
// Selecting a library reveals its settings fields...
|
|
720
|
+
await user.selectOptions(addLibrarySelect(), "bpl");
|
|
721
|
+
expect(
|
|
722
|
+
container.querySelector('input[name="library_text_setting"]')
|
|
723
|
+
).toBeInTheDocument();
|
|
724
|
+
expect(
|
|
725
|
+
container.querySelector('select[name="library_select_setting"]')
|
|
726
|
+
).toBeInTheDocument();
|
|
727
|
+
|
|
728
|
+
// ...and deselecting hides them again.
|
|
729
|
+
await user.selectOptions(addLibrarySelect(), "none");
|
|
730
|
+
expect(
|
|
731
|
+
container.querySelector('input[name="library_text_setting"]')
|
|
732
|
+
).toBeNull();
|
|
733
|
+
expect(
|
|
734
|
+
container.querySelector('select[name="library_select_setting"]')
|
|
735
|
+
).toBeNull();
|
|
736
|
+
|
|
737
|
+
// Re-select and fill out the settings.
|
|
738
|
+
await user.selectOptions(addLibrarySelect(), "bpl");
|
|
739
|
+
const textInput = container.querySelector(
|
|
740
|
+
'input[name="library_text_setting"]'
|
|
741
|
+
) as HTMLInputElement;
|
|
742
|
+
await user.clear(textInput);
|
|
743
|
+
await user.type(textInput, "library text");
|
|
744
|
+
await user.selectOptions(
|
|
745
|
+
container.querySelector(
|
|
746
|
+
'select[name="library_select_setting"]'
|
|
747
|
+
) as HTMLSelectElement,
|
|
748
|
+
"option4"
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
// The buttons live inside a collapsed (aria-hidden) panel, so click them
|
|
752
|
+
// by class rather than by accessible role.
|
|
753
|
+
await user.click(
|
|
754
|
+
container.querySelector("button.left-align") as HTMLElement
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
// The library is added, showing its name plus Delete and Edit controls.
|
|
758
|
+
const libraries = container.querySelectorAll(".with-remove-button");
|
|
759
|
+
expect(libraries).toHaveLength(1);
|
|
760
|
+
expect(libraries[0]).toHaveTextContent("Brooklyn Public Library");
|
|
761
|
+
expect(libraries[0]).toHaveTextContent("Delete");
|
|
762
|
+
expect(libraries[0]).toHaveTextContent("Edit");
|
|
763
|
+
|
|
764
|
+
// Expanding the library shows the settings that were saved with it,
|
|
765
|
+
// proving the added library retained its short_name and settings.
|
|
766
|
+
await user.click(
|
|
767
|
+
(libraries[0] as HTMLElement).querySelector(
|
|
768
|
+
"button.edit-btn"
|
|
769
|
+
) as HTMLElement
|
|
770
|
+
);
|
|
771
|
+
const editSection = container.querySelector(
|
|
772
|
+
".edit-library-settings"
|
|
773
|
+
) as HTMLElement;
|
|
774
|
+
expect(
|
|
775
|
+
(
|
|
776
|
+
editSection.querySelector(
|
|
777
|
+
'input[name="library_text_setting"]'
|
|
778
|
+
) as HTMLInputElement
|
|
779
|
+
).value
|
|
780
|
+
).toBe("library text");
|
|
781
|
+
expect(
|
|
782
|
+
(
|
|
783
|
+
editSection.querySelector(
|
|
784
|
+
'select[name="library_select_setting"]'
|
|
785
|
+
) as HTMLSelectElement
|
|
786
|
+
).value
|
|
787
|
+
).toBe("option4");
|
|
788
|
+
// Collapse the edit section again.
|
|
789
|
+
await user.click(
|
|
790
|
+
(libraries[0] as HTMLElement).querySelector(
|
|
791
|
+
"button.edit-btn"
|
|
792
|
+
) as HTMLElement
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
// Selecting a different library resets the add-new-library settings.
|
|
796
|
+
await user.selectOptions(addLibrarySelect(), "nypl");
|
|
797
|
+
expect(
|
|
798
|
+
(
|
|
799
|
+
container.querySelector(
|
|
800
|
+
'input[name="library_text_setting"]'
|
|
801
|
+
) as HTMLInputElement
|
|
802
|
+
).value
|
|
803
|
+
).toBe("");
|
|
804
|
+
expect(
|
|
805
|
+
(
|
|
806
|
+
container.querySelector(
|
|
807
|
+
'select[name="library_select_setting"]'
|
|
808
|
+
) as HTMLSelectElement
|
|
809
|
+
).value
|
|
810
|
+
).toBe("option3");
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("removes a library", async () => {
|
|
814
|
+
const user = userEvent.setup();
|
|
815
|
+
const { container } = renderForm({ item: serviceData });
|
|
816
|
+
|
|
817
|
+
let libraries = container.querySelectorAll(".with-remove-button");
|
|
818
|
+
expect(libraries).toHaveLength(1);
|
|
819
|
+
expect(libraries[0]).toHaveTextContent("New York Public Library");
|
|
820
|
+
|
|
821
|
+
// The remove button lives inside a collapsed (aria-hidden) panel, so
|
|
822
|
+
// click it by class rather than by accessible role.
|
|
823
|
+
await user.click(
|
|
824
|
+
(libraries[0] as HTMLElement).querySelector(
|
|
825
|
+
"button.remove-btn"
|
|
826
|
+
) as HTMLElement
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
libraries = container.querySelectorAll(".with-remove-button");
|
|
830
|
+
expect(libraries).toHaveLength(0);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it("edits a library", async () => {
|
|
834
|
+
const user = userEvent.setup();
|
|
835
|
+
const { container } = renderForm({ item: serviceData });
|
|
836
|
+
|
|
837
|
+
const libraryItem = container.querySelector(
|
|
838
|
+
".with-remove-button"
|
|
839
|
+
) as HTMLElement;
|
|
840
|
+
expect(libraryItem).toHaveTextContent("New York Public Library");
|
|
841
|
+
|
|
842
|
+
// The edit button lives inside a collapsed (aria-hidden) panel, so click
|
|
843
|
+
// it by class rather than by accessible role.
|
|
844
|
+
const clickEdit = () =>
|
|
845
|
+
user.click(libraryItem.querySelector("button.edit-btn") as HTMLElement);
|
|
846
|
+
|
|
847
|
+
// Open the edit settings; they start with the library's current values.
|
|
848
|
+
await clickEdit();
|
|
849
|
+
let editSection = container.querySelector(
|
|
850
|
+
".edit-library-settings"
|
|
851
|
+
) as HTMLElement;
|
|
852
|
+
expect(editSection).toBeInTheDocument();
|
|
853
|
+
let textInput = editSection.querySelector(
|
|
854
|
+
'input[name="library_text_setting"]'
|
|
855
|
+
) as HTMLInputElement;
|
|
856
|
+
let selectInput = editSection.querySelector(
|
|
857
|
+
'select[name="library_select_setting"]'
|
|
858
|
+
) as HTMLSelectElement;
|
|
859
|
+
expect(textInput.value).toBe("library text setting");
|
|
860
|
+
expect(selectInput.value).toBe("option4");
|
|
861
|
+
|
|
862
|
+
// Change the values but collapse without saving.
|
|
863
|
+
await user.clear(textInput);
|
|
864
|
+
await user.type(textInput, "new library text");
|
|
865
|
+
await user.selectOptions(selectInput, "option3");
|
|
866
|
+
await clickEdit();
|
|
867
|
+
expect(container.querySelector(".edit-library-settings")).toBeNull();
|
|
868
|
+
|
|
869
|
+
// Reopening shows the original (unsaved) values.
|
|
870
|
+
await clickEdit();
|
|
871
|
+
editSection = container.querySelector(
|
|
872
|
+
".edit-library-settings"
|
|
873
|
+
) as HTMLElement;
|
|
874
|
+
textInput = editSection.querySelector(
|
|
875
|
+
'input[name="library_text_setting"]'
|
|
876
|
+
) as HTMLInputElement;
|
|
877
|
+
selectInput = editSection.querySelector(
|
|
878
|
+
'select[name="library_select_setting"]'
|
|
879
|
+
) as HTMLSelectElement;
|
|
880
|
+
expect(textInput.value).toBe("library text setting");
|
|
881
|
+
expect(selectInput.value).toBe("option4");
|
|
882
|
+
|
|
883
|
+
// This time change the values and click Save.
|
|
884
|
+
await user.clear(textInput);
|
|
885
|
+
await user.type(textInput, "new library text");
|
|
886
|
+
await user.selectOptions(selectInput, "option3");
|
|
887
|
+
await user.click(
|
|
888
|
+
editSection.querySelector("button.edit-library") as HTMLElement
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
// The library remains (short_name unchanged) and reopening the editor
|
|
892
|
+
// shows the newly saved values.
|
|
893
|
+
const libraries = container.querySelectorAll(".with-remove-button");
|
|
894
|
+
expect(libraries).toHaveLength(1);
|
|
895
|
+
await clickEdit();
|
|
896
|
+
editSection = container.querySelector(
|
|
897
|
+
".edit-library-settings"
|
|
898
|
+
) as HTMLElement;
|
|
899
|
+
expect(
|
|
900
|
+
(
|
|
901
|
+
editSection.querySelector(
|
|
902
|
+
'input[name="library_text_setting"]'
|
|
903
|
+
) as HTMLInputElement
|
|
904
|
+
).value
|
|
905
|
+
).toBe("new library text");
|
|
906
|
+
expect(
|
|
907
|
+
(
|
|
908
|
+
editSection.querySelector(
|
|
909
|
+
'select[name="library_select_setting"]'
|
|
910
|
+
) as HTMLSelectElement
|
|
911
|
+
).value
|
|
912
|
+
).toBe("option3");
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it("calls save when the form is submitted", async () => {
|
|
916
|
+
// The reusable Form wires submission solely through its submit button's
|
|
917
|
+
// onClick, so clicking it is the only way to submit; assert the resulting
|
|
918
|
+
// save receives the built form data.
|
|
919
|
+
const user = userEvent.setup();
|
|
920
|
+
const { container } = renderForm();
|
|
921
|
+
|
|
922
|
+
await user.click(
|
|
923
|
+
within(container).getByRole("button", { name: "Submit" })
|
|
924
|
+
);
|
|
925
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
926
|
+
expect(save.mock.calls[0][0]).toBeInstanceOf(FormDataShim);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it("calls save on submit even if there is a collapsible panel", async () => {
|
|
930
|
+
const user = userEvent.setup();
|
|
931
|
+
const { container } = renderForm();
|
|
932
|
+
|
|
933
|
+
await user.selectOptions(
|
|
934
|
+
container.querySelector('select[name="protocol"]') as HTMLSelectElement,
|
|
935
|
+
"protocol with instructions"
|
|
936
|
+
);
|
|
937
|
+
expect(container.querySelectorAll(".panel")).toHaveLength(3);
|
|
938
|
+
|
|
939
|
+
await user.click(
|
|
940
|
+
within(container).getByRole("button", { name: "Submit" })
|
|
941
|
+
);
|
|
942
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it("calls handleData", async () => {
|
|
946
|
+
// handleData has no direct DOM effect; observe it via the side effect it
|
|
947
|
+
// performs on submit — appending the JSON-encoded libraries to the data
|
|
948
|
+
// passed to `save`.
|
|
949
|
+
const user = userEvent.setup();
|
|
950
|
+
const { container } = renderForm({ item: serviceData });
|
|
951
|
+
|
|
952
|
+
await user.click(
|
|
953
|
+
within(container).getByRole("button", { name: "Submit" })
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
957
|
+
const formData = save.mock.calls[0][0];
|
|
958
|
+
expect(formData.get("libraries")).toBe(
|
|
959
|
+
JSON.stringify(serviceData.libraries)
|
|
960
|
+
);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it("submits data", async () => {
|
|
964
|
+
const user = userEvent.setup();
|
|
965
|
+
const { container } = renderForm({ item: serviceData });
|
|
966
|
+
|
|
967
|
+
await user.click(
|
|
968
|
+
within(container).getByRole("button", { name: "Submit" })
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
972
|
+
const formData = save.mock.calls[0][0];
|
|
973
|
+
expect(formData.get("id")).toBe("2");
|
|
974
|
+
expect(formData.get("protocol")).toBe("protocol 1");
|
|
975
|
+
expect(formData.get("text_setting")).toBe("text setting");
|
|
976
|
+
expect(formData.get("select_setting")).toBe("option2");
|
|
977
|
+
expect(formData.get("libraries")).toBe(
|
|
978
|
+
JSON.stringify(serviceData.libraries)
|
|
979
|
+
);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it("clears the form", async () => {
|
|
983
|
+
const user = userEvent.setup();
|
|
984
|
+
const { container, rerender } = renderForm();
|
|
985
|
+
|
|
986
|
+
const nameInput = () =>
|
|
987
|
+
container.querySelector('input[name="name"]') as HTMLInputElement;
|
|
988
|
+
await user.type(nameInput(), "new service");
|
|
989
|
+
expect(nameInput().value).toBe("new service");
|
|
990
|
+
|
|
991
|
+
// A successful response (no error) clears the form fields.
|
|
992
|
+
rerenderForm(rerender, { responseBody: "new service" });
|
|
993
|
+
expect(nameInput().value).toBe("");
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it("doesn't clear the form if there's an error message", async () => {
|
|
997
|
+
const user = userEvent.setup();
|
|
998
|
+
const { container, rerender } = renderForm();
|
|
999
|
+
|
|
1000
|
+
const nameInput = () =>
|
|
1001
|
+
container.querySelector('input[name="name"]') as HTMLInputElement;
|
|
1002
|
+
await user.type(nameInput(), "new service");
|
|
1003
|
+
expect(nameInput().value).toBe("new service");
|
|
1004
|
+
|
|
1005
|
+
// A response accompanied by a fetch error must NOT clear the form.
|
|
1006
|
+
// `fetchError` is read by the component but is not a declared prop, so it
|
|
1007
|
+
// is passed via an untyped props object.
|
|
1008
|
+
const propsWithError: any = {
|
|
1009
|
+
disabled: false,
|
|
1010
|
+
data: servicesData,
|
|
1011
|
+
save,
|
|
1012
|
+
urlBase,
|
|
1013
|
+
listDataKey: "services",
|
|
1014
|
+
responseBody: "new service",
|
|
1015
|
+
fetchError: "ERROR",
|
|
1016
|
+
};
|
|
1017
|
+
rerender(<TestServiceEditForm {...propsWithError} />);
|
|
1018
|
+
expect(nameInput().value).toBe("new service");
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
describe("library removal confirmation", () => {
|
|
1023
|
+
// The base class's `isLibraryRemovalPermitted` always returns true and is a
|
|
1024
|
+
// documented override hook. Rather than stubbing a live instance method
|
|
1025
|
+
// (impossible in RTL), subclass the form to control the return value and
|
|
1026
|
+
// assert the observable outcome of clicking the real remove button.
|
|
1027
|
+
class AllowRemovalServiceEditForm extends ServiceEditForm<ServicesData> {
|
|
1028
|
+
isLibraryRemovalPermitted(): boolean {
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
class BlockRemovalServiceEditForm extends ServiceEditForm<ServicesData> {
|
|
1033
|
+
isLibraryRemovalPermitted(): boolean {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const commonProps = () => ({
|
|
1039
|
+
disabled: false,
|
|
1040
|
+
data: servicesData,
|
|
1041
|
+
save,
|
|
1042
|
+
item: serviceData,
|
|
1043
|
+
urlBase,
|
|
1044
|
+
listDataKey: "services",
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const clickRemove = async (container: HTMLElement) => {
|
|
1048
|
+
const user = userEvent.setup();
|
|
1049
|
+
// The remove button lives inside a collapsed (aria-hidden) panel, so
|
|
1050
|
+
// click it by class rather than by accessible role.
|
|
1051
|
+
const removeButton = container.querySelector(
|
|
1052
|
+
".with-remove-button button.remove-btn"
|
|
1053
|
+
) as HTMLElement;
|
|
1054
|
+
await user.click(removeButton);
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
it("should allow removal by default", async () => {
|
|
1058
|
+
// Base class: removal permitted by default.
|
|
1059
|
+
const { container } = render(<TestServiceEditForm {...commonProps()} />);
|
|
1060
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(1);
|
|
1061
|
+
|
|
1062
|
+
await clickRemove(container);
|
|
1063
|
+
|
|
1064
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(0);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it("should allow remove if isLibraryRemovalPermitted returns true", async () => {
|
|
1068
|
+
const { container } = render(
|
|
1069
|
+
<AllowRemovalServiceEditForm {...commonProps()} />
|
|
1070
|
+
);
|
|
1071
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(1);
|
|
1072
|
+
|
|
1073
|
+
await clickRemove(container);
|
|
1074
|
+
|
|
1075
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(0);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it("should not allow remove if isLibraryRemovalPermitted returns false", async () => {
|
|
1079
|
+
const { container } = render(
|
|
1080
|
+
<BlockRemovalServiceEditForm {...commonProps()} />
|
|
1081
|
+
);
|
|
1082
|
+
expect(container.querySelectorAll(".with-remove-button")).toHaveLength(1);
|
|
1083
|
+
|
|
1084
|
+
await clickRemove(container);
|
|
1085
|
+
|
|
1086
|
+
// Removal is blocked: the library remains.
|
|
1087
|
+
const libraries = container.querySelectorAll(".with-remove-button");
|
|
1088
|
+
expect(libraries).toHaveLength(1);
|
|
1089
|
+
expect(libraries[0]).toHaveTextContent("New York Public Library");
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
});
|