@thepalaceproject/circulation-admin 1.43.0-post.9 → 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 +22 -10
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/jest.config.js +11 -44
- package/package.json +1 -5
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +111 -0
- package/tests/jest/components/Announcement.test.tsx +76 -0
- package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
- package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
- package/tests/jest/components/Autocomplete.test.tsx +67 -0
- package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
- package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/BookEditor.test.tsx +20 -21
- package/tests/jest/components/CatalogPage.test.tsx +158 -0
- package/tests/jest/components/CatalogServices.test.tsx +38 -1
- package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
- package/tests/jest/components/Classifications.test.tsx +166 -0
- package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
- package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
- package/tests/jest/components/Collections.test.tsx +335 -7
- package/tests/jest/components/ColorPicker.test.tsx +58 -0
- package/tests/jest/components/ComplaintForm.test.tsx +171 -0
- package/tests/jest/components/Complaints.test.tsx +259 -0
- package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/Contributors.test.tsx +157 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
- package/tests/jest/components/CustomLists.test.tsx +621 -4
- package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
- package/tests/jest/components/EditableConfigList.test.tsx +598 -7
- package/tests/jest/components/EditableInput.test.tsx +250 -1
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/GenreForm.test.tsx +152 -0
- package/tests/jest/components/Header.test.tsx +421 -0
- package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
- package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/Lane.test.tsx +132 -10
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +377 -9
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/Libraries.test.tsx +48 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/MetadataServices.test.tsx +37 -1
- package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/ProtocolFormField.test.tsx +443 -7
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +0 -7
- package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
- package/tests/jest/components/ToolTip.test.tsx +46 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
- package/tests/jest/features/book.test.ts +75 -80
- package/tests/jest/fixtures/classificationsData.ts +45 -0
- package/tests/jest/fixtures/genreData.ts +811 -0
- package/tests/jest/README.md +0 -6
- package/tests/jest/legacy-setup.ts +0 -13
- package/tests/jest/legacyJsdomEnvironment.js +0 -49
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
within,
|
|
6
|
+
waitFor,
|
|
7
|
+
fireEvent,
|
|
8
|
+
act,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import userEvent from "@testing-library/user-event";
|
|
11
|
+
|
|
12
|
+
import { Lanes } from "../../../src/components/Lanes";
|
|
13
|
+
import { LaneData, CustomListData } from "../../../src/interfaces";
|
|
14
|
+
|
|
15
|
+
// Render react-router's Link as a marker so the lane tree and editor links
|
|
16
|
+
// render without a Router in context (see Lane.test.tsx / CustomListsSidebar.test.tsx).
|
|
17
|
+
jest.mock("react-router", () => ({
|
|
18
|
+
...jest.requireActual("react-router"),
|
|
19
|
+
Link: (props) => (
|
|
20
|
+
<div data-testid="Link" data-to={props.to} className={props.className}>
|
|
21
|
+
{props.children}
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// NOTE ON DRAG-AND-DROP COVERAGE
|
|
27
|
+
// ------------------------------
|
|
28
|
+
// `Lanes` reorders lanes with `react-beautiful-dnd`, and a real drag/drop cannot
|
|
29
|
+
// be simulated in jsdom. The outcomes of a drag are still covered:
|
|
30
|
+
// - `orderChanged` (and so the "Change Lane Order" panel), saving the reordered
|
|
31
|
+
// tree via `changeLaneOrder`, and cancelling it via `resetOrder` are all
|
|
32
|
+
// exercised in the "Lanes - reorder flow" describe below, which calls
|
|
33
|
+
// `Lanes.drag(...)` through a ref — the same callback LanesSidebar invokes on
|
|
34
|
+
// a real drop.
|
|
35
|
+
// - The drop boundary (a lane may only be dropped within its own parent) lives
|
|
36
|
+
// in LanesSidebar and is asserted in LanesSidebar.test.tsx.
|
|
37
|
+
// Only the pointer-level gestures themselves ("drags a top-level lane", "drags a
|
|
38
|
+
// sublane") have no jsdom-reachable trigger; the tree structure and expand/collapse
|
|
39
|
+
// behavior they produce are asserted instead (see "renders the draggable lane tree"
|
|
40
|
+
// and "expands and collapses lanes").
|
|
41
|
+
//
|
|
42
|
+
// The connected default export is not rendered here: it mounts with real
|
|
43
|
+
// dispatch props (fetchLanes/fetchCustomLists fire a network request on mount,
|
|
44
|
+
// which would reject and crash the worker) and would override the explicit
|
|
45
|
+
// `lanes`/`customLists`/dispatch stubs these behavior tests assert against.
|
|
46
|
+
|
|
47
|
+
const makeLanesData = (): LaneData[] => {
|
|
48
|
+
const subsublane: LaneData = {
|
|
49
|
+
id: 3,
|
|
50
|
+
display_name: "sublane 3",
|
|
51
|
+
visible: false,
|
|
52
|
+
count: 2,
|
|
53
|
+
sublanes: [],
|
|
54
|
+
custom_list_ids: [2],
|
|
55
|
+
inherit_parent_restrictions: false,
|
|
56
|
+
};
|
|
57
|
+
const sublane: LaneData = {
|
|
58
|
+
id: 2,
|
|
59
|
+
display_name: "sublane 2",
|
|
60
|
+
visible: false,
|
|
61
|
+
count: 3,
|
|
62
|
+
sublanes: [subsublane],
|
|
63
|
+
custom_list_ids: [2],
|
|
64
|
+
inherit_parent_restrictions: false,
|
|
65
|
+
};
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
id: 1,
|
|
69
|
+
display_name: "lane 1",
|
|
70
|
+
visible: true,
|
|
71
|
+
count: 5,
|
|
72
|
+
sublanes: [sublane],
|
|
73
|
+
custom_list_ids: [1],
|
|
74
|
+
inherit_parent_restrictions: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 4,
|
|
78
|
+
display_name: "lane 4",
|
|
79
|
+
visible: true,
|
|
80
|
+
count: 1,
|
|
81
|
+
sublanes: [],
|
|
82
|
+
custom_list_ids: [],
|
|
83
|
+
inherit_parent_restrictions: false,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const customListsData: CustomListData[] = [
|
|
89
|
+
{ id: 1, name: "list 1", entry_count: 0, is_owner: true, is_shared: false },
|
|
90
|
+
{ id: 2, name: "list 2", entry_count: 2, is_owner: true, is_shared: false },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
describe("Lanes", () => {
|
|
94
|
+
let fetchLanes: jest.Mock;
|
|
95
|
+
let fetchCustomLists: jest.Mock;
|
|
96
|
+
let editLane: jest.Mock;
|
|
97
|
+
let deleteLane: jest.Mock;
|
|
98
|
+
let showLane: jest.Mock;
|
|
99
|
+
let hideLane: jest.Mock;
|
|
100
|
+
let resetLanes: jest.Mock;
|
|
101
|
+
let changeLaneOrder: jest.Mock;
|
|
102
|
+
let confirmSpy: jest.SpyInstance;
|
|
103
|
+
let lanesData: LaneData[];
|
|
104
|
+
|
|
105
|
+
const makeProps = (overrides: Record<string, unknown> = {}) => ({
|
|
106
|
+
csrfToken: "token",
|
|
107
|
+
library: "library",
|
|
108
|
+
lanes: lanesData,
|
|
109
|
+
customLists: customListsData,
|
|
110
|
+
isFetching: false,
|
|
111
|
+
fetchLanes,
|
|
112
|
+
fetchCustomLists,
|
|
113
|
+
editLane,
|
|
114
|
+
deleteLane,
|
|
115
|
+
showLane,
|
|
116
|
+
hideLane,
|
|
117
|
+
resetLanes,
|
|
118
|
+
changeLaneOrder,
|
|
119
|
+
...overrides,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
lanesData = makeLanesData();
|
|
124
|
+
fetchLanes = jest.fn();
|
|
125
|
+
fetchCustomLists = jest.fn();
|
|
126
|
+
editLane = jest.fn().mockResolvedValue(undefined);
|
|
127
|
+
deleteLane = jest.fn().mockResolvedValue(undefined);
|
|
128
|
+
showLane = jest.fn().mockResolvedValue(undefined);
|
|
129
|
+
hideLane = jest.fn().mockResolvedValue(undefined);
|
|
130
|
+
resetLanes = jest.fn().mockResolvedValue(undefined);
|
|
131
|
+
changeLaneOrder = jest.fn().mockResolvedValue(undefined);
|
|
132
|
+
confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
afterEach(() => {
|
|
136
|
+
jest.restoreAllMocks();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("renders an error message from a bad form submission", () => {
|
|
140
|
+
const { container, rerender } = render(<Lanes {...makeProps()} />);
|
|
141
|
+
expect(container.querySelector(".alert-danger")).toBeNull();
|
|
142
|
+
|
|
143
|
+
rerender(
|
|
144
|
+
<Lanes
|
|
145
|
+
{...makeProps({
|
|
146
|
+
formError: { status: 500, response: "Error", url: "url" },
|
|
147
|
+
})}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
expect(container.querySelector(".alert-danger")).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("renders an error message from a loading error", () => {
|
|
154
|
+
const { container, rerender } = render(<Lanes {...makeProps()} />);
|
|
155
|
+
expect(container.querySelector(".alert-danger")).toBeNull();
|
|
156
|
+
|
|
157
|
+
rerender(
|
|
158
|
+
<Lanes
|
|
159
|
+
{...makeProps({
|
|
160
|
+
fetchError: { status: 500, response: "Error", url: "url" },
|
|
161
|
+
})}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
expect(container.querySelector(".alert-danger")).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("renders a loading indicator while fetching", () => {
|
|
168
|
+
const { container, rerender } = render(<Lanes {...makeProps()} />);
|
|
169
|
+
expect(container.querySelector(".loading")).toBeNull();
|
|
170
|
+
|
|
171
|
+
rerender(<Lanes {...makeProps({ isFetching: true })} />);
|
|
172
|
+
expect(container.querySelector(".loading")).toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("renders the create editor by default", () => {
|
|
176
|
+
const { rerender } = render(<Lanes {...makeProps()} />);
|
|
177
|
+
|
|
178
|
+
expect(screen.getByText("New top-level lane")).toBeInTheDocument();
|
|
179
|
+
expect(
|
|
180
|
+
screen.getByRole("button", { name: "Save lane" })
|
|
181
|
+
).toBeInTheDocument();
|
|
182
|
+
|
|
183
|
+
// With an identifier, the create editor makes a sublane of that lane.
|
|
184
|
+
rerender(<Lanes {...makeProps({ identifier: "2" })} />);
|
|
185
|
+
expect(screen.getByText("New sublane of sublane 2")).toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("renders the edit editor for a lane", () => {
|
|
189
|
+
const { container } = render(
|
|
190
|
+
<Lanes {...makeProps({ editOrCreate: "edit", identifier: "2" })} />
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// The editor header identifies the lane being edited (id 2).
|
|
194
|
+
expect(container.querySelector(".save-or-edit")).toHaveTextContent("ID-2");
|
|
195
|
+
expect(
|
|
196
|
+
screen.getByRole("button", { name: "Save lane" })
|
|
197
|
+
).toBeInTheDocument();
|
|
198
|
+
expect(
|
|
199
|
+
screen.getByRole("button", { name: /Delete lane/ })
|
|
200
|
+
).toBeInTheDocument();
|
|
201
|
+
expect(
|
|
202
|
+
container.querySelector(".lane-custom-lists-drag-and-drop")
|
|
203
|
+
).toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("renders the reset confirmation form", () => {
|
|
207
|
+
render(<Lanes {...makeProps({ editOrCreate: "reset" })} />);
|
|
208
|
+
|
|
209
|
+
expect(
|
|
210
|
+
screen.getByRole("heading", { name: "Reset all lanes" })
|
|
211
|
+
).toBeInTheDocument();
|
|
212
|
+
expect(screen.getByText(/cannot be undone/)).toBeInTheDocument();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("enables the reset button only after typing RESET", async () => {
|
|
216
|
+
const user = userEvent.setup();
|
|
217
|
+
const { container } = render(
|
|
218
|
+
<Lanes {...makeProps({ editOrCreate: "reset" })} />
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const resetButton = container.querySelector<HTMLButtonElement>(
|
|
222
|
+
"button.reset-button"
|
|
223
|
+
);
|
|
224
|
+
const input = container.querySelector<HTMLInputElement>(".reset input");
|
|
225
|
+
|
|
226
|
+
expect(resetButton).toBeDisabled();
|
|
227
|
+
|
|
228
|
+
await user.type(input, "RESET");
|
|
229
|
+
expect(resetButton).not.toBeDisabled();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("resets the lanes only when RESET is confirmed", async () => {
|
|
233
|
+
const user = userEvent.setup();
|
|
234
|
+
const { container } = render(
|
|
235
|
+
<Lanes {...makeProps({ editOrCreate: "reset" })} />
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const resetButton = container.querySelector<HTMLButtonElement>(
|
|
239
|
+
"button.reset-button"
|
|
240
|
+
);
|
|
241
|
+
const input = container.querySelector<HTMLInputElement>(".reset input");
|
|
242
|
+
|
|
243
|
+
// The wrong confirmation text leaves the button disabled and does nothing.
|
|
244
|
+
await user.type(input, "NOPE");
|
|
245
|
+
expect(resetButton).toBeDisabled();
|
|
246
|
+
fireEvent.click(resetButton);
|
|
247
|
+
expect(resetLanes).not.toHaveBeenCalled();
|
|
248
|
+
|
|
249
|
+
// Typing RESET enables the button and resets on click.
|
|
250
|
+
await user.clear(input);
|
|
251
|
+
await user.type(input, "RESET");
|
|
252
|
+
expect(resetButton).not.toBeDisabled();
|
|
253
|
+
|
|
254
|
+
await user.click(resetButton);
|
|
255
|
+
expect(resetLanes).toHaveBeenCalledTimes(1);
|
|
256
|
+
// fetchLanes runs once on mount and again after the reset resolves.
|
|
257
|
+
await waitFor(() => expect(fetchLanes).toHaveBeenCalledTimes(2));
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("edits a lane through the editor", async () => {
|
|
261
|
+
const user = userEvent.setup();
|
|
262
|
+
render(<Lanes {...makeProps({ editOrCreate: "edit", identifier: "2" })} />);
|
|
263
|
+
|
|
264
|
+
await user.click(screen.getByRole("button", { name: "Save lane" }));
|
|
265
|
+
|
|
266
|
+
expect(editLane).toHaveBeenCalledTimes(1);
|
|
267
|
+
await waitFor(() => expect(fetchLanes).toHaveBeenCalledTimes(2));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("deletes a lane through the editor", async () => {
|
|
271
|
+
const user = userEvent.setup();
|
|
272
|
+
render(<Lanes {...makeProps({ editOrCreate: "edit", identifier: "2" })} />);
|
|
273
|
+
|
|
274
|
+
const deleteButton = screen.getByRole("button", { name: /Delete lane/ });
|
|
275
|
+
|
|
276
|
+
// Declining the confirmation does not delete.
|
|
277
|
+
confirmSpy.mockReturnValue(false);
|
|
278
|
+
await user.click(deleteButton);
|
|
279
|
+
expect(deleteLane).not.toHaveBeenCalled();
|
|
280
|
+
|
|
281
|
+
// Confirming deletes the lane and refetches.
|
|
282
|
+
confirmSpy.mockReturnValue(true);
|
|
283
|
+
await user.click(deleteButton);
|
|
284
|
+
expect(deleteLane).toHaveBeenCalledTimes(1);
|
|
285
|
+
expect(deleteLane).toHaveBeenCalledWith("2");
|
|
286
|
+
await waitFor(() => expect(fetchLanes).toHaveBeenCalledTimes(2));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("renders the draggable lane tree", () => {
|
|
290
|
+
const { container } = render(<Lanes {...makeProps()} />);
|
|
291
|
+
|
|
292
|
+
const sidebar = container.querySelector<HTMLElement>(".lanes-sidebar");
|
|
293
|
+
expect(sidebar).toBeInTheDocument();
|
|
294
|
+
|
|
295
|
+
// The two top-level lanes render inside a droppable list, each as a
|
|
296
|
+
// draggable row.
|
|
297
|
+
expect(sidebar.querySelector("ul.droppable")).toBeInTheDocument();
|
|
298
|
+
expect(sidebar.querySelectorAll(".lane-info.draggable")).toHaveLength(2);
|
|
299
|
+
expect(within(sidebar).getByText("lane 1")).toBeInTheDocument();
|
|
300
|
+
expect(within(sidebar).getByText("lane 4")).toBeInTheDocument();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("expands and collapses lanes to reveal sublanes", async () => {
|
|
304
|
+
const user = userEvent.setup();
|
|
305
|
+
const { container } = render(<Lanes {...makeProps()} />);
|
|
306
|
+
const sidebar = container.querySelector<HTMLElement>(".lanes-sidebar");
|
|
307
|
+
|
|
308
|
+
// Lane 1 starts expanded (so sublane 2 shows), but sublane 2 starts
|
|
309
|
+
// collapsed (so sublane 3 is hidden).
|
|
310
|
+
expect(within(sidebar).getByText("sublane 2")).toBeInTheDocument();
|
|
311
|
+
expect(within(sidebar).queryByText("sublane 3")).toBeNull();
|
|
312
|
+
|
|
313
|
+
const sublane2 = within(sidebar)
|
|
314
|
+
.getByText("sublane 2")
|
|
315
|
+
.closest(".lane-parent") as HTMLElement;
|
|
316
|
+
await user.click(sublane2.querySelector("button.expand-button"));
|
|
317
|
+
expect(within(sidebar).getByText("sublane 3")).toBeInTheDocument();
|
|
318
|
+
|
|
319
|
+
// Collapsing lane 1 hides all of its sublanes.
|
|
320
|
+
const lane1 = within(sidebar)
|
|
321
|
+
.getByText("lane 1")
|
|
322
|
+
.closest(".lane-parent") as HTMLElement;
|
|
323
|
+
await user.click(lane1.querySelector("button.collapse-button"));
|
|
324
|
+
expect(within(sidebar).queryByText("sublane 2")).toBeNull();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// The lane-reorder flow. `Lanes.drag` is the callback LanesSidebar invokes on a
|
|
329
|
+
// real drop (which jsdom can't perform), so we call it through a ref to drive the
|
|
330
|
+
// order-changed UI, then exercise save/reset and findParentOfLane.
|
|
331
|
+
describe("Lanes - reorder flow", () => {
|
|
332
|
+
const makeLanesData = (): LaneData[] => {
|
|
333
|
+
const subsublane: LaneData = {
|
|
334
|
+
id: 3,
|
|
335
|
+
display_name: "sublane 3",
|
|
336
|
+
visible: false,
|
|
337
|
+
count: 2,
|
|
338
|
+
sublanes: [],
|
|
339
|
+
custom_list_ids: [2],
|
|
340
|
+
inherit_parent_restrictions: false,
|
|
341
|
+
};
|
|
342
|
+
const sublane: LaneData = {
|
|
343
|
+
id: 2,
|
|
344
|
+
display_name: "sublane 2",
|
|
345
|
+
visible: false,
|
|
346
|
+
count: 3,
|
|
347
|
+
sublanes: [subsublane],
|
|
348
|
+
custom_list_ids: [2],
|
|
349
|
+
inherit_parent_restrictions: false,
|
|
350
|
+
};
|
|
351
|
+
return [
|
|
352
|
+
{
|
|
353
|
+
id: 1,
|
|
354
|
+
display_name: "lane 1",
|
|
355
|
+
visible: true,
|
|
356
|
+
count: 5,
|
|
357
|
+
sublanes: [sublane],
|
|
358
|
+
custom_list_ids: [1],
|
|
359
|
+
inherit_parent_restrictions: true,
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const props = (overrides: Record<string, unknown> = {}) => ({
|
|
365
|
+
csrfToken: "token",
|
|
366
|
+
library: "library",
|
|
367
|
+
lanes: makeLanesData(),
|
|
368
|
+
customLists: [],
|
|
369
|
+
isFetching: false,
|
|
370
|
+
fetchLanes: jest.fn(),
|
|
371
|
+
fetchCustomLists: jest.fn(),
|
|
372
|
+
editLane: jest.fn(),
|
|
373
|
+
deleteLane: jest.fn(),
|
|
374
|
+
showLane: jest.fn(),
|
|
375
|
+
hideLane: jest.fn(),
|
|
376
|
+
resetLanes: jest.fn(),
|
|
377
|
+
changeLaneOrder: jest.fn().mockResolvedValue(undefined),
|
|
378
|
+
...overrides,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("shows the reorder UI after a drag, then saves the reordered lanes and resets", async () => {
|
|
382
|
+
const user = userEvent.setup();
|
|
383
|
+
const changeLaneOrder = jest.fn().mockResolvedValue(undefined);
|
|
384
|
+
const fetchLanes = jest.fn();
|
|
385
|
+
const ref = React.createRef<any>();
|
|
386
|
+
// Two top-level lanes, so a reorder is observable in the saved payload.
|
|
387
|
+
const secondLane: LaneData = {
|
|
388
|
+
id: 4,
|
|
389
|
+
display_name: "lane 4",
|
|
390
|
+
visible: true,
|
|
391
|
+
count: 1,
|
|
392
|
+
sublanes: [],
|
|
393
|
+
custom_list_ids: [3],
|
|
394
|
+
inherit_parent_restrictions: true,
|
|
395
|
+
};
|
|
396
|
+
const initialLanes = [...makeLanesData(), secondLane];
|
|
397
|
+
const { container } = render(
|
|
398
|
+
<Lanes
|
|
399
|
+
ref={ref}
|
|
400
|
+
{...props({ changeLaneOrder, fetchLanes, lanes: initialLanes })}
|
|
401
|
+
/>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// A drop hands the reordered tree to Lanes.drag (what LanesSidebar does),
|
|
405
|
+
// which stores it and flags orderChanged.
|
|
406
|
+
const originalOrder = ref.current.state.lanes;
|
|
407
|
+
const reordered = [...originalOrder].reverse();
|
|
408
|
+
act(() => ref.current.drag({ lanes: reordered, orderChanged: true }));
|
|
409
|
+
expect(container.querySelector(".order-change-info")).toBeInTheDocument();
|
|
410
|
+
|
|
411
|
+
fetchLanes.mockClear(); // ignore the fetch fired on mount
|
|
412
|
+
await user.click(
|
|
413
|
+
container.querySelector("button.save-lane-order-changes") as HTMLElement
|
|
414
|
+
);
|
|
415
|
+
// The exact reordered tree is persisted — not the original order.
|
|
416
|
+
expect(changeLaneOrder).toHaveBeenCalledTimes(1);
|
|
417
|
+
expect(changeLaneOrder).toHaveBeenCalledWith(reordered);
|
|
418
|
+
expect(changeLaneOrder.mock.calls[0][0]).not.toEqual(originalOrder);
|
|
419
|
+
// saveOrder() only fetches once changeLaneOrder resolves.
|
|
420
|
+
await waitFor(() => expect(fetchLanes).toHaveBeenCalledTimes(1));
|
|
421
|
+
|
|
422
|
+
// With a reorder pending in state, cancelling runs resetOrder(), which
|
|
423
|
+
// restores state.lanes from props and clears the panel.
|
|
424
|
+
act(() => ref.current.drag({ lanes: reordered, orderChanged: true }));
|
|
425
|
+
expect(ref.current.state.lanes).toEqual(reordered);
|
|
426
|
+
|
|
427
|
+
await user.click(
|
|
428
|
+
container.querySelector("button.cancel-lane-order-changes") as HTMLElement
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(ref.current.state.lanes).toEqual(initialLanes);
|
|
432
|
+
expect(
|
|
433
|
+
container.querySelector(".order-change-info")
|
|
434
|
+
).not.toBeInTheDocument();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("finds the parent of a deeply nested lane", () => {
|
|
438
|
+
const ref = React.createRef<any>();
|
|
439
|
+
render(<Lanes ref={ref} {...props()} />);
|
|
440
|
+
|
|
441
|
+
// The subsublane (id 3) is a child of sublane 2, not a top-level lane, so the
|
|
442
|
+
// lookup recurses one level down.
|
|
443
|
+
const subsublane = { id: 3 } as LaneData;
|
|
444
|
+
expect(ref.current.findParentOfLane(subsublane).id).toBe(2);
|
|
445
|
+
});
|
|
446
|
+
});
|