@thepalaceproject/circulation-admin 1.43.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, act, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import ProtocolFormField from "../../../src/components/ProtocolFormField";
|
|
6
|
+
|
|
7
|
+
// InputList is always rendered by ProtocolFormField, which supplies the real
|
|
8
|
+
// `createEditableInput` and `labelAndDescription` callbacks it depends on.
|
|
9
|
+
// Rendering through ProtocolFormField exercises that wiring while letting us
|
|
10
|
+
// assert the DOM the InputList actually produces. The imperative `getValue()` /
|
|
11
|
+
// `clear()` API is exposed through ProtocolFormField's ref, which is how
|
|
12
|
+
// production calls it.
|
|
13
|
+
|
|
14
|
+
const setting = {
|
|
15
|
+
key: "setting",
|
|
16
|
+
label: "label",
|
|
17
|
+
description: "description",
|
|
18
|
+
type: "list",
|
|
19
|
+
};
|
|
20
|
+
const value = ["Thing 1", "Thing 2"];
|
|
21
|
+
|
|
22
|
+
const renderList = (
|
|
23
|
+
{ setting: s = setting, value: v = value, ...rest }: any = {},
|
|
24
|
+
ref?: React.Ref<ProtocolFormField>
|
|
25
|
+
) =>
|
|
26
|
+
render(
|
|
27
|
+
<ProtocolFormField
|
|
28
|
+
ref={ref}
|
|
29
|
+
setting={s}
|
|
30
|
+
value={v}
|
|
31
|
+
disabled={false}
|
|
32
|
+
{...rest}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// The list-item inputs live in the `ul.input-list-ul`; the "add" input lives in
|
|
37
|
+
// `.add-list-item-container`.
|
|
38
|
+
const itemInputs = (container: HTMLElement) =>
|
|
39
|
+
Array.from(
|
|
40
|
+
container.querySelectorAll<HTMLInputElement>(
|
|
41
|
+
"ul.input-list-ul input.form-control"
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
const addInput = (container: HTMLElement) =>
|
|
45
|
+
container.querySelector<HTMLInputElement>(
|
|
46
|
+
".add-list-item-container span.add-list-item input"
|
|
47
|
+
);
|
|
48
|
+
const addButton = (container: HTMLElement) =>
|
|
49
|
+
container.querySelector<HTMLButtonElement>("button.add-list-item");
|
|
50
|
+
const removeButtons = (container: HTMLElement) =>
|
|
51
|
+
container.querySelectorAll("button.remove-btn");
|
|
52
|
+
const listItemWrappers = (container: HTMLElement) =>
|
|
53
|
+
container.querySelectorAll(".with-remove-button");
|
|
54
|
+
|
|
55
|
+
describe("InputList", () => {
|
|
56
|
+
it("renders a label and description", () => {
|
|
57
|
+
const { container } = renderList();
|
|
58
|
+
const labels = container.querySelectorAll("label");
|
|
59
|
+
expect(labels).toHaveLength(1);
|
|
60
|
+
expect(labels[0]).toHaveTextContent("label");
|
|
61
|
+
|
|
62
|
+
// The first `.description` is the one produced by labelAndDescription.
|
|
63
|
+
const description = container.querySelectorAll(".description")[0];
|
|
64
|
+
expect(description).toHaveTextContent("description");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("renders a list of items", () => {
|
|
68
|
+
const { container } = renderList();
|
|
69
|
+
const inputs =
|
|
70
|
+
container.querySelectorAll<HTMLInputElement>("input.form-control");
|
|
71
|
+
// Two existing items plus the empty "add" input.
|
|
72
|
+
expect(inputs).toHaveLength(3);
|
|
73
|
+
|
|
74
|
+
expect(inputs[0]).toHaveValue("Thing 1");
|
|
75
|
+
expect(inputs[0]).toHaveAttribute("type", "text");
|
|
76
|
+
expect(inputs[0]).toHaveAttribute("name", "setting");
|
|
77
|
+
|
|
78
|
+
expect(inputs[1]).toHaveValue("Thing 2");
|
|
79
|
+
expect(inputs[1]).toHaveAttribute("type", "text");
|
|
80
|
+
expect(inputs[1]).toHaveAttribute("name", "setting");
|
|
81
|
+
|
|
82
|
+
expect(inputs[2]).toHaveValue("");
|
|
83
|
+
expect(inputs[2]).toHaveAttribute("type", "text");
|
|
84
|
+
expect(inputs[2]).toHaveAttribute("name", "setting");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("optionally renders links", () => {
|
|
88
|
+
const urlBase = (itemName: string) => `admin/web/${itemName}`;
|
|
89
|
+
const { container } = renderList({ setting: { ...setting, urlBase } });
|
|
90
|
+
const links = container.querySelectorAll("a");
|
|
91
|
+
expect(links).toHaveLength(2);
|
|
92
|
+
links.forEach((link, idx) => {
|
|
93
|
+
expect(link).toHaveTextContent(`Thing ${idx + 1}`);
|
|
94
|
+
expect(link).toHaveAttribute("href", `admin/web/Thing ${idx + 1}`);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("renders WithRemoveButton elements", () => {
|
|
99
|
+
const { container } = renderList();
|
|
100
|
+
const wrappers = listItemWrappers(container);
|
|
101
|
+
expect(wrappers).toHaveLength(2);
|
|
102
|
+
const removes = removeButtons(container);
|
|
103
|
+
expect(removes).toHaveLength(2);
|
|
104
|
+
removes.forEach((btn) => expect(btn).not.toBeDisabled());
|
|
105
|
+
|
|
106
|
+
const inputs = itemInputs(container);
|
|
107
|
+
expect(inputs[0]).toHaveValue("Thing 1");
|
|
108
|
+
expect(inputs[1]).toHaveValue("Thing 2");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("optionally disables the remove buttons", () => {
|
|
112
|
+
const { container } = renderList({ disableButton: true });
|
|
113
|
+
const removes = removeButtons(container);
|
|
114
|
+
expect(removes).toHaveLength(2);
|
|
115
|
+
removes.forEach((btn) => expect(btn).toBeDisabled());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders a button for adding a list item", () => {
|
|
119
|
+
const { container } = renderList();
|
|
120
|
+
const addContainer = container.querySelector(".add-list-item-container");
|
|
121
|
+
expect(addContainer).toBeInTheDocument();
|
|
122
|
+
expect(addInput(container)).toHaveValue("");
|
|
123
|
+
// The add container itself has no remove buttons.
|
|
124
|
+
expect(addContainer.querySelectorAll(".with-remove-button")).toHaveLength(
|
|
125
|
+
0
|
|
126
|
+
);
|
|
127
|
+
expect(addButton(container)).toHaveTextContent("Add");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("optionally renders a geographic tooltip with extra content", () => {
|
|
131
|
+
const valueWithObject = [{ "Thing 3": "extra information!" }];
|
|
132
|
+
const geographicSetting = { ...setting, format: "geographic" };
|
|
133
|
+
const { container } = renderList({
|
|
134
|
+
setting: geographicSetting,
|
|
135
|
+
value: valueWithObject,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const withAddOn = container.querySelectorAll(".with-add-on");
|
|
139
|
+
expect(withAddOn).toHaveLength(1);
|
|
140
|
+
|
|
141
|
+
const addOn = withAddOn[0].querySelectorAll(".input-group-addon");
|
|
142
|
+
expect(addOn).toHaveLength(1);
|
|
143
|
+
|
|
144
|
+
expect(container.querySelector("svg.locatorIcon")).toBeInTheDocument();
|
|
145
|
+
|
|
146
|
+
const toolTip = container.querySelector(".tool-tip");
|
|
147
|
+
expect(toolTip).toHaveClass("point-right");
|
|
148
|
+
expect(toolTip).toHaveTextContent("extra information!");
|
|
149
|
+
|
|
150
|
+
const input = withAddOn[0].querySelector("input");
|
|
151
|
+
expect(input).toHaveValue("Thing 3");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("renders an autocomplete field for languages", () => {
|
|
155
|
+
const languageSetting = { ...setting, format: "language-code" };
|
|
156
|
+
const languages = {
|
|
157
|
+
eng: ["English"],
|
|
158
|
+
spa: ["Spanish", "Castilian"],
|
|
159
|
+
};
|
|
160
|
+
const { container } = renderList({
|
|
161
|
+
setting: languageSetting,
|
|
162
|
+
value: ["abc"],
|
|
163
|
+
additionalData: languages,
|
|
164
|
+
});
|
|
165
|
+
const languageInputs = container.querySelectorAll<HTMLInputElement>(
|
|
166
|
+
'input[list="setting-autocomplete-list"]'
|
|
167
|
+
);
|
|
168
|
+
// One for the existing "abc" item, one for the empty "add" field.
|
|
169
|
+
expect(languageInputs).toHaveLength(2);
|
|
170
|
+
|
|
171
|
+
expect(languageInputs[0]).toHaveValue("abc");
|
|
172
|
+
expect(languageInputs[0]).toHaveAttribute("name", "setting");
|
|
173
|
+
|
|
174
|
+
expect(languageInputs[1]).toHaveValue("");
|
|
175
|
+
expect(languageInputs[1]).toHaveAttribute("name", "setting");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("removes an item", async () => {
|
|
179
|
+
const user = userEvent.setup();
|
|
180
|
+
const { container } = renderList();
|
|
181
|
+
expect(listItemWrappers(container)).toHaveLength(2);
|
|
182
|
+
|
|
183
|
+
await user.click(removeButtons(container)[0]);
|
|
184
|
+
|
|
185
|
+
await waitFor(() => expect(listItemWrappers(container)).toHaveLength(1));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("adds a regular item", async () => {
|
|
189
|
+
const user = userEvent.setup();
|
|
190
|
+
const { container } = renderList();
|
|
191
|
+
expect(listItemWrappers(container)).toHaveLength(2);
|
|
192
|
+
|
|
193
|
+
await user.type(addInput(container), "Another thing...");
|
|
194
|
+
await user.click(addButton(container));
|
|
195
|
+
|
|
196
|
+
await waitFor(() => expect(listItemWrappers(container)).toHaveLength(3));
|
|
197
|
+
expect(addInput(container)).toHaveValue("");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("adds and capitalizes an item", async () => {
|
|
201
|
+
const user = userEvent.setup();
|
|
202
|
+
// capitalize only runs when the setting has capitalize set; the geographic
|
|
203
|
+
// format is what makes two-letter tokens uppercase.
|
|
204
|
+
const capSetting = { ...setting, capitalize: true, format: "geographic" };
|
|
205
|
+
const { container } = renderList({ setting: capSetting, value: [] });
|
|
206
|
+
|
|
207
|
+
await user.type(addInput(container), "california");
|
|
208
|
+
await user.click(addButton(container));
|
|
209
|
+
await waitFor(() =>
|
|
210
|
+
expect(itemInputs(container)[0]).toHaveValue("California")
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
await user.type(addInput(container), "fl");
|
|
214
|
+
await user.click(addButton(container));
|
|
215
|
+
await waitFor(() => expect(itemInputs(container)[1]).toHaveValue("FL"));
|
|
216
|
+
|
|
217
|
+
await user.type(addInput(container), "new york city, ny");
|
|
218
|
+
await user.click(addButton(container));
|
|
219
|
+
await waitFor(() =>
|
|
220
|
+
expect(itemInputs(container)[2]).toHaveValue("New York City, NY")
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(addInput(container)).toHaveValue("");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("adds an autocompleted item", async () => {
|
|
227
|
+
const user = userEvent.setup();
|
|
228
|
+
const languageSetting = { ...setting, format: "language-code" };
|
|
229
|
+
const { container } = renderList({
|
|
230
|
+
setting: languageSetting,
|
|
231
|
+
value: ["abc"],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let autocompletes = container.querySelectorAll<HTMLInputElement>(
|
|
235
|
+
'input[list="setting-autocomplete-list"]'
|
|
236
|
+
);
|
|
237
|
+
expect(autocompletes).toHaveLength(2);
|
|
238
|
+
|
|
239
|
+
await user.type(autocompletes[1], "Another language");
|
|
240
|
+
await user.click(addButton(container));
|
|
241
|
+
|
|
242
|
+
await waitFor(() => {
|
|
243
|
+
autocompletes = container.querySelectorAll<HTMLInputElement>(
|
|
244
|
+
'input[list="setting-autocomplete-list"]'
|
|
245
|
+
);
|
|
246
|
+
expect(autocompletes).toHaveLength(3);
|
|
247
|
+
});
|
|
248
|
+
expect(autocompletes[2]).toHaveValue("");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("does not add an empty input item", async () => {
|
|
252
|
+
const user = userEvent.setup();
|
|
253
|
+
const { container } = renderList();
|
|
254
|
+
expect(listItemWrappers(container)).toHaveLength(2);
|
|
255
|
+
// The add button is disabled while the input is blank.
|
|
256
|
+
expect(addButton(container)).toBeDisabled();
|
|
257
|
+
|
|
258
|
+
await user.type(addInput(container), "something new");
|
|
259
|
+
expect(addButton(container)).not.toBeDisabled();
|
|
260
|
+
|
|
261
|
+
await user.click(addButton(container));
|
|
262
|
+
await waitFor(() => expect(listItemWrappers(container)).toHaveLength(3));
|
|
263
|
+
// Once added, the input is cleared and the button is disabled again.
|
|
264
|
+
expect(addButton(container)).toBeDisabled();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("optionally accepts an onChange prop", async () => {
|
|
268
|
+
const user = userEvent.setup();
|
|
269
|
+
const onChange = jest.fn();
|
|
270
|
+
const { container } = renderList({ onChange });
|
|
271
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
272
|
+
|
|
273
|
+
await user.type(addInput(container), "test");
|
|
274
|
+
await user.click(addButton(container));
|
|
275
|
+
await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1));
|
|
276
|
+
expect(onChange.mock.calls[0][0]).toHaveProperty("listItems");
|
|
277
|
+
|
|
278
|
+
await user.click(removeButtons(container)[0]);
|
|
279
|
+
await waitFor(() => expect(onChange).toHaveBeenCalledTimes(2));
|
|
280
|
+
expect(onChange.mock.calls[1][0]).toHaveProperty("listItems");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("optionally accepts a readOnly prop", () => {
|
|
284
|
+
const { container } = renderList({ readOnly: true });
|
|
285
|
+
const inputs = itemInputs(container);
|
|
286
|
+
expect(inputs[0]).toHaveAttribute("readonly");
|
|
287
|
+
expect(inputs[1]).toHaveAttribute("readonly");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("gets the value", () => {
|
|
291
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
292
|
+
renderList({}, ref);
|
|
293
|
+
expect(ref.current!.getValue()).toStrictEqual(value);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("clears all the items", () => {
|
|
297
|
+
const ref = React.createRef<ProtocolFormField>();
|
|
298
|
+
const { container } = renderList({}, ref);
|
|
299
|
+
expect(listItemWrappers(container)).toHaveLength(2);
|
|
300
|
+
|
|
301
|
+
act(() => {
|
|
302
|
+
ref.current!.clear();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(listItemWrappers(container)).toHaveLength(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("dropdown menu", () => {
|
|
309
|
+
const menuOptions = [1, 2, 3].map((n) => (
|
|
310
|
+
<option value={`Option ${n}`} key={n} aria-selected={false}>
|
|
311
|
+
{`Option ${n}`}
|
|
312
|
+
</option>
|
|
313
|
+
));
|
|
314
|
+
const menuSetting = {
|
|
315
|
+
...setting,
|
|
316
|
+
type: "menu",
|
|
317
|
+
menuOptions,
|
|
318
|
+
description: null,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
it("renders a dropdown menu", () => {
|
|
322
|
+
const { container } = renderList({ setting: menuSetting });
|
|
323
|
+
const menu = container.querySelectorAll("select");
|
|
324
|
+
expect(menu).toHaveLength(1);
|
|
325
|
+
const options = menu[0].querySelectorAll("option");
|
|
326
|
+
expect(options).toHaveLength(3);
|
|
327
|
+
options.forEach((option, idx) => {
|
|
328
|
+
expect(option).toHaveTextContent(`Option ${idx + 1}`);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("adds an item from the menu", async () => {
|
|
333
|
+
const user = userEvent.setup();
|
|
334
|
+
const { container } = renderList({ setting: menuSetting });
|
|
335
|
+
expect(listItemWrappers(container)).toHaveLength(2);
|
|
336
|
+
// The select defaults to the first option.
|
|
337
|
+
expect(container.querySelector("select")).toHaveValue("Option 1");
|
|
338
|
+
|
|
339
|
+
await user.click(addButton(container));
|
|
340
|
+
|
|
341
|
+
await waitFor(() => expect(listItemWrappers(container)).toHaveLength(3));
|
|
342
|
+
const inputs = itemInputs(container);
|
|
343
|
+
expect(inputs[inputs.length - 1]).toHaveValue("Option 1");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("doesn't lose options when nothing is selected", () => {
|
|
347
|
+
const narrowSetting = {
|
|
348
|
+
...menuSetting,
|
|
349
|
+
default: null,
|
|
350
|
+
format: "narrow",
|
|
351
|
+
};
|
|
352
|
+
const { container } = renderList({
|
|
353
|
+
setting: narrowSetting,
|
|
354
|
+
value: null,
|
|
355
|
+
});
|
|
356
|
+
expect(container.querySelectorAll("option")).toHaveLength(3);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("optionally eliminates already-selected options from the menu", async () => {
|
|
360
|
+
const user = userEvent.setup();
|
|
361
|
+
const { container, rerender } = renderList({ setting: menuSetting });
|
|
362
|
+
let options = container.querySelectorAll("option");
|
|
363
|
+
expect(options).toHaveLength(3);
|
|
364
|
+
expect(
|
|
365
|
+
Array.from(options).some((o) => o.textContent === "Option 1")
|
|
366
|
+
).toBe(true);
|
|
367
|
+
|
|
368
|
+
// Adding an option while the format is not "narrow" leaves the menu intact.
|
|
369
|
+
await user.click(addButton(container));
|
|
370
|
+
await waitFor(() => expect(listItemWrappers(container)).toHaveLength(3));
|
|
371
|
+
options = container.querySelectorAll("option");
|
|
372
|
+
expect(options).toHaveLength(3);
|
|
373
|
+
|
|
374
|
+
// Switch to the "narrow" format, which prunes selected options.
|
|
375
|
+
const narrowSetting = { ...menuSetting, format: "narrow" };
|
|
376
|
+
rerender(
|
|
377
|
+
<ProtocolFormField
|
|
378
|
+
setting={narrowSetting}
|
|
379
|
+
value={value}
|
|
380
|
+
disabled={false}
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
await user.click(addButton(container));
|
|
385
|
+
await waitFor(() => {
|
|
386
|
+
options = container.querySelectorAll("option");
|
|
387
|
+
expect(options).toHaveLength(2);
|
|
388
|
+
});
|
|
389
|
+
expect(
|
|
390
|
+
Array.from(container.querySelectorAll("option")).some(
|
|
391
|
+
(o) => o.textContent === "Option 1"
|
|
392
|
+
)
|
|
393
|
+
).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("optionally renders a label", () => {
|
|
397
|
+
const settingWithLabel = {
|
|
398
|
+
...menuSetting,
|
|
399
|
+
menuTitle: "Custom Menu Title",
|
|
400
|
+
};
|
|
401
|
+
const { container } = renderList({ setting: settingWithLabel });
|
|
402
|
+
const label = container.querySelector("select").closest("label");
|
|
403
|
+
expect(label).toHaveTextContent("Custom Menu Title");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("optionally marks the menu as required", () => {
|
|
407
|
+
const { container, rerender } = renderList({ setting: menuSetting });
|
|
408
|
+
expect(container.querySelectorAll(".required-field")).toHaveLength(0);
|
|
409
|
+
|
|
410
|
+
const requiredSetting = {
|
|
411
|
+
...menuSetting,
|
|
412
|
+
menuTitle: "title",
|
|
413
|
+
required: true,
|
|
414
|
+
};
|
|
415
|
+
rerender(
|
|
416
|
+
<ProtocolFormField
|
|
417
|
+
setting={requiredSetting}
|
|
418
|
+
value={value}
|
|
419
|
+
disabled={false}
|
|
420
|
+
/>
|
|
421
|
+
);
|
|
422
|
+
const required = container.querySelectorAll(".required-field");
|
|
423
|
+
expect(required).toHaveLength(1);
|
|
424
|
+
expect(required[0]).toHaveTextContent("Required");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("optionally renders an alternate value if there are no list items", () => {
|
|
428
|
+
const { container, rerender } = renderList({
|
|
429
|
+
setting: menuSetting,
|
|
430
|
+
altValue: "No list items!",
|
|
431
|
+
});
|
|
432
|
+
// There are still list items, so the placeholder isn't rendered yet.
|
|
433
|
+
expect(container.querySelectorAll(".input-list > span")).toHaveLength(0);
|
|
434
|
+
|
|
435
|
+
rerender(
|
|
436
|
+
<ProtocolFormField
|
|
437
|
+
setting={menuSetting}
|
|
438
|
+
value={[]}
|
|
439
|
+
altValue="No list items!"
|
|
440
|
+
disabled={false}
|
|
441
|
+
/>
|
|
442
|
+
);
|
|
443
|
+
const placeholder = container.querySelectorAll(".input-list > span");
|
|
444
|
+
expect(placeholder).toHaveLength(1);
|
|
445
|
+
expect(placeholder[0]).toHaveTextContent("No list items!");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("optionally renders an alternate value if all the available list items have already been added", () => {
|
|
449
|
+
const narrowSetting = { ...menuSetting, format: "narrow" };
|
|
450
|
+
const { container } = renderList({
|
|
451
|
+
setting: narrowSetting,
|
|
452
|
+
value: ["Option 1", "Option 2", "Option 3"],
|
|
453
|
+
onEmpty: "You've run out of options!",
|
|
454
|
+
});
|
|
455
|
+
expect(container.querySelector("select")).not.toBeInTheDocument();
|
|
456
|
+
const message = container.querySelector(".add-list-item-container span");
|
|
457
|
+
expect(message).toHaveTextContent("You've run out of options!");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("renders option elements if necessary", () => {
|
|
461
|
+
// Without a menuOptions property, InputList builds the menu from `options`.
|
|
462
|
+
const options = [
|
|
463
|
+
{ key: "key_1", label: "label_1" },
|
|
464
|
+
{ key: "key_2", label: "label_2" },
|
|
465
|
+
];
|
|
466
|
+
const optionsSetting = { ...menuSetting, options, menuOptions: null };
|
|
467
|
+
const { container } = renderList({ setting: optionsSetting });
|
|
468
|
+
const select = container.querySelector("select");
|
|
469
|
+
expect(select).toBeInTheDocument();
|
|
470
|
+
const opts = select.querySelectorAll("option");
|
|
471
|
+
expect(opts[0]).toHaveValue("key_1");
|
|
472
|
+
expect(opts[0]).toHaveTextContent("label_1");
|
|
473
|
+
expect(opts[1]).toHaveValue("key_2");
|
|
474
|
+
expect(opts[1]).toHaveTextContent("label_2");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("renders the default values", () => {
|
|
478
|
+
const defaultSetting = {
|
|
479
|
+
...menuSetting,
|
|
480
|
+
default: ["Option 1", "Option 2"],
|
|
481
|
+
};
|
|
482
|
+
const { container } = renderList({
|
|
483
|
+
setting: defaultSetting,
|
|
484
|
+
value: null,
|
|
485
|
+
});
|
|
486
|
+
const inputs = itemInputs(container);
|
|
487
|
+
expect(inputs[0]).toHaveValue("Option 1");
|
|
488
|
+
expect(inputs[1]).toHaveValue("Option 2");
|
|
489
|
+
// The remaining option is still available in the menu.
|
|
490
|
+
expect(container.querySelector("select")).toBeInTheDocument();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
});
|
|
@@ -304,11 +304,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
304
304
|
const NO_EMAIL_CONTEXT_PROVIDER_PROPS = { email: undefined };
|
|
305
305
|
|
|
306
306
|
it("shows the modal when `show` is true", async () => {
|
|
307
|
-
const {
|
|
308
|
-
getAllByRole,
|
|
309
|
-
queryAllByRole,
|
|
310
|
-
rerender,
|
|
311
|
-
} = renderWithProviders(
|
|
307
|
+
const { getAllByRole, queryAllByRole, rerender } = renderWithProviders(
|
|
312
308
|
<InventoryReportRequestModal
|
|
313
309
|
show={false}
|
|
314
310
|
onHide={onHide}
|
|
@@ -385,19 +381,15 @@ describe("InventoryReportRequestModal", () => {
|
|
|
385
381
|
let show = true;
|
|
386
382
|
onHide.mockImplementation(() => (show = false));
|
|
387
383
|
|
|
388
|
-
const {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
library={LIBRARY}
|
|
398
|
-
/>,
|
|
399
|
-
{ queryClient }
|
|
400
|
-
);
|
|
384
|
+
const { getAllByRole, getByRole, queryAllByRole, rerender } =
|
|
385
|
+
renderWithProviders(
|
|
386
|
+
<InventoryReportRequestModal
|
|
387
|
+
show={show}
|
|
388
|
+
onHide={onHide}
|
|
389
|
+
library={LIBRARY}
|
|
390
|
+
/>,
|
|
391
|
+
{ queryClient }
|
|
392
|
+
);
|
|
401
393
|
getAllByRole("dialog");
|
|
402
394
|
expect(show).toBe(true);
|
|
403
395
|
expect(onHide).not.toHaveBeenCalled();
|
|
@@ -420,10 +412,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
420
412
|
});
|
|
421
413
|
|
|
422
414
|
it("updates confirmation message, if report info request arrives in time", async () => {
|
|
423
|
-
const {
|
|
424
|
-
getByRole,
|
|
425
|
-
rerender,
|
|
426
|
-
} = renderWithProviders(
|
|
415
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
427
416
|
<InventoryReportRequestModal
|
|
428
417
|
show={true}
|
|
429
418
|
onHide={onHide}
|
|
@@ -457,10 +446,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
457
446
|
)
|
|
458
447
|
);
|
|
459
448
|
|
|
460
|
-
const {
|
|
461
|
-
getByRole,
|
|
462
|
-
rerender,
|
|
463
|
-
} = renderWithProviders(
|
|
449
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
464
450
|
<InventoryReportRequestModal
|
|
465
451
|
show={true}
|
|
466
452
|
onHide={onHide}
|
|
@@ -505,10 +491,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
505
491
|
)
|
|
506
492
|
);
|
|
507
493
|
|
|
508
|
-
const {
|
|
509
|
-
getByRole,
|
|
510
|
-
rerender,
|
|
511
|
-
} = renderWithProviders(
|
|
494
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
512
495
|
<InventoryReportRequestModal
|
|
513
496
|
show={true}
|
|
514
497
|
onHide={onHide}
|
|
@@ -538,10 +521,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
538
521
|
it("requests report generation, if request confirmed", async () => {
|
|
539
522
|
const user = userEvent.setup();
|
|
540
523
|
|
|
541
|
-
const {
|
|
542
|
-
getByRole,
|
|
543
|
-
rerender,
|
|
544
|
-
} = renderWithProviders(
|
|
524
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
545
525
|
<InventoryReportRequestModal
|
|
546
526
|
show={true}
|
|
547
527
|
onHide={onHide}
|
|
@@ -574,10 +554,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
574
554
|
it("displays success message, when request is successful", async () => {
|
|
575
555
|
const user = userEvent.setup();
|
|
576
556
|
|
|
577
|
-
const {
|
|
578
|
-
getByRole,
|
|
579
|
-
rerender,
|
|
580
|
-
} = renderWithProviders(
|
|
557
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
581
558
|
<InventoryReportRequestModal
|
|
582
559
|
show={true}
|
|
583
560
|
onHide={onHide}
|
|
@@ -615,10 +592,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
615
592
|
);
|
|
616
593
|
const user = userEvent.setup();
|
|
617
594
|
|
|
618
|
-
const {
|
|
619
|
-
getByRole,
|
|
620
|
-
rerender,
|
|
621
|
-
} = renderWithProviders(
|
|
595
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
622
596
|
<InventoryReportRequestModal
|
|
623
597
|
show={true}
|
|
624
598
|
onHide={onHide}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { render, screen, act, fireEvent } from "@testing-library/react";
|
|
3
3
|
import JsonField, { JsonFieldHandle } from "../../../src/components/JsonField";
|
|
4
|
+
import { MONOSPACE_FONT_STACK } from "../../../src/fonts";
|
|
4
5
|
|
|
5
6
|
const setting = {
|
|
6
7
|
key: "my_json",
|
|
@@ -472,6 +473,25 @@ describe("JsonField", () => {
|
|
|
472
473
|
});
|
|
473
474
|
});
|
|
474
475
|
|
|
476
|
+
describe("use_monospace_font", () => {
|
|
477
|
+
(
|
|
478
|
+
[
|
|
479
|
+
{ id: "set", use_monospace_font: true, hasStyle: true },
|
|
480
|
+
{ id: "absent", hasStyle: false },
|
|
481
|
+
] as { id: string; use_monospace_font?: boolean; hasStyle: boolean }[]
|
|
482
|
+
).forEach(({ id, use_monospace_font, hasStyle }) => {
|
|
483
|
+
it(id, () => {
|
|
484
|
+
render(<JsonField setting={{ ...setting, use_monospace_font }} />);
|
|
485
|
+
const ta = screen.getByLabelText("JSON Setting");
|
|
486
|
+
if (hasStyle) {
|
|
487
|
+
expect(ta).toHaveStyle({ fontFamily: MONOSPACE_FONT_STACK });
|
|
488
|
+
} else {
|
|
489
|
+
expect(ta).not.toHaveStyle({ fontFamily: MONOSPACE_FONT_STACK });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
475
495
|
describe("clear()", () => {
|
|
476
496
|
it("resets text and clears parse error", () => {
|
|
477
497
|
const ref = React.createRef<JsonFieldHandle>();
|