@thepalaceproject/circulation-admin 1.43.0 → 1.44.0-post.4
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,212 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, within } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import AdvancedSearchFilterInput from "../../../src/components/AdvancedSearchFilterInput";
|
|
5
|
+
import {
|
|
6
|
+
fields,
|
|
7
|
+
operators,
|
|
8
|
+
} from "../../../src/components/AdvancedSearchBuilder";
|
|
9
|
+
|
|
10
|
+
describe("AdvancedSearchFilterInput", () => {
|
|
11
|
+
let onAdd;
|
|
12
|
+
let onClearFiltersFlagChange;
|
|
13
|
+
|
|
14
|
+
const renderInput = () =>
|
|
15
|
+
render(
|
|
16
|
+
<AdvancedSearchFilterInput
|
|
17
|
+
name="include"
|
|
18
|
+
onAdd={onAdd}
|
|
19
|
+
onClearFiltersFlagChange={onClearFiltersFlagChange}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const fieldKeySelect = () =>
|
|
24
|
+
screen.getByRole("combobox", { name: "filter field key" });
|
|
25
|
+
const operatorSelect = () =>
|
|
26
|
+
screen.getByRole("combobox", { name: "filter operator" });
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
onAdd = jest.fn();
|
|
30
|
+
onClearFiltersFlagChange = jest.fn();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should render select with field names for field selection", () => {
|
|
34
|
+
renderInput();
|
|
35
|
+
|
|
36
|
+
const options = within(fieldKeySelect()).getAllByRole("option");
|
|
37
|
+
|
|
38
|
+
expect(options).toHaveLength(fields.length);
|
|
39
|
+
|
|
40
|
+
fields.forEach((field, index) => {
|
|
41
|
+
expect(options[index]).toHaveValue(field.value);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should render a select with options for operator selection", () => {
|
|
46
|
+
renderInput();
|
|
47
|
+
|
|
48
|
+
const options = within(operatorSelect()).getAllByRole("option");
|
|
49
|
+
|
|
50
|
+
expect(options).toHaveLength(operators.length);
|
|
51
|
+
|
|
52
|
+
operators.forEach((op, index) => {
|
|
53
|
+
expect(options[index]).toHaveValue(op.value);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should restrict the operator selection, if the filter requests it", async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
renderInput();
|
|
60
|
+
|
|
61
|
+
// Select the fiction field key.
|
|
62
|
+
await user.selectOptions(fieldKeySelect(), "fiction");
|
|
63
|
+
|
|
64
|
+
// Look up the fiction field operator values.
|
|
65
|
+
const fictionField = fields.find((element) => element.value === "fiction");
|
|
66
|
+
const options = within(operatorSelect()).getAllByRole("option");
|
|
67
|
+
|
|
68
|
+
// And make sure the number of options is correct.
|
|
69
|
+
expect(options).toHaveLength(fictionField.operators.length);
|
|
70
|
+
options.forEach((option, index) => {
|
|
71
|
+
expect(option).toHaveValue(fictionField.operators[index]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should use the currently selected operator when changing filters", async () => {
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
renderInput();
|
|
78
|
+
|
|
79
|
+
const fictionField = fields.find((element) => element.value === "fiction");
|
|
80
|
+
|
|
81
|
+
await user.selectOptions(operatorSelect(), "regex");
|
|
82
|
+
|
|
83
|
+
// Select data source/Distributor as the filter key.
|
|
84
|
+
await user.selectOptions(fieldKeySelect(), "data_source");
|
|
85
|
+
expect(operatorSelect()).toHaveValue("regex");
|
|
86
|
+
|
|
87
|
+
// But the "regex" operator is not supported by the "fiction" field,
|
|
88
|
+
// so we switch to fiction's first available operator.
|
|
89
|
+
await user.selectOptions(fieldKeySelect(), "fiction");
|
|
90
|
+
expect(operatorSelect()).toHaveValue(fictionField.operators[0]);
|
|
91
|
+
|
|
92
|
+
// Since data source/distributor supports fiction's first operator,
|
|
93
|
+
// we'll continue to use it when changing back to that field.
|
|
94
|
+
await user.selectOptions(fieldKeySelect(), "data_source");
|
|
95
|
+
expect(operatorSelect()).toHaveValue(fictionField.operators[0]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should render a text input for value entry", () => {
|
|
99
|
+
renderInput();
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
screen.getByRole("textbox", { name: "filter value" })
|
|
103
|
+
).toBeInTheDocument();
|
|
104
|
+
expect(
|
|
105
|
+
screen.queryByRole("combobox", { name: "filter value" })
|
|
106
|
+
).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("surfaces the field's placeholder and help text for the publication date field", async () => {
|
|
110
|
+
const user = userEvent.setup();
|
|
111
|
+
renderInput();
|
|
112
|
+
|
|
113
|
+
// "publication date" is the only field that defines a placeholder and
|
|
114
|
+
// helpText; selecting it surfaces both on the value input (as its
|
|
115
|
+
// placeholder and accessible description). This is the sole coverage for
|
|
116
|
+
// those two props of AdvancedSearchFilterInput.
|
|
117
|
+
await user.selectOptions(fieldKeySelect(), "published");
|
|
118
|
+
|
|
119
|
+
const valueInput = screen.getByRole("textbox", { name: "filter value" });
|
|
120
|
+
expect(valueInput).toHaveAttribute("placeholder", "YYYY-MM-DD");
|
|
121
|
+
expect(valueInput).toHaveAccessibleDescription(/publication date/i);
|
|
122
|
+
expect(valueInput).toHaveAccessibleDescription(/YYYY-MM-DD/i);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should render a select for value selection, if the filter requests it", async () => {
|
|
126
|
+
const user = userEvent.setup();
|
|
127
|
+
renderInput();
|
|
128
|
+
|
|
129
|
+
await user.selectOptions(fieldKeySelect(), "fiction");
|
|
130
|
+
|
|
131
|
+
expect(
|
|
132
|
+
screen.getByRole("combobox", { name: "filter value" })
|
|
133
|
+
).toBeInTheDocument();
|
|
134
|
+
expect(
|
|
135
|
+
screen.queryByRole("textbox", { name: "filter value" })
|
|
136
|
+
).not.toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should clear the current value when changing the filter", async () => {
|
|
140
|
+
const user = userEvent.setup();
|
|
141
|
+
renderInput();
|
|
142
|
+
|
|
143
|
+
const valueInput = screen.getByRole("textbox", { name: "filter value" });
|
|
144
|
+
|
|
145
|
+
await user.type(valueInput, "ABC");
|
|
146
|
+
|
|
147
|
+
await user.selectOptions(fieldKeySelect(), "data_source");
|
|
148
|
+
|
|
149
|
+
expect(screen.getByRole("textbox", { name: "filter value" })).toHaveValue(
|
|
150
|
+
""
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should render an Add Filter button", () => {
|
|
155
|
+
renderInput();
|
|
156
|
+
|
|
157
|
+
const buttons = screen.getAllByRole("button");
|
|
158
|
+
|
|
159
|
+
expect(buttons).toHaveLength(1);
|
|
160
|
+
expect(buttons[0]).toHaveTextContent("Add filter");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should disable the Add Filter button when the value is blank", async () => {
|
|
164
|
+
const user = userEvent.setup();
|
|
165
|
+
renderInput();
|
|
166
|
+
|
|
167
|
+
const valueInput = screen.getByRole("textbox", { name: "filter value" });
|
|
168
|
+
|
|
169
|
+
expect(screen.getByRole("button", { name: "Add filter" })).toBeDisabled();
|
|
170
|
+
|
|
171
|
+
await user.type(valueInput, " ");
|
|
172
|
+
expect(screen.getByRole("button", { name: "Add filter" })).toBeDisabled();
|
|
173
|
+
|
|
174
|
+
await user.clear(valueInput);
|
|
175
|
+
await user.type(valueInput, "hello");
|
|
176
|
+
expect(screen.getByRole("button", { name: "Add filter" })).toBeEnabled();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should call onAdd when the Add Filter button is clicked", async () => {
|
|
180
|
+
const user = userEvent.setup();
|
|
181
|
+
renderInput();
|
|
182
|
+
|
|
183
|
+
const valueInput = screen.getByRole("textbox", { name: "filter value" });
|
|
184
|
+
|
|
185
|
+
await user.type(valueInput, "hello");
|
|
186
|
+
await user.click(screen.getByRole("button", { name: "Add filter" }));
|
|
187
|
+
|
|
188
|
+
expect(onAdd).toHaveBeenCalledTimes(1);
|
|
189
|
+
expect(onAdd).toHaveBeenCalledWith({
|
|
190
|
+
key: "genre",
|
|
191
|
+
op: "eq",
|
|
192
|
+
value: "hello",
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should trim whitespace from the value", async () => {
|
|
197
|
+
const user = userEvent.setup();
|
|
198
|
+
renderInput();
|
|
199
|
+
|
|
200
|
+
const valueInput = screen.getByRole("textbox", { name: "filter value" });
|
|
201
|
+
|
|
202
|
+
await user.type(valueInput, " hello ");
|
|
203
|
+
await user.click(screen.getByRole("button", { name: "Add filter" }));
|
|
204
|
+
|
|
205
|
+
expect(onAdd).toHaveBeenCalledTimes(1);
|
|
206
|
+
expect(onAdd).toHaveBeenCalledWith({
|
|
207
|
+
key: "genre",
|
|
208
|
+
op: "eq",
|
|
209
|
+
value: "hello",
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { DndProvider } from "react-dnd";
|
|
4
|
+
import { HTML5Backend } from "react-dnd-html5-backend";
|
|
5
|
+
import AdvancedSearchFilterViewer from "../../../src/components/AdvancedSearchFilterViewer";
|
|
6
|
+
|
|
7
|
+
describe("AdvancedSearchFilterViewer", () => {
|
|
8
|
+
let onBooleanChange;
|
|
9
|
+
let onMove;
|
|
10
|
+
let onSelect;
|
|
11
|
+
let onRemove;
|
|
12
|
+
let query;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
onBooleanChange = jest.fn();
|
|
16
|
+
onMove = jest.fn();
|
|
17
|
+
onSelect = jest.fn();
|
|
18
|
+
onRemove = jest.fn();
|
|
19
|
+
|
|
20
|
+
query = {
|
|
21
|
+
id: "0",
|
|
22
|
+
key: "genre",
|
|
23
|
+
value: "Horror",
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should render an AdvancedSearchFilter", () => {
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<DndProvider backend={HTML5Backend}>
|
|
30
|
+
<AdvancedSearchFilterViewer
|
|
31
|
+
query={query}
|
|
32
|
+
readOnly={false}
|
|
33
|
+
selectedQueryId="0"
|
|
34
|
+
onBooleanChange={onBooleanChange}
|
|
35
|
+
onMove={onMove}
|
|
36
|
+
onSelect={onSelect}
|
|
37
|
+
onRemove={onRemove}
|
|
38
|
+
/>
|
|
39
|
+
</DndProvider>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// The viewer wraps its filter in a tree container and passes the query through,
|
|
43
|
+
// so the value filter renders inside it.
|
|
44
|
+
expect(screen.getByRole("tree")).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText("Genre Horror")).toBeInTheDocument();
|
|
46
|
+
|
|
47
|
+
// readOnly is false and selectedQueryId matches the query id, so it is selected.
|
|
48
|
+
expect(
|
|
49
|
+
container.querySelector(".advanced-search-value-filter")
|
|
50
|
+
).toHaveClass("selected");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { DndProvider } from "react-dnd";
|
|
5
|
+
import { HTML5Backend } from "react-dnd-html5-backend";
|
|
6
|
+
import AdvancedSearchValueFilter from "../../../src/components/AdvancedSearchValueFilter";
|
|
7
|
+
import {
|
|
8
|
+
fields,
|
|
9
|
+
operators,
|
|
10
|
+
} from "../../../src/components/AdvancedSearchBuilder";
|
|
11
|
+
|
|
12
|
+
describe("AdvancedSearchValueFilter", () => {
|
|
13
|
+
let onMove;
|
|
14
|
+
let onRemove;
|
|
15
|
+
let onSelect;
|
|
16
|
+
let query;
|
|
17
|
+
|
|
18
|
+
const renderFilter = (props = {}) =>
|
|
19
|
+
render(
|
|
20
|
+
<DndProvider backend={HTML5Backend}>
|
|
21
|
+
<AdvancedSearchValueFilter
|
|
22
|
+
query={query}
|
|
23
|
+
onMove={onMove}
|
|
24
|
+
onSelect={onSelect}
|
|
25
|
+
onRemove={onRemove}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
</DndProvider>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
onMove = jest.fn();
|
|
33
|
+
onRemove = jest.fn();
|
|
34
|
+
onSelect = jest.fn();
|
|
35
|
+
|
|
36
|
+
query = {
|
|
37
|
+
id: "91",
|
|
38
|
+
key: "title",
|
|
39
|
+
op: "eq",
|
|
40
|
+
value: "foo",
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should render the field label, operator symbol, and value of the query", () => {
|
|
45
|
+
renderFilter();
|
|
46
|
+
|
|
47
|
+
const expectedField = `${
|
|
48
|
+
fields.find((field) => field.value === "title")?.label
|
|
49
|
+
}`;
|
|
50
|
+
const expectedOperator = `${
|
|
51
|
+
operators.find((op) => op.value === "eq")?.symbol
|
|
52
|
+
}`;
|
|
53
|
+
const filterLabelText = `${expectedField} ${expectedOperator} foo`;
|
|
54
|
+
|
|
55
|
+
expect(screen.getByText(filterLabelText)).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should apply the selected class when selected is true", () => {
|
|
59
|
+
const { container } = renderFilter({ selected: true });
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
container.querySelector(".advanced-search-value-filter")
|
|
63
|
+
).toHaveClass("selected");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should not apply the selected class when readOnly is true", () => {
|
|
67
|
+
const { container } = renderFilter({ readOnly: true, selected: true });
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
container.querySelector(".advanced-search-value-filter")
|
|
71
|
+
).not.toHaveClass("selected");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should render a remove button", () => {
|
|
75
|
+
renderFilter();
|
|
76
|
+
|
|
77
|
+
const button = screen.getByRole("button");
|
|
78
|
+
|
|
79
|
+
expect(button).toHaveTextContent("×");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should not render a remove button when readOnly is true", () => {
|
|
83
|
+
renderFilter({ readOnly: true });
|
|
84
|
+
|
|
85
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should call onRemove when the remove button is clicked", async () => {
|
|
89
|
+
const user = userEvent.setup();
|
|
90
|
+
renderFilter();
|
|
91
|
+
|
|
92
|
+
await user.click(screen.getByRole("button"));
|
|
93
|
+
|
|
94
|
+
expect(onRemove).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(onRemove).toHaveBeenCalledWith("91");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should call onSelect when clicked", async () => {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
const { container } = renderFilter();
|
|
101
|
+
|
|
102
|
+
await user.click(container.querySelector(".advanced-search-value-filter"));
|
|
103
|
+
|
|
104
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(onSelect).toHaveBeenCalledWith("91");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should call onSelect when the space bar is depressed", async () => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
const { container } = renderFilter();
|
|
111
|
+
|
|
112
|
+
// The filter is a focusable treeitem; focus it and press space, as a
|
|
113
|
+
// keyboard user would.
|
|
114
|
+
const item = container.querySelector(
|
|
115
|
+
".advanced-search-value-filter"
|
|
116
|
+
) as HTMLElement;
|
|
117
|
+
item.focus();
|
|
118
|
+
await user.keyboard(" ");
|
|
119
|
+
|
|
120
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(onSelect).toHaveBeenCalledWith("91");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import Announcement from "../../../src/components/Announcement";
|
|
6
|
+
|
|
7
|
+
describe("Announcement", () => {
|
|
8
|
+
let stubDelete: jest.Mock;
|
|
9
|
+
let stubEdit: jest.Mock;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
stubDelete = jest.fn();
|
|
13
|
+
stubEdit = jest.fn();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const renderAnnouncement = (
|
|
17
|
+
overrides: Partial<React.ComponentProps<typeof Announcement>> = {}
|
|
18
|
+
) =>
|
|
19
|
+
render(
|
|
20
|
+
<Announcement
|
|
21
|
+
content="test"
|
|
22
|
+
start="2020-05-12"
|
|
23
|
+
finish="2020-05-28"
|
|
24
|
+
id="1"
|
|
25
|
+
delete={stubDelete}
|
|
26
|
+
edit={stubEdit}
|
|
27
|
+
{...overrides}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
it("renders the content and dates", () => {
|
|
32
|
+
const { container } = renderAnnouncement();
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
container.querySelector("section.announcement-info")
|
|
36
|
+
).toBeInTheDocument();
|
|
37
|
+
expect(container.querySelector(".dates")).toHaveTextContent(
|
|
38
|
+
"05/12/2020 – 05/28/2020"
|
|
39
|
+
);
|
|
40
|
+
expect(screen.getByText("test")).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("renders an edit button", async () => {
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
renderAnnouncement();
|
|
46
|
+
|
|
47
|
+
const editButton = screen.getByRole("button", { name: "Edit" });
|
|
48
|
+
await user.click(editButton);
|
|
49
|
+
|
|
50
|
+
expect(stubEdit).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(stubEdit).toHaveBeenCalledWith("1");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders a delete button", async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
renderAnnouncement();
|
|
57
|
+
|
|
58
|
+
const deleteButton = screen.getByRole("button", { name: "Delete" });
|
|
59
|
+
await user.click(deleteButton);
|
|
60
|
+
|
|
61
|
+
expect(stubDelete).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(stubDelete).toHaveBeenCalledWith("1");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("formats dates", () => {
|
|
66
|
+
// The dates are stored as year-month-day but displayed as month/day/year.
|
|
67
|
+
const { container } = renderAnnouncement({
|
|
68
|
+
start: "2020-03-14",
|
|
69
|
+
finish: "2021-12-25",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(container.querySelector(".dates")).toHaveTextContent(
|
|
73
|
+
"03/14/2020 – 12/25/2021"
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import AnnouncementForm from "../../../src/components/AnnouncementForm";
|
|
6
|
+
|
|
7
|
+
// 66 characters — within the 15-350 valid range.
|
|
8
|
+
const CONTENT =
|
|
9
|
+
"Here is some sample content which comes out to over 15 characters.";
|
|
10
|
+
|
|
11
|
+
const getTextarea = () => screen.getByRole("textbox") as HTMLTextAreaElement;
|
|
12
|
+
const getDateInputs = (container: HTMLElement) =>
|
|
13
|
+
Array.from(
|
|
14
|
+
container.querySelectorAll<HTMLInputElement>('input[type="date"]')
|
|
15
|
+
);
|
|
16
|
+
const getCounter = () => screen.getByText(/Current length:/);
|
|
17
|
+
|
|
18
|
+
// Enter valid content and a start/finish date.
|
|
19
|
+
const fillOutForm = async (
|
|
20
|
+
user: ReturnType<typeof userEvent.setup>,
|
|
21
|
+
container: HTMLElement
|
|
22
|
+
) => {
|
|
23
|
+
await user.type(getTextarea(), CONTENT);
|
|
24
|
+
const [start, finish] = getDateInputs(container);
|
|
25
|
+
// Date inputs are read via a ref, so `fireEvent.change` (set value + dispatch)
|
|
26
|
+
// reliably sets the value.
|
|
27
|
+
fireEvent.change(start, { target: { value: "2020-06-01" } });
|
|
28
|
+
fireEvent.change(finish, { target: { value: "2020-07-01" } });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("AnnouncementForm", () => {
|
|
32
|
+
let add: jest.Mock;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
add = jest.fn();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders the input fields", () => {
|
|
38
|
+
const { container } = render(<AnnouncementForm add={add} />);
|
|
39
|
+
|
|
40
|
+
// Content field: a textarea carrying the 15/350 length constraints.
|
|
41
|
+
const textarea = getTextarea();
|
|
42
|
+
expect(textarea.tagName).toBe("TEXTAREA");
|
|
43
|
+
expect(textarea).toHaveAttribute("minlength", "15");
|
|
44
|
+
expect(textarea).toHaveAttribute("maxlength", "350");
|
|
45
|
+
expect(
|
|
46
|
+
screen.getByText("New Announcement Text (15-350 characters)")
|
|
47
|
+
).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText("(Current length: 0/350)")).toBeInTheDocument();
|
|
49
|
+
|
|
50
|
+
// Two date fields, each with its label and description.
|
|
51
|
+
const dates = getDateInputs(container);
|
|
52
|
+
expect(dates).toHaveLength(2);
|
|
53
|
+
// EditableInput prepends "(Optional) " to each description, so match the
|
|
54
|
+
// full sentence as a substring rather than the whole text content.
|
|
55
|
+
expect(screen.getByText("Start Date")).toBeInTheDocument();
|
|
56
|
+
expect(
|
|
57
|
+
screen.getByText(
|
|
58
|
+
/If no start date is chosen, the default start date is today's date\./
|
|
59
|
+
)
|
|
60
|
+
).toBeInTheDocument();
|
|
61
|
+
expect(screen.getByText("End Date")).toBeInTheDocument();
|
|
62
|
+
expect(
|
|
63
|
+
screen.getByText(
|
|
64
|
+
/If no expiration date is chosen, the default expiration date is 2 months from the start date\./
|
|
65
|
+
)
|
|
66
|
+
).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// The other tests only compare the date fields against whatever they rendered
|
|
70
|
+
// with, so they would all still pass if the defaults came out blank. Pin the
|
|
71
|
+
// clock and assert the actual values.
|
|
72
|
+
it("prefills the date fields with today and two months from today", () => {
|
|
73
|
+
jest.useFakeTimers().setSystemTime(new Date(2026, 2, 15));
|
|
74
|
+
try {
|
|
75
|
+
const { container } = render(<AnnouncementForm add={add} />);
|
|
76
|
+
|
|
77
|
+
const [start, finish] = getDateInputs(container);
|
|
78
|
+
expect(start.value).toBe("2026-03-15");
|
|
79
|
+
expect(finish.value).toBe("2026-05-15");
|
|
80
|
+
} finally {
|
|
81
|
+
jest.useRealTimers();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("renders the buttons", () => {
|
|
86
|
+
render(<AnnouncementForm add={add} />);
|
|
87
|
+
expect(screen.getAllByRole("button")).toHaveLength(2);
|
|
88
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("keeps track of whether the content is too short or too long", async () => {
|
|
93
|
+
const user = userEvent.setup();
|
|
94
|
+
const { container } = render(<AnnouncementForm add={add} />);
|
|
95
|
+
|
|
96
|
+
// Empty content: 0/350, flagged as the wrong length, Add disabled.
|
|
97
|
+
expect(getCounter()).toHaveTextContent("(Current length: 0/350)");
|
|
98
|
+
expect(getCounter().parentElement).toHaveClass("wrong-length");
|
|
99
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
|
|
100
|
+
|
|
101
|
+
// Valid content: length flag cleared and Add enabled.
|
|
102
|
+
await fillOutForm(user, container);
|
|
103
|
+
expect(getCounter()).toHaveTextContent("(Current length: 66/350)");
|
|
104
|
+
expect(getCounter().parentElement).not.toHaveClass("wrong-length");
|
|
105
|
+
expect(screen.getByRole("button", { name: "Add" })).not.toBeDisabled();
|
|
106
|
+
|
|
107
|
+
// Content at the 350-character cap is flagged as too long again.
|
|
108
|
+
const longString =
|
|
109
|
+
"Here's some extremely/gratuitously long content. The point of the content is just to get up to 350 characters so that I can test whether the class name will change. I realize even as I am typing this that it probably would have been a better idea to do it with filler text; oh well...? Anyway, I have now written the most boring announcement EVER.";
|
|
110
|
+
fireEvent.change(getTextarea(), { target: { value: longString } });
|
|
111
|
+
expect(getCounter()).toHaveTextContent("(Current length: 350/350)");
|
|
112
|
+
expect(getCounter().parentElement).toHaveClass("wrong-length");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("adds a new announcement", async () => {
|
|
116
|
+
const user = userEvent.setup();
|
|
117
|
+
const { container } = render(<AnnouncementForm add={add} />);
|
|
118
|
+
await fillOutForm(user, container);
|
|
119
|
+
|
|
120
|
+
await user.click(screen.getByRole("button", { name: "Add" }));
|
|
121
|
+
|
|
122
|
+
expect(add).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(add.mock.calls[0][0].content).toBe(CONTENT);
|
|
124
|
+
expect(add.mock.calls[0][0].start).toBe("2020-06-01");
|
|
125
|
+
expect(add.mock.calls[0][0].finish).toBe("2020-07-01");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("cancels adding a new announcement", async () => {
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
const { container } = render(<AnnouncementForm add={add} />);
|
|
131
|
+
const [start0, finish0] = getDateInputs(container);
|
|
132
|
+
const defaultStart = start0.value;
|
|
133
|
+
const defaultFinish = finish0.value;
|
|
134
|
+
|
|
135
|
+
await fillOutForm(user, container);
|
|
136
|
+
expect(getTextarea()).toHaveValue(CONTENT);
|
|
137
|
+
|
|
138
|
+
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
|
139
|
+
|
|
140
|
+
// Cancelling a brand-new announcement adds nothing and restores the
|
|
141
|
+
// empty content field and the default dates.
|
|
142
|
+
expect(add).not.toHaveBeenCalled();
|
|
143
|
+
expect(getTextarea()).toHaveValue("");
|
|
144
|
+
expect(getCounter()).toHaveTextContent("(Current length: 0/350)");
|
|
145
|
+
const [start1, finish1] = getDateInputs(container);
|
|
146
|
+
expect(start1.value).toBe(defaultStart);
|
|
147
|
+
expect(finish1.value).toBe(defaultFinish);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("edits an existing announcement", async () => {
|
|
151
|
+
const user = userEvent.setup();
|
|
152
|
+
const { container, rerender } = render(<AnnouncementForm add={add} />);
|
|
153
|
+
const [start0, finish0] = getDateInputs(container);
|
|
154
|
+
const defaultStart = start0.value;
|
|
155
|
+
const defaultFinish = finish0.value;
|
|
156
|
+
|
|
157
|
+
// Supplying an existing announcement's props switches the form to edit mode.
|
|
158
|
+
rerender(
|
|
159
|
+
<AnnouncementForm
|
|
160
|
+
add={add}
|
|
161
|
+
content={CONTENT}
|
|
162
|
+
start="07/01/2020"
|
|
163
|
+
finish="08/01/2020"
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// The form is populated with the existing announcement (dates reformatted).
|
|
168
|
+
expect(getTextarea()).toHaveValue(CONTENT);
|
|
169
|
+
const [start1, finish1] = getDateInputs(container);
|
|
170
|
+
expect(start1.value).toBe("2020-07-01");
|
|
171
|
+
expect(finish1.value).toBe("2020-08-01");
|
|
172
|
+
|
|
173
|
+
// Edit the content and save it.
|
|
174
|
+
fireEvent.change(getTextarea(), {
|
|
175
|
+
target: { value: "Here is an edited version of the content" },
|
|
176
|
+
});
|
|
177
|
+
await user.click(screen.getByRole("button", { name: "Add" }));
|
|
178
|
+
|
|
179
|
+
expect(add).toHaveBeenCalledTimes(1);
|
|
180
|
+
expect(add.mock.calls[0][0].content).toBe(
|
|
181
|
+
"Here is an edited version of the content"
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// The form resets to an empty content field and the default dates.
|
|
185
|
+
expect(getTextarea()).toHaveValue("");
|
|
186
|
+
const [start2, finish2] = getDateInputs(container);
|
|
187
|
+
expect(start2.value).toBe(defaultStart);
|
|
188
|
+
expect(finish2.value).toBe(defaultFinish);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("cancels editing an existing announcement", async () => {
|
|
192
|
+
const user = userEvent.setup();
|
|
193
|
+
const { container, rerender } = render(<AnnouncementForm add={add} />);
|
|
194
|
+
const [start0, finish0] = getDateInputs(container);
|
|
195
|
+
const defaultStart = start0.value;
|
|
196
|
+
const defaultFinish = finish0.value;
|
|
197
|
+
|
|
198
|
+
rerender(
|
|
199
|
+
<AnnouncementForm
|
|
200
|
+
add={add}
|
|
201
|
+
content={CONTENT}
|
|
202
|
+
start="07/01/2020"
|
|
203
|
+
finish="08/01/2020"
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(getTextarea()).toHaveValue(CONTENT);
|
|
208
|
+
const [start1, finish1] = getDateInputs(container);
|
|
209
|
+
expect(start1.value).toBe("2020-07-01");
|
|
210
|
+
expect(finish1.value).toBe("2020-08-01");
|
|
211
|
+
|
|
212
|
+
fireEvent.change(getTextarea(), {
|
|
213
|
+
target: { value: "Here is an edited version of the content" },
|
|
214
|
+
});
|
|
215
|
+
// Cancelling while editing re-submits the (edited) announcement rather than
|
|
216
|
+
// discarding it — unlike cancelling a brand-new one.
|
|
217
|
+
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
|
218
|
+
|
|
219
|
+
expect(add).toHaveBeenCalledTimes(1);
|
|
220
|
+
expect(add.mock.calls[0][0].content).toBe(
|
|
221
|
+
"Here is an edited version of the content"
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(getTextarea()).toHaveValue("");
|
|
225
|
+
const [start2, finish2] = getDateInputs(container);
|
|
226
|
+
expect(start2.value).toBe(defaultStart);
|
|
227
|
+
expect(finish2.value).toBe(defaultFinish);
|
|
228
|
+
});
|
|
229
|
+
});
|