@thepalaceproject/circulation-admin 1.43.0 → 1.44.0-post.3
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 +28 -7
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +22 -8
- package/package.json +6 -20
- package/tests/jest/actions.test.ts +962 -0
- package/tests/jest/components/AccountPage.test.tsx +41 -0
- package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +93 -20
- package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
- package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
- package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
- package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -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/BookDetails.test.tsx +133 -0
- package/tests/jest/components/BookDetailsContainer.test.tsx +116 -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 +100 -0
- package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -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/ConfigPage.test.tsx +58 -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/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomLists.test.tsx +621 -4
- package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
- package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
- package/tests/jest/components/DashboardPage.test.tsx +76 -0
- package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
- package/tests/jest/components/EditableConfigList.test.tsx +606 -9
- package/tests/jest/components/EditableInput.test.tsx +250 -1
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
- package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
- package/tests/jest/components/ErrorMessage.test.tsx +97 -0
- package/tests/jest/components/Footer.test.tsx +141 -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/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/JsonField.test.tsx +20 -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 -13
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -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/LoadButton.test.tsx +49 -0
- package/tests/jest/components/ManagePatrons.test.tsx +73 -0
- package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
- package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
- package/tests/jest/components/MetadataServices.test.tsx +95 -0
- 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/PatronBlockingRulesEditor.test.tsx +3 -1
- package/tests/jest/components/PatronInfo.test.tsx +54 -0
- package/tests/jest/components/ProtocolFormField.test.tsx +479 -7
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SaveButton.test.tsx +47 -0
- package/tests/jest/components/SelfTestResult.test.tsx +164 -0
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
- package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
- package/tests/jest/components/SetupPage.test.tsx +41 -0
- package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +4 -15
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -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/components/TroubleshootingTabContainer.test.tsx +68 -0
- package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
- package/tests/jest/components/WelcomePage.test.tsx +43 -0
- package/tests/jest/components/WithEditButton.test.tsx +55 -0
- package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
- package/tests/jest/editorAdapter.test.ts +245 -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/index.test.tsx +50 -0
- package/tests/jest/models/Admin.test.ts +206 -0
- package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
- package/tests/jest/reducers/classifications.test.ts +161 -0
- package/tests/jest/reducers/collections.test.ts +96 -0
- package/tests/jest/reducers/complaints.test.ts +166 -0
- package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
- package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
- package/tests/jest/reducers/customListDetails.test.ts +120 -0
- package/tests/jest/reducers/customListEditor.test.ts +2823 -0
- package/tests/jest/reducers/managePatrons.test.ts +71 -0
- package/tests/jest/testUtils/formDataShim.test.ts +135 -0
- package/tests/jest/testUtils/formDataShim.ts +112 -0
- package/tests/jest/utils/sharedFunctions.test.ts +121 -0
- package/tsconfig.json +3 -4
- package/testReporter.js +0 -29
- package/tests/jest/README.md +0 -6
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
fireEvent,
|
|
6
|
+
act,
|
|
7
|
+
within,
|
|
8
|
+
waitFor,
|
|
9
|
+
} from "@testing-library/react";
|
|
3
10
|
import userEvent from "@testing-library/user-event";
|
|
4
11
|
import ProtocolFormField from "../../../src/components/ProtocolFormField";
|
|
5
|
-
|
|
6
|
-
// NB: This file adds / duplicates existing tests from:
|
|
7
|
-
// - `src/components/__tests__/ProtocolFormField-test.tsx`.
|
|
8
|
-
//
|
|
9
|
-
// Those tests should eventually be migrated here and
|
|
10
|
-
// adapted to the Jest/React Testing Library paradigm.
|
|
12
|
+
import { MONOSPACE_FONT_STACK } from "../../../src/fonts";
|
|
11
13
|
|
|
12
14
|
describe("ProtocolFormField — json type", () => {
|
|
13
15
|
const jsonSetting = {
|
|
@@ -64,6 +66,41 @@ describe("ProtocolFormField — json type", () => {
|
|
|
64
66
|
});
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
describe("ProtocolFormField — use_monospace_font", () => {
|
|
70
|
+
(
|
|
71
|
+
[
|
|
72
|
+
{ id: "text-input", use_monospace_font: true, hasStyle: true },
|
|
73
|
+
{
|
|
74
|
+
id: "textarea",
|
|
75
|
+
type: "textarea",
|
|
76
|
+
use_monospace_font: true,
|
|
77
|
+
hasStyle: true,
|
|
78
|
+
},
|
|
79
|
+
{ id: "absent", hasStyle: false },
|
|
80
|
+
] as {
|
|
81
|
+
id: string;
|
|
82
|
+
type?: string;
|
|
83
|
+
use_monospace_font?: boolean;
|
|
84
|
+
hasStyle: boolean;
|
|
85
|
+
}[]
|
|
86
|
+
).forEach(({ id, type, use_monospace_font, hasStyle }) => {
|
|
87
|
+
it(id, () => {
|
|
88
|
+
render(
|
|
89
|
+
<ProtocolFormField
|
|
90
|
+
setting={{ key: "f", label: "My Field", type, use_monospace_font }}
|
|
91
|
+
disabled={false}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
const el = screen.getByLabelText("My Field");
|
|
95
|
+
if (hasStyle) {
|
|
96
|
+
expect(el).toHaveStyle({ fontFamily: MONOSPACE_FONT_STACK });
|
|
97
|
+
} else {
|
|
98
|
+
expect(el).not.toHaveStyle({ fontFamily: MONOSPACE_FONT_STACK });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
67
104
|
describe("ProtocolFormField", () => {
|
|
68
105
|
it("renders date-picker setting", async () => {
|
|
69
106
|
const user = userEvent.setup();
|
|
@@ -90,3 +127,438 @@ describe("ProtocolFormField", () => {
|
|
|
90
127
|
expect(input.value).toBe(testDate);
|
|
91
128
|
});
|
|
92
129
|
});
|
|
130
|
+
|
|
131
|
+
// These render each setting `type` and assert the rendered control via the DOM,
|
|
132
|
+
// reading uncontrolled values through the component's real ref-based `getValue()`
|
|
133
|
+
// / `clear()` API (the same API production calls), like the json-type tests above.
|
|
134
|
+
describe("ProtocolFormField — setting types", () => {
|
|
135
|
+
const baseSetting = {
|
|
136
|
+
key: "setting",
|
|
137
|
+
label: "label",
|
|
138
|
+
description: "<p>description</p>",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const menuOptions = ["A", "B", "C"].map((x) => (
|
|
142
|
+
<option key={x} value={x} aria-selected={false}>
|
|
143
|
+
{x}
|
|
144
|
+
</option>
|
|
145
|
+
));
|
|
146
|
+
|
|
147
|
+
it("renders a text setting and reads/clears its value via ref", () => {
|
|
148
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
149
|
+
render(
|
|
150
|
+
<ProtocolFormField
|
|
151
|
+
setting={baseSetting}
|
|
152
|
+
disabled={false}
|
|
153
|
+
value="test"
|
|
154
|
+
ref={ref}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
const input = screen.getByLabelText("label") as HTMLInputElement;
|
|
158
|
+
expect(input.tagName).toBe("INPUT");
|
|
159
|
+
expect(input.value).toBe("test");
|
|
160
|
+
expect(ref.current!.getValue()).toBe("test");
|
|
161
|
+
|
|
162
|
+
act(() => {
|
|
163
|
+
ref.current!.clear();
|
|
164
|
+
});
|
|
165
|
+
expect(input.value).toBe("");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("shows an (Optional) prefix on the description of a non-required field", () => {
|
|
169
|
+
const { container } = render(
|
|
170
|
+
<ProtocolFormField setting={baseSetting} disabled={false} />
|
|
171
|
+
);
|
|
172
|
+
expect(container.querySelector(".description")).toHaveTextContent(
|
|
173
|
+
"(Optional) description"
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("renders a setting with a default value", () => {
|
|
178
|
+
render(
|
|
179
|
+
<ProtocolFormField
|
|
180
|
+
setting={{ ...baseSetting, default: "default" }}
|
|
181
|
+
disabled={false}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
expect((screen.getByLabelText("label") as HTMLInputElement).value).toBe(
|
|
185
|
+
"default"
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("renders a number setting", () => {
|
|
190
|
+
render(
|
|
191
|
+
<ProtocolFormField
|
|
192
|
+
setting={{ ...baseSetting, type: "number" }}
|
|
193
|
+
disabled={false}
|
|
194
|
+
value="42"
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
expect((screen.getByLabelText("label") as HTMLInputElement).value).toBe(
|
|
198
|
+
"42"
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("renders a textarea setting", () => {
|
|
203
|
+
render(
|
|
204
|
+
<ProtocolFormField
|
|
205
|
+
setting={{ ...baseSetting, type: "textarea" }}
|
|
206
|
+
disabled={false}
|
|
207
|
+
value="test"
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
const ta = screen.getByLabelText("label") as HTMLTextAreaElement;
|
|
211
|
+
expect(ta.tagName).toBe("TEXTAREA");
|
|
212
|
+
expect(ta.value).toBe("test");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("renders a select setting with its options", () => {
|
|
216
|
+
render(
|
|
217
|
+
<ProtocolFormField
|
|
218
|
+
setting={{
|
|
219
|
+
...baseSetting,
|
|
220
|
+
type: "select",
|
|
221
|
+
options: [
|
|
222
|
+
{ key: "option1", label: "option 1" },
|
|
223
|
+
{ key: "option2", label: "option 2" },
|
|
224
|
+
],
|
|
225
|
+
}}
|
|
226
|
+
disabled={false}
|
|
227
|
+
/>
|
|
228
|
+
);
|
|
229
|
+
const select = screen.getByLabelText("label") as HTMLSelectElement;
|
|
230
|
+
const options = within(select).getAllByRole(
|
|
231
|
+
"option"
|
|
232
|
+
) as HTMLOptionElement[];
|
|
233
|
+
expect(options).toHaveLength(2);
|
|
234
|
+
expect(options[0].value).toBe("option1");
|
|
235
|
+
expect(options[0]).toHaveTextContent("option 1");
|
|
236
|
+
expect(options[1].value).toBe("option2");
|
|
237
|
+
expect(options[1]).toHaveTextContent("option 2");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("renders a randomizable setting whose button generates a 32-char value", async () => {
|
|
241
|
+
const user = userEvent.setup();
|
|
242
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
243
|
+
render(
|
|
244
|
+
<ProtocolFormField
|
|
245
|
+
setting={{ ...baseSetting, randomizable: true }}
|
|
246
|
+
disabled={false}
|
|
247
|
+
ref={ref}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
const button = screen.getByRole("button", { name: /random/i });
|
|
251
|
+
await user.click(button);
|
|
252
|
+
const value = ref.current!.getValue();
|
|
253
|
+
expect(value).toHaveLength(32);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("renders an image setting with a preview img", () => {
|
|
257
|
+
const { container } = render(
|
|
258
|
+
<ProtocolFormField
|
|
259
|
+
setting={{ ...baseSetting, type: "image" }}
|
|
260
|
+
disabled={false}
|
|
261
|
+
value="image data"
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
const img = container.querySelector("img");
|
|
265
|
+
expect(img).not.toBeNull();
|
|
266
|
+
expect(img!.getAttribute("src")).toBe("image data");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("renders a color-picker setting with its label and default value", () => {
|
|
270
|
+
const { container } = render(
|
|
271
|
+
<ProtocolFormField
|
|
272
|
+
setting={{ ...baseSetting, type: "color-picker", default: "#aaaaaa" }}
|
|
273
|
+
disabled={false}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
expect(screen.getByText("label")).toBeInTheDocument();
|
|
277
|
+
const hidden = container.querySelector(
|
|
278
|
+
'input[type="hidden"]'
|
|
279
|
+
) as HTMLInputElement;
|
|
280
|
+
expect(hidden.value).toBe("#aaaaaa");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("renders a list setting and reads its value via ref", () => {
|
|
284
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
285
|
+
render(
|
|
286
|
+
<ProtocolFormField
|
|
287
|
+
setting={{ ...baseSetting, type: "list" }}
|
|
288
|
+
disabled={false}
|
|
289
|
+
value={["item 1", "item 2"]}
|
|
290
|
+
ref={ref}
|
|
291
|
+
/>
|
|
292
|
+
);
|
|
293
|
+
// The InputList renders an "Add" button.
|
|
294
|
+
expect(screen.getByRole("button", { name: /add/i })).toBeInTheDocument();
|
|
295
|
+
expect(ref.current!.getValue()).toEqual(["item 1", "item 2"]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("optionally renders instructions for a list setting", () => {
|
|
299
|
+
const { container } = render(
|
|
300
|
+
<ProtocolFormField
|
|
301
|
+
setting={{
|
|
302
|
+
...baseSetting,
|
|
303
|
+
type: "list",
|
|
304
|
+
instructions: "<ul><li>Step 1</li></ul>",
|
|
305
|
+
}}
|
|
306
|
+
disabled={false}
|
|
307
|
+
/>
|
|
308
|
+
);
|
|
309
|
+
const instructions = container.querySelector(".well");
|
|
310
|
+
expect(instructions).not.toBeNull();
|
|
311
|
+
expect(instructions).toHaveClass("description");
|
|
312
|
+
expect(instructions).toHaveTextContent("Step 1");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("renders a menu setting as a select", () => {
|
|
316
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
317
|
+
const { container } = render(
|
|
318
|
+
<ProtocolFormField
|
|
319
|
+
setting={{ ...baseSetting, type: "menu", menuOptions }}
|
|
320
|
+
disabled={false}
|
|
321
|
+
value={[]}
|
|
322
|
+
altValue="Alternate"
|
|
323
|
+
ref={ref}
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
326
|
+
expect(container.querySelector("select")).not.toBeNull();
|
|
327
|
+
expect(ref.current!.getValue()).toEqual([]);
|
|
328
|
+
// With no items selected, the InputList shows the alt text in their place.
|
|
329
|
+
expect(screen.getByText("Alternate")).toBeInTheDocument();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("renders a hidden setting inside an invisible wrapper and reads its value via ref", () => {
|
|
333
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
334
|
+
render(
|
|
335
|
+
<ProtocolFormField
|
|
336
|
+
setting={{ ...baseSetting, hidden: true }}
|
|
337
|
+
disabled={false}
|
|
338
|
+
value="test"
|
|
339
|
+
ref={ref}
|
|
340
|
+
/>
|
|
341
|
+
);
|
|
342
|
+
// The hidden element is wrapped in a visually-hidden div.
|
|
343
|
+
const wrapper = screen.getByLabelText("label").closest("div[style]");
|
|
344
|
+
expect(wrapper).toHaveStyle({ visibility: "hidden" });
|
|
345
|
+
expect(ref.current!.getValue()).toBe("test");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("reads the value of a hidden list setting via ref", () => {
|
|
349
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
350
|
+
render(
|
|
351
|
+
<ProtocolFormField
|
|
352
|
+
setting={{ ...baseSetting, hidden: true, type: "list" }}
|
|
353
|
+
disabled={false}
|
|
354
|
+
value={["item 1", "item 2"]}
|
|
355
|
+
ref={ref}
|
|
356
|
+
/>
|
|
357
|
+
);
|
|
358
|
+
expect(ref.current!.getValue()).toEqual(["item 1", "item 2"]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("resolves a hidden select value: first option, then default, then explicit value", () => {
|
|
362
|
+
const options = [
|
|
363
|
+
{ key: "option1", label: "option 1" },
|
|
364
|
+
{ key: "option2", label: "option 2" },
|
|
365
|
+
{ key: "option3", label: "option 3" },
|
|
366
|
+
];
|
|
367
|
+
const selectValue = (extra: Record<string, unknown>, value?: string) => {
|
|
368
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
369
|
+
render(
|
|
370
|
+
<ProtocolFormField
|
|
371
|
+
setting={
|
|
372
|
+
{
|
|
373
|
+
...baseSetting,
|
|
374
|
+
hidden: true,
|
|
375
|
+
type: "select",
|
|
376
|
+
options,
|
|
377
|
+
...extra,
|
|
378
|
+
} as any
|
|
379
|
+
}
|
|
380
|
+
disabled={false}
|
|
381
|
+
value={value}
|
|
382
|
+
ref={ref}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
return ref.current!.getValue();
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// No value and no default -> the first option.
|
|
389
|
+
expect(selectValue({})).toBe("option1");
|
|
390
|
+
// A default with no value -> the default.
|
|
391
|
+
expect(selectValue({ default: "option3" })).toBe("option3");
|
|
392
|
+
// An explicit value -> that value, overriding the default.
|
|
393
|
+
expect(selectValue({ default: "option3" }, "option2")).toBe("option2");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Hidden settings render the same element inside an invisible wrapper, so each
|
|
397
|
+
// variant must still surface its value through its ref (elementRef for the
|
|
398
|
+
// scalar inputs, colorPickerRef for the color picker, inputListRef for the
|
|
399
|
+
// menu; hidden text and hidden list are covered above).
|
|
400
|
+
it.each([
|
|
401
|
+
{
|
|
402
|
+
name: "date-picker",
|
|
403
|
+
extra: { type: "date-picker" },
|
|
404
|
+
value: "2020-06-01",
|
|
405
|
+
},
|
|
406
|
+
{ name: "number", extra: { type: "number" }, value: "42" },
|
|
407
|
+
{ name: "textarea", extra: { type: "textarea" }, value: "some text" },
|
|
408
|
+
{ name: "color-picker", extra: { type: "color-picker" }, value: "#123456" },
|
|
409
|
+
{ name: "randomizable", extra: { randomizable: true }, value: "test" },
|
|
410
|
+
{ name: "menu", extra: { type: "menu", menuOptions }, value: ["A"] },
|
|
411
|
+
])(
|
|
412
|
+
"reads a hidden $name setting's value back via its ref",
|
|
413
|
+
({ extra, value }) => {
|
|
414
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
415
|
+
render(
|
|
416
|
+
<ProtocolFormField
|
|
417
|
+
setting={{ ...baseSetting, hidden: true, ...extra } as any}
|
|
418
|
+
disabled={false}
|
|
419
|
+
value={value as any}
|
|
420
|
+
ref={ref}
|
|
421
|
+
/>
|
|
422
|
+
);
|
|
423
|
+
expect(ref.current!.getValue()).toEqual(value);
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
it("falls back to the setting's default for a hidden setting with no value", () => {
|
|
428
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
429
|
+
render(
|
|
430
|
+
<ProtocolFormField
|
|
431
|
+
setting={
|
|
432
|
+
{ ...baseSetting, hidden: true, default: "the default" } as any
|
|
433
|
+
}
|
|
434
|
+
disabled={false}
|
|
435
|
+
ref={ref}
|
|
436
|
+
/>
|
|
437
|
+
);
|
|
438
|
+
expect(ref.current!.getValue()).toBe("the default");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("does not submit a stale value through a hidden image setting", () => {
|
|
442
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
443
|
+
const { container } = render(
|
|
444
|
+
<ProtocolFormField
|
|
445
|
+
setting={{ ...baseSetting, hidden: true, type: "image" } as any}
|
|
446
|
+
disabled={false}
|
|
447
|
+
value="data:image/png;base64,abc"
|
|
448
|
+
ref={ref}
|
|
449
|
+
/>
|
|
450
|
+
);
|
|
451
|
+
// An image setting is a file input whose value is forced to undefined, so it
|
|
452
|
+
// submits nothing; the current value is surfaced as a preview instead.
|
|
453
|
+
expect(ref.current!.getValue()).toBe("");
|
|
454
|
+
expect(container.querySelector("img")).toHaveAttribute(
|
|
455
|
+
"src",
|
|
456
|
+
"data:image/png;base64,abc"
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("forwards onChange to the underlying EditableInput", async () => {
|
|
461
|
+
const user = userEvent.setup();
|
|
462
|
+
const onChange = jest.fn();
|
|
463
|
+
render(
|
|
464
|
+
<ProtocolFormField
|
|
465
|
+
setting={baseSetting}
|
|
466
|
+
disabled={false}
|
|
467
|
+
onChange={onChange}
|
|
468
|
+
/>
|
|
469
|
+
);
|
|
470
|
+
await user.type(screen.getByLabelText("label"), "x");
|
|
471
|
+
expect(onChange).toHaveBeenCalled();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("forwards onChange to the InputList (a list setting reports add/remove)", async () => {
|
|
475
|
+
const user = userEvent.setup();
|
|
476
|
+
const onChange = jest.fn();
|
|
477
|
+
render(
|
|
478
|
+
<ProtocolFormField
|
|
479
|
+
setting={{ ...baseSetting, type: "list" }}
|
|
480
|
+
disabled={false}
|
|
481
|
+
value={[]}
|
|
482
|
+
onChange={onChange}
|
|
483
|
+
/>
|
|
484
|
+
);
|
|
485
|
+
await user.type(screen.getByRole("textbox"), "new item");
|
|
486
|
+
await user.click(screen.getByRole("button", { name: /add/i }));
|
|
487
|
+
await waitFor(() => expect(onChange).toHaveBeenCalled());
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("forwards readOnly to the InputList (list item inputs are read-only)", () => {
|
|
491
|
+
render(
|
|
492
|
+
<ProtocolFormField
|
|
493
|
+
setting={{ ...baseSetting, type: "list" }}
|
|
494
|
+
disabled={false}
|
|
495
|
+
value={["item 1"]}
|
|
496
|
+
readOnly
|
|
497
|
+
/>
|
|
498
|
+
);
|
|
499
|
+
expect(
|
|
500
|
+
(screen.getByDisplayValue("item 1") as HTMLInputElement).readOnly
|
|
501
|
+
).toBe(true);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// A `menu` setting also goes through InputList, but its add-control is built by
|
|
505
|
+
// InputList.renderMenu(), which does not restate onChange/readOnly — it relies
|
|
506
|
+
// on them arriving through createEditableInput. That is a different path from
|
|
507
|
+
// the `list` add-control above, so it needs its own coverage.
|
|
508
|
+
it("forwards onChange to the InputList menu control", async () => {
|
|
509
|
+
const user = userEvent.setup();
|
|
510
|
+
const onChange = jest.fn();
|
|
511
|
+
render(
|
|
512
|
+
<ProtocolFormField
|
|
513
|
+
setting={{ ...baseSetting, type: "menu", menuOptions } as any}
|
|
514
|
+
disabled={false}
|
|
515
|
+
value={[]}
|
|
516
|
+
onChange={onChange}
|
|
517
|
+
/>
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
await user.selectOptions(screen.getByRole("combobox"), "B");
|
|
521
|
+
|
|
522
|
+
expect(onChange).toHaveBeenCalled();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("forwards readOnly to the InputList menu control", () => {
|
|
526
|
+
const onChange = jest.fn();
|
|
527
|
+
render(
|
|
528
|
+
<ProtocolFormField
|
|
529
|
+
setting={{ ...baseSetting, type: "menu", menuOptions } as any}
|
|
530
|
+
disabled={false}
|
|
531
|
+
value={[]}
|
|
532
|
+
readOnly
|
|
533
|
+
onChange={onChange}
|
|
534
|
+
/>
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// A <select> ignores the readonly attribute, so dispatch the change directly:
|
|
538
|
+
// the component's own readOnly guard is what must stop onChange from firing.
|
|
539
|
+
fireEvent.change(screen.getByRole("combobox"), { target: { value: "B" } });
|
|
540
|
+
|
|
541
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("forwards disableButton to the InputList (the Add button is disabled)", () => {
|
|
545
|
+
// Use a menu: for the other types InputList also disables Add whenever its
|
|
546
|
+
// text input is empty, which would mask whether disableButton was forwarded.
|
|
547
|
+
const renderMenu = (extra: Record<string, unknown> = {}) =>
|
|
548
|
+
render(
|
|
549
|
+
<ProtocolFormField
|
|
550
|
+
setting={{ ...baseSetting, type: "menu", menuOptions } as any}
|
|
551
|
+
disabled={false}
|
|
552
|
+
value={[]}
|
|
553
|
+
{...extra}
|
|
554
|
+
/>
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
const { unmount } = renderMenu();
|
|
558
|
+
expect(screen.getByRole("button", { name: /add/i })).not.toBeDisabled();
|
|
559
|
+
unmount();
|
|
560
|
+
|
|
561
|
+
renderMenu({ disableButton: true });
|
|
562
|
+
expect(screen.getByRole("button", { name: /add/i })).toBeDisabled();
|
|
563
|
+
});
|
|
564
|
+
});
|