@thepalaceproject/circulation-admin 1.43.0-post.10 → 1.43.0-post.14
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/package.json +1 -1
- package/tests/jest/components/Announcement.test.tsx +76 -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/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/CatalogPage.test.tsx +158 -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/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/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 +315 -0
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/GenreForm.test.tsx +152 -0
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/Lane.test.tsx +130 -7
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +367 -2
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, within, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import PairedMenus from "../../../src/components/PairedMenus";
|
|
5
|
+
import { SettingData, LibraryData } from "../../../src/interfaces";
|
|
6
|
+
|
|
7
|
+
describe("PairedMenus", () => {
|
|
8
|
+
const inputListSetting: SettingData = {
|
|
9
|
+
key: "input",
|
|
10
|
+
label: "Input List",
|
|
11
|
+
default: ["a", "b", "c"],
|
|
12
|
+
options: [
|
|
13
|
+
{ key: "a", label: "A" },
|
|
14
|
+
{ key: "b", label: "B" },
|
|
15
|
+
{ key: "c", label: "C" },
|
|
16
|
+
{ key: "d", label: "D" },
|
|
17
|
+
{ key: "e", label: "E" },
|
|
18
|
+
],
|
|
19
|
+
paired: "dropdown",
|
|
20
|
+
};
|
|
21
|
+
const dropdownSetting: SettingData = {
|
|
22
|
+
key: "dropdown",
|
|
23
|
+
label: "Dropdown",
|
|
24
|
+
default: "b",
|
|
25
|
+
options: [
|
|
26
|
+
{ key: "a", label: "A" },
|
|
27
|
+
{ key: "b", label: "B" },
|
|
28
|
+
{ key: "c", label: "C" },
|
|
29
|
+
{ key: "d", label: "D" },
|
|
30
|
+
{ key: "e", label: "E" },
|
|
31
|
+
],
|
|
32
|
+
type: "select",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const renderPairedMenus = (props = {}) =>
|
|
36
|
+
render(
|
|
37
|
+
<PairedMenus
|
|
38
|
+
inputListSetting={inputListSetting}
|
|
39
|
+
dropdownSetting={dropdownSetting}
|
|
40
|
+
disabled={false}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// The current InputList items are rendered as read-only text inputs inside the
|
|
46
|
+
// `.input-list-ul` list; return their displayed values.
|
|
47
|
+
const listItemValues = (container: HTMLElement): string[] =>
|
|
48
|
+
within(container.querySelector<HTMLElement>(".input-list-ul")!)
|
|
49
|
+
.getAllByRole("textbox")
|
|
50
|
+
.map((input) => (input as HTMLInputElement).value);
|
|
51
|
+
|
|
52
|
+
const dropdown = (container: HTMLElement): HTMLSelectElement =>
|
|
53
|
+
container.querySelector<HTMLSelectElement>('select[name="dropdown"]')!;
|
|
54
|
+
|
|
55
|
+
it("renders a fieldset with an InputList and a dropdown", () => {
|
|
56
|
+
const { container } = renderPairedMenus();
|
|
57
|
+
expect(
|
|
58
|
+
container.querySelector("fieldset.paired-menus")
|
|
59
|
+
).toBeInTheDocument();
|
|
60
|
+
expect(container.querySelector(".input-list")).toBeInTheDocument();
|
|
61
|
+
expect(dropdown(container)).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders the InputList items as read-only fields", () => {
|
|
65
|
+
const { container } = renderPairedMenus();
|
|
66
|
+
const listItems = within(
|
|
67
|
+
container.querySelector<HTMLElement>(".input-list-ul")!
|
|
68
|
+
).getAllByRole("textbox");
|
|
69
|
+
expect(listItems).toHaveLength(3);
|
|
70
|
+
listItems.forEach((input) =>
|
|
71
|
+
expect((input as HTMLInputElement).readOnly).toBe(true)
|
|
72
|
+
);
|
|
73
|
+
expect(listItemValues(container)).toEqual(["A", "B", "C"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("renders the dropdown with its default value and available options", () => {
|
|
77
|
+
const { container } = renderPairedMenus();
|
|
78
|
+
const select = dropdown(container);
|
|
79
|
+
expect(select).toHaveValue("b");
|
|
80
|
+
const options = within(select).getAllByRole("option");
|
|
81
|
+
expect(options.map((o) => o.textContent)).toEqual(["A", "B", "C"]);
|
|
82
|
+
expect(options.map((o) => (o as HTMLOptionElement).value)).toEqual([
|
|
83
|
+
"a",
|
|
84
|
+
"b",
|
|
85
|
+
"c",
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("optionally disables the InputList's button", () => {
|
|
90
|
+
const { rerender } = renderPairedMenus();
|
|
91
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeEnabled();
|
|
92
|
+
|
|
93
|
+
rerender(
|
|
94
|
+
<PairedMenus
|
|
95
|
+
inputListSetting={inputListSetting}
|
|
96
|
+
dropdownSetting={dropdownSetting}
|
|
97
|
+
disabled={false}
|
|
98
|
+
readOnly={true}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("reflects the default InputList values and dropdown value", () => {
|
|
105
|
+
const { container } = renderPairedMenus();
|
|
106
|
+
expect(listItemValues(container)).toEqual(["A", "B", "C"]);
|
|
107
|
+
expect(dropdown(container)).toHaveValue("b");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("takes an optional item prop", () => {
|
|
111
|
+
const item: LibraryData = {
|
|
112
|
+
name: "Library",
|
|
113
|
+
short_name: "lib",
|
|
114
|
+
uuid: "123",
|
|
115
|
+
settings: {
|
|
116
|
+
input: ["c", "d", "e"],
|
|
117
|
+
dropdown: "d",
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const { container } = renderPairedMenus({ item });
|
|
121
|
+
|
|
122
|
+
const listItems = within(
|
|
123
|
+
container.querySelector<HTMLElement>(".input-list-ul")!
|
|
124
|
+
).getAllByRole("textbox");
|
|
125
|
+
expect(listItems.map((i) => (i as HTMLInputElement).value)).toEqual([
|
|
126
|
+
"C",
|
|
127
|
+
"D",
|
|
128
|
+
"E",
|
|
129
|
+
]);
|
|
130
|
+
expect(listItems.map((i) => (i as HTMLInputElement).name)).toEqual([
|
|
131
|
+
"input_c",
|
|
132
|
+
"input_d",
|
|
133
|
+
"input_e",
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const select = dropdown(container);
|
|
137
|
+
expect(select).toHaveValue("d");
|
|
138
|
+
expect(
|
|
139
|
+
within(select)
|
|
140
|
+
.getAllByRole("option")
|
|
141
|
+
.map((o) => o.textContent)
|
|
142
|
+
).toEqual(["C", "D", "E"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("updates the InputList when an item is removed", async () => {
|
|
146
|
+
const user = userEvent.setup();
|
|
147
|
+
const { container } = renderPairedMenus();
|
|
148
|
+
expect(listItemValues(container)).toEqual(["A", "B", "C"]);
|
|
149
|
+
|
|
150
|
+
// Remove the first list item ("A").
|
|
151
|
+
await user.click(screen.getAllByRole("button", { name: /Delete/ })[0]);
|
|
152
|
+
|
|
153
|
+
await waitFor(() =>
|
|
154
|
+
expect(
|
|
155
|
+
within(
|
|
156
|
+
container.querySelector<HTMLElement>(".input-list-ul")!
|
|
157
|
+
).getAllByRole("textbox")
|
|
158
|
+
).toHaveLength(2)
|
|
159
|
+
);
|
|
160
|
+
expect(listItemValues(container)).toEqual(["B", "C"]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("recalculates the dropdown options based on the InputList values", async () => {
|
|
164
|
+
const user = userEvent.setup();
|
|
165
|
+
const { container } = renderPairedMenus();
|
|
166
|
+
expect(
|
|
167
|
+
within(dropdown(container))
|
|
168
|
+
.getAllByRole("option")
|
|
169
|
+
.map((o) => o.textContent)
|
|
170
|
+
).toEqual(["A", "B", "C"]);
|
|
171
|
+
|
|
172
|
+
// Removing "A" leaves the InputList holding [b, c], so the dropdown may only
|
|
173
|
+
// offer B and C.
|
|
174
|
+
await user.click(screen.getAllByRole("button", { name: /Delete/ })[0]);
|
|
175
|
+
|
|
176
|
+
await waitFor(() =>
|
|
177
|
+
expect(
|
|
178
|
+
within(dropdown(container))
|
|
179
|
+
.getAllByRole("option")
|
|
180
|
+
.map((o) => o.textContent)
|
|
181
|
+
).toEqual(["B", "C"])
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("updates the dropdown selection", async () => {
|
|
186
|
+
const user = userEvent.setup();
|
|
187
|
+
const { container } = renderPairedMenus();
|
|
188
|
+
const select = dropdown(container);
|
|
189
|
+
expect(select).toHaveValue("b");
|
|
190
|
+
|
|
191
|
+
await user.selectOptions(select, "c");
|
|
192
|
+
expect(select).toHaveValue("c");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("displays a message when the dropdown has no available values", async () => {
|
|
196
|
+
const user = userEvent.setup();
|
|
197
|
+
const { container } = renderPairedMenus();
|
|
198
|
+
|
|
199
|
+
// Remove every item from the InputList.
|
|
200
|
+
for (let remaining = 3; remaining > 0; remaining--) {
|
|
201
|
+
const removeButtons = screen.getAllByRole("button", { name: /Delete/ });
|
|
202
|
+
expect(removeButtons).toHaveLength(remaining);
|
|
203
|
+
await user.click(removeButtons[0]);
|
|
204
|
+
await waitFor(() =>
|
|
205
|
+
expect(
|
|
206
|
+
screen.queryAllByRole("button", { name: /Delete/ })
|
|
207
|
+
).toHaveLength(remaining - 1)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const message =
|
|
212
|
+
"In order to set this value, you must add at least one option from the menu above.";
|
|
213
|
+
expect(screen.getByText(message)).toBeInTheDocument();
|
|
214
|
+
expect(container.querySelector(".bg-warning")).toHaveTextContent(message);
|
|
215
|
+
|
|
216
|
+
// The dropdown select is replaced by the warning; only the add menu remains.
|
|
217
|
+
expect(dropdown(container)).not.toBeInTheDocument();
|
|
218
|
+
expect(container.querySelectorAll("select")).toHaveLength(1);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen, waitFor } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
import buildStore from "../../../src/store";
|
|
6
|
+
// Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
|
|
7
|
+
// are exercised: the panel fetches its data on mount, the fetch populates the
|
|
8
|
+
// Redux store, and mapStateToProps feeds it back in as props.
|
|
9
|
+
import PatronAuthServices from "../../../src/components/PatronAuthServices";
|
|
10
|
+
|
|
11
|
+
const listData = {
|
|
12
|
+
patron_auth_services: [
|
|
13
|
+
{
|
|
14
|
+
id: 2,
|
|
15
|
+
protocol: "test protocol",
|
|
16
|
+
settings: { test_setting: "test setting" },
|
|
17
|
+
libraries: [
|
|
18
|
+
{ short_name: "nypl", test_library_setting: "test library setting" },
|
|
19
|
+
],
|
|
20
|
+
name: "nypl protocol",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
protocols: [
|
|
24
|
+
{ name: "test protocol", label: "test protocol label", settings: [] },
|
|
25
|
+
],
|
|
26
|
+
allLibraries: [{ short_name: "nypl" }],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stub `fetch` so the panel's on-mount fetch resolves with `body`. Returning a
|
|
31
|
+
* fresh Response per call avoids "body already used" when several fetches run.
|
|
32
|
+
*/
|
|
33
|
+
const stubFetch = (body: unknown) =>
|
|
34
|
+
jest.spyOn(globalThis, "fetch").mockImplementation(
|
|
35
|
+
async () =>
|
|
36
|
+
new Response(JSON.stringify(body), {
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// A fresh store per test keeps the fetched data isolated between tests.
|
|
43
|
+
const renderConnected = (element: React.ReactElement) =>
|
|
44
|
+
renderWithProviders(element, {
|
|
45
|
+
reduxProviderProps: { store: buildStore() },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("PatronAuthServices", () => {
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
jest.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("shows the patron auth service list", async () => {
|
|
54
|
+
stubFetch(listData);
|
|
55
|
+
const { container } = renderConnected(
|
|
56
|
+
<PatronAuthServices csrfToken="token" />
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// The list appears once the connected component's on-mount fetch resolves.
|
|
60
|
+
// Its heading is the protocol-aware label built by PatronAuthServices.label().
|
|
61
|
+
expect(
|
|
62
|
+
await screen.findByRole("heading", {
|
|
63
|
+
level: 3,
|
|
64
|
+
name: /nypl protocol: test protocol label/,
|
|
65
|
+
})
|
|
66
|
+
).toBeInTheDocument();
|
|
67
|
+
|
|
68
|
+
const editLink = container.querySelector("a.edit-item");
|
|
69
|
+
expect(editLink).not.toBeNull();
|
|
70
|
+
expect(editLink.getAttribute("href")).toBe(
|
|
71
|
+
"/admin/web/config/patronAuth/edit/2"
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// No neighborhood analytics panel is shown in list mode.
|
|
75
|
+
expect(
|
|
76
|
+
screen.queryByText("Patron Neighborhood Analytics: Disabled")
|
|
77
|
+
).not.toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("shows the neighborhood analytics panel when creating a service whose protocol has a neighborhood_mode setting", async () => {
|
|
81
|
+
const neighborhoodSetting = {
|
|
82
|
+
key: "neighborhood_mode",
|
|
83
|
+
label: "Neighborhood Mode",
|
|
84
|
+
description: "",
|
|
85
|
+
options: [],
|
|
86
|
+
};
|
|
87
|
+
const createData = {
|
|
88
|
+
patron_auth_services: [],
|
|
89
|
+
protocols: [
|
|
90
|
+
{
|
|
91
|
+
name: "test protocol",
|
|
92
|
+
label: "test protocol label",
|
|
93
|
+
settings: [neighborhoodSetting],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
allLibraries: [{ short_name: "nypl" }],
|
|
97
|
+
};
|
|
98
|
+
stubFetch(createData);
|
|
99
|
+
|
|
100
|
+
const { container } = renderConnected(
|
|
101
|
+
<PatronAuthServices csrfToken="token" editOrCreate="create" />
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Once the protocol data loads, the create form's extra section (the
|
|
105
|
+
// NeighborhoodAnalyticsForm) renders because the selected protocol carries a
|
|
106
|
+
// "neighborhood_mode" setting.
|
|
107
|
+
await waitFor(() =>
|
|
108
|
+
expect(container.querySelector("#neighborhood-panel")).toBeInTheDocument()
|
|
109
|
+
);
|
|
110
|
+
expect(
|
|
111
|
+
screen.getByText("Patron Neighborhood Analytics: Disabled")
|
|
112
|
+
).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import buildStore from "../../../src/store";
|
|
6
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
7
|
+
import ConnectedResetAdobeId, {
|
|
8
|
+
ResetAdobeId,
|
|
9
|
+
} from "../../../src/components/ResetAdobeId";
|
|
10
|
+
|
|
11
|
+
const patrons = [
|
|
12
|
+
{
|
|
13
|
+
authorization_expires: "",
|
|
14
|
+
username: "User Name",
|
|
15
|
+
personal_name: "Personal Name",
|
|
16
|
+
email_address: "user@email.com",
|
|
17
|
+
authorization_identifier: "1234",
|
|
18
|
+
authorization_identifiers: [""],
|
|
19
|
+
block_reason: "",
|
|
20
|
+
external_type: "",
|
|
21
|
+
fines: "",
|
|
22
|
+
permanent_id: "",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
authorization_expires: "",
|
|
26
|
+
personal_name: "Personal Name2",
|
|
27
|
+
authorization_identifier: "5678",
|
|
28
|
+
authorization_identifiers: [""],
|
|
29
|
+
block_reason: "",
|
|
30
|
+
external_type: "",
|
|
31
|
+
fines: "",
|
|
32
|
+
permanent_id: "",
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe("ResetAdobeId", () => {
|
|
37
|
+
let store: ReturnType<typeof buildStore>;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
store = buildStore();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
jest.restoreAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Render the unconnected component with explicit props (patron / resetAdobeId /
|
|
48
|
+
// responseBody / fetchError are what `connect` would otherwise source from the
|
|
49
|
+
// store, so tests inject them directly). Wrapped in providers because the
|
|
50
|
+
// ManagePatronsForm it renders is connected.
|
|
51
|
+
const resetElement = (props: Record<string, unknown> = {}) => (
|
|
52
|
+
<ResetAdobeId store={store} csrfToken="token" library="nypl" {...props} />
|
|
53
|
+
);
|
|
54
|
+
const renderReset = (props: Record<string, unknown> = {}) =>
|
|
55
|
+
renderWithProviders(resetElement(props));
|
|
56
|
+
|
|
57
|
+
describe("rendering without patron", () => {
|
|
58
|
+
it("has the patron-actions class", () => {
|
|
59
|
+
const { container } = renderReset({ patron: null });
|
|
60
|
+
expect(container.firstChild).toHaveClass("patron-actions");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("has a header", () => {
|
|
64
|
+
renderReset({ patron: null });
|
|
65
|
+
expect(
|
|
66
|
+
screen.getByRole("heading", { name: "Reset Adobe ID" })
|
|
67
|
+
).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("does not render a patron-info section", () => {
|
|
71
|
+
const { container } = renderReset({ patron: null });
|
|
72
|
+
expect(container.querySelector(".patron-info")).not.toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("prompts the user to search for a patron", () => {
|
|
76
|
+
renderReset({ patron: null });
|
|
77
|
+
expect(
|
|
78
|
+
screen.getByText("Search for a patron to begin.")
|
|
79
|
+
).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("rendering with patron", () => {
|
|
84
|
+
it("shows a warning before the submit button", () => {
|
|
85
|
+
const { container } = renderReset({ patron: patrons[0] });
|
|
86
|
+
expect(container.querySelector(".patron-warning")).toHaveTextContent(
|
|
87
|
+
"IMPORTANT: Patron User Name will lose access to any existing loans. Loans will still appear in the patron's book list until they expire, but the patron will be unable to read or return them."
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("gives the submit button a danger class", () => {
|
|
92
|
+
renderReset({ patron: patrons[0] });
|
|
93
|
+
expect(
|
|
94
|
+
screen.getByRole("button", { name: "Reset Adobe ID" })
|
|
95
|
+
).toHaveClass("danger");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("behavior", () => {
|
|
100
|
+
it("has a disabled submit button initially", () => {
|
|
101
|
+
renderReset({ patron: patrons[0], resetAdobeId: jest.fn() });
|
|
102
|
+
expect(
|
|
103
|
+
screen.getByRole("button", { name: "Reset Adobe ID" })
|
|
104
|
+
).toBeDisabled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("enables the submit button when the checkbox is checked", async () => {
|
|
108
|
+
const user = userEvent.setup();
|
|
109
|
+
renderReset({ patron: patrons[0], resetAdobeId: jest.fn() });
|
|
110
|
+
const button = screen.getByRole("button", { name: "Reset Adobe ID" });
|
|
111
|
+
expect(button).toBeDisabled();
|
|
112
|
+
|
|
113
|
+
await user.click(screen.getByRole("checkbox"));
|
|
114
|
+
|
|
115
|
+
expect(button).not.toBeDisabled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("disables the submit button when the checkbox is unchecked again", async () => {
|
|
119
|
+
const user = userEvent.setup();
|
|
120
|
+
renderReset({ patron: patrons[0], resetAdobeId: jest.fn() });
|
|
121
|
+
const button = screen.getByRole("button", { name: "Reset Adobe ID" });
|
|
122
|
+
const checkbox = screen.getByRole("checkbox");
|
|
123
|
+
|
|
124
|
+
await user.click(checkbox);
|
|
125
|
+
expect(button).not.toBeDisabled();
|
|
126
|
+
|
|
127
|
+
await user.click(checkbox);
|
|
128
|
+
expect(button).toBeDisabled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("calls the Adobe ID action when the button is clicked", async () => {
|
|
132
|
+
const user = userEvent.setup();
|
|
133
|
+
const resetAdobeId = jest.fn().mockResolvedValue(undefined);
|
|
134
|
+
renderReset({ patron: patrons[0], resetAdobeId });
|
|
135
|
+
expect(resetAdobeId).not.toHaveBeenCalled();
|
|
136
|
+
|
|
137
|
+
await user.click(screen.getByRole("checkbox"));
|
|
138
|
+
const button = screen.getByRole("button", { name: "Reset Adobe ID" });
|
|
139
|
+
expect(button).not.toBeDisabled();
|
|
140
|
+
await user.click(button);
|
|
141
|
+
|
|
142
|
+
await waitFor(() => expect(resetAdobeId).toHaveBeenCalledTimes(1));
|
|
143
|
+
const [data, library] = resetAdobeId.mock.calls[0];
|
|
144
|
+
expect(data.get("identifier")).toBe("1234");
|
|
145
|
+
expect(library).toBe("nypl");
|
|
146
|
+
|
|
147
|
+
// After the reset resolves the component clears the checkbox, re-disabling
|
|
148
|
+
// the button; awaiting that settles the pending promise.
|
|
149
|
+
await waitFor(() => expect(button).toBeDisabled());
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("shows a success alert message if the reset is successful", () => {
|
|
153
|
+
const { container, rerender } = renderReset({
|
|
154
|
+
patron: patrons[0],
|
|
155
|
+
resetAdobeId: jest.fn().mockResolvedValue(undefined),
|
|
156
|
+
});
|
|
157
|
+
expect(container.querySelector(".alert-success")).not.toBeInTheDocument();
|
|
158
|
+
|
|
159
|
+
// The response now comes from the server (via the responseBody prop).
|
|
160
|
+
rerender(
|
|
161
|
+
resetElement({
|
|
162
|
+
patron: patrons[0],
|
|
163
|
+
responseBody: "Adobe ID for patron has been reset.",
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const alert = container.querySelector(".alert-success");
|
|
168
|
+
expect(alert).toBeInTheDocument();
|
|
169
|
+
expect(alert).toHaveTextContent("Adobe ID for patron has been reset.");
|
|
170
|
+
expect(alert).toHaveTextContent(
|
|
171
|
+
"Please instruct the patron to sign back into their account."
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("hides the checkbox and the reset button if the reset is successful", () => {
|
|
176
|
+
const { container, rerender } = renderReset({
|
|
177
|
+
patron: patrons[0],
|
|
178
|
+
resetAdobeId: jest.fn().mockResolvedValue(undefined),
|
|
179
|
+
});
|
|
180
|
+
expect(
|
|
181
|
+
container.querySelector(".reset-adobe-id input")
|
|
182
|
+
).toBeInTheDocument();
|
|
183
|
+
expect(
|
|
184
|
+
container.querySelector(".reset-adobe-id button")
|
|
185
|
+
).toBeInTheDocument();
|
|
186
|
+
|
|
187
|
+
rerender(
|
|
188
|
+
resetElement({
|
|
189
|
+
patron: patrons[0],
|
|
190
|
+
responseBody: "Adobe ID for patron has been reset.",
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(
|
|
195
|
+
container.querySelector(".reset-adobe-id input")
|
|
196
|
+
).not.toBeInTheDocument();
|
|
197
|
+
expect(
|
|
198
|
+
container.querySelector(".reset-adobe-id button")
|
|
199
|
+
).not.toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("shows a failure alert message if the reset fails", () => {
|
|
203
|
+
const fetchError = { status: 400, response: "", url: "" };
|
|
204
|
+
// The legacy test modeled the failing action with a rejecting stub; the
|
|
205
|
+
// failure alert itself is driven by the fetchError prop the reducer sets.
|
|
206
|
+
const { container, rerender } = renderReset({
|
|
207
|
+
patron: patrons[0],
|
|
208
|
+
resetAdobeId: jest.fn().mockRejectedValue(fetchError),
|
|
209
|
+
});
|
|
210
|
+
expect(container.querySelector(".alert-danger")).not.toBeInTheDocument();
|
|
211
|
+
|
|
212
|
+
rerender(
|
|
213
|
+
resetElement({
|
|
214
|
+
patron: patrons[0],
|
|
215
|
+
resetAdobeId: jest.fn().mockRejectedValue(fetchError),
|
|
216
|
+
fetchError,
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const alert = container.querySelector(".alert-danger");
|
|
221
|
+
expect(alert).toBeInTheDocument();
|
|
222
|
+
expect(alert).toHaveTextContent(
|
|
223
|
+
"Error: failed to reset Adobe ID for patron 1234"
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("updates the warning message if a new patron is searched and found", () => {
|
|
228
|
+
const { container, rerender } = renderReset({
|
|
229
|
+
patron: patrons[0],
|
|
230
|
+
resetAdobeId: jest.fn(),
|
|
231
|
+
});
|
|
232
|
+
expect(container.querySelector(".patron-warning")).toHaveTextContent(
|
|
233
|
+
"IMPORTANT: Patron User Name will lose access"
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
rerender(resetElement({ patron: patrons[1], resetAdobeId: jest.fn() }));
|
|
237
|
+
|
|
238
|
+
expect(container.querySelector(".patron-warning")).toHaveTextContent(
|
|
239
|
+
"IMPORTANT: Patron Personal Name2 will lose access"
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("restores the checkbox and the reset button if a new patron is searched and found", () => {
|
|
244
|
+
const { container, rerender } = renderReset({
|
|
245
|
+
patron: patrons[0],
|
|
246
|
+
responseBody: "Adobe ID for patron has been reset.",
|
|
247
|
+
resetAdobeId: jest.fn(),
|
|
248
|
+
});
|
|
249
|
+
expect(
|
|
250
|
+
container.querySelector(".reset-adobe-id input")
|
|
251
|
+
).not.toBeInTheDocument();
|
|
252
|
+
expect(
|
|
253
|
+
container.querySelector(".reset-adobe-id button")
|
|
254
|
+
).not.toBeInTheDocument();
|
|
255
|
+
|
|
256
|
+
rerender(
|
|
257
|
+
resetElement({
|
|
258
|
+
patron: patrons[0],
|
|
259
|
+
responseBody: "",
|
|
260
|
+
resetAdobeId: jest.fn(),
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(
|
|
265
|
+
container.querySelector(".reset-adobe-id input")
|
|
266
|
+
).toBeInTheDocument();
|
|
267
|
+
expect(
|
|
268
|
+
container.querySelector(".reset-adobe-id button")
|
|
269
|
+
).toBeInTheDocument();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("connected (default export)", () => {
|
|
274
|
+
it("wires state and dispatch through connect", async () => {
|
|
275
|
+
// Defensive: nothing fetches on this mount, but stub fetch so any stray
|
|
276
|
+
// request settles rather than crashing the strict worker.
|
|
277
|
+
jest.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
278
|
+
new Response(JSON.stringify({}), {
|
|
279
|
+
status: 200,
|
|
280
|
+
headers: { "Content-Type": "application/json" },
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
renderWithProviders(
|
|
285
|
+
<ConnectedResetAdobeId
|
|
286
|
+
store={buildStore()}
|
|
287
|
+
csrfToken="token"
|
|
288
|
+
library="nypl"
|
|
289
|
+
/>
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// With no patron in the store, mapStateToProps yields patron=undefined and
|
|
293
|
+
// the connected component prompts the user to search.
|
|
294
|
+
expect(
|
|
295
|
+
await screen.findByRole("heading", { name: "Reset Adobe ID" })
|
|
296
|
+
).toBeInTheDocument();
|
|
297
|
+
expect(
|
|
298
|
+
screen.getByText("Search for a patron to begin.")
|
|
299
|
+
).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|