@thepalaceproject/circulation-admin 1.43.0-post.10 → 1.43.0-post.11
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/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/Classifications.test.tsx +166 -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/CustomListEditor.test.tsx +367 -0
- 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/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -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,141 @@
|
|
|
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 DiagnosticsServiceTabs from "../../../src/components/DiagnosticsServiceTabs";
|
|
6
|
+
|
|
7
|
+
describe("DiagnosticsServiceTabs", () => {
|
|
8
|
+
let goToTab: jest.Mock;
|
|
9
|
+
|
|
10
|
+
const ts1 = {
|
|
11
|
+
service: "test_service_1",
|
|
12
|
+
id: "1",
|
|
13
|
+
start: "start_time_string_1",
|
|
14
|
+
duration: "0",
|
|
15
|
+
collection_name: "collection1",
|
|
16
|
+
};
|
|
17
|
+
const ts2 = {
|
|
18
|
+
service: "test_service_2",
|
|
19
|
+
id: "2",
|
|
20
|
+
start: "start_time_string_2",
|
|
21
|
+
duration: "0",
|
|
22
|
+
collection_name: "collection2",
|
|
23
|
+
exception: "Stack trace",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const content: any = {
|
|
27
|
+
test_service_1: { collection1: [ts1] },
|
|
28
|
+
test_service_2: { collection2: [ts2] },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const renderTabs = () => {
|
|
32
|
+
goToTab = jest.fn();
|
|
33
|
+
return render(
|
|
34
|
+
<DiagnosticsServiceTabs
|
|
35
|
+
goToTab={goToTab}
|
|
36
|
+
content={content}
|
|
37
|
+
tab="test_service_1"
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Find the collapsible `.panel` whose header text matches `title`. The
|
|
43
|
+
// collection panels are titled by collection name; the timestamp panels nested
|
|
44
|
+
// inside them are titled by their start time.
|
|
45
|
+
const panelByTitle = (container: HTMLElement, title: string) => {
|
|
46
|
+
const titleEl = Array.from(container.querySelectorAll(".panel-title")).find(
|
|
47
|
+
(el) => el.textContent === title
|
|
48
|
+
);
|
|
49
|
+
return titleEl ? titleEl.closest(".panel") : null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
describe("nav", () => {
|
|
53
|
+
it("renders a tab for each service", () => {
|
|
54
|
+
const { container } = renderTabs();
|
|
55
|
+
expect(container.querySelectorAll("ul.nav-tabs")).toHaveLength(1);
|
|
56
|
+
const tabs = container.querySelectorAll("ul.nav-tabs li a");
|
|
57
|
+
expect(tabs).toHaveLength(2);
|
|
58
|
+
expect(tabs[0]).toHaveTextContent("Test_service_1");
|
|
59
|
+
expect(tabs[1]).toHaveTextContent("Test_service_2");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("displays a badge", () => {
|
|
63
|
+
const { container } = renderTabs();
|
|
64
|
+
const tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
65
|
+
|
|
66
|
+
// The first service has no exception: a plain count badge with a tooltip.
|
|
67
|
+
const tabWithoutException = tabs[0];
|
|
68
|
+
expect(tabWithoutException.querySelector(".danger")).toBeNull();
|
|
69
|
+
const badge = tabWithoutException.querySelector(".badge");
|
|
70
|
+
expect(badge).toHaveTextContent("1");
|
|
71
|
+
const toolTip = tabWithoutException.querySelector(".tool-tip");
|
|
72
|
+
expect(toolTip).toHaveTextContent(
|
|
73
|
+
"Total number of timestamps for this service"
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// The second service contains a timestamp with an exception, so it shows a
|
|
77
|
+
// danger badge (a warning) and no tooltip.
|
|
78
|
+
const tabWithException = tabs[1];
|
|
79
|
+
const dangerBadge = tabWithException.querySelector(".badge.danger");
|
|
80
|
+
expect(dangerBadge).toHaveTextContent("!");
|
|
81
|
+
expect(tabWithException.querySelector(".tool-tip")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("defaults to showing the first tab", () => {
|
|
85
|
+
const { container } = renderTabs();
|
|
86
|
+
const tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
87
|
+
expect(tabs[0]).toHaveClass("active");
|
|
88
|
+
expect(tabs[1]).not.toHaveClass("active");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("content", () => {
|
|
93
|
+
it("renders collections", () => {
|
|
94
|
+
const { container } = renderTabs();
|
|
95
|
+
expect(container.querySelectorAll(".tab-content")).toHaveLength(1);
|
|
96
|
+
|
|
97
|
+
// collection1 has no exception, so its panel starts collapsed.
|
|
98
|
+
const collection1 = panelByTitle(container, "collection1");
|
|
99
|
+
expect(collection1).toBeInTheDocument();
|
|
100
|
+
expect(collection1.querySelector(".panel-heading")).toHaveAttribute(
|
|
101
|
+
"aria-expanded",
|
|
102
|
+
"false"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// collection2 contains a timestamp with an exception, so its panel starts
|
|
106
|
+
// expanded.
|
|
107
|
+
const collection2 = panelByTitle(container, "collection2");
|
|
108
|
+
expect(collection2).toBeInTheDocument();
|
|
109
|
+
expect(collection2.querySelector(".panel-heading")).toHaveAttribute(
|
|
110
|
+
"aria-expanded",
|
|
111
|
+
"true"
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders timestamps", () => {
|
|
116
|
+
const { container } = renderTabs();
|
|
117
|
+
// Each timestamp renders as a panel headed by its start time.
|
|
118
|
+
expect(
|
|
119
|
+
panelByTitle(container, "start_time_string_1")
|
|
120
|
+
).toBeInTheDocument();
|
|
121
|
+
expect(
|
|
122
|
+
panelByTitle(container, "start_time_string_2")
|
|
123
|
+
).toBeInTheDocument();
|
|
124
|
+
// Both timestamps render their duration.
|
|
125
|
+
expect(screen.getAllByText("Duration: 0 seconds")).toHaveLength(2);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("behavior", () => {
|
|
130
|
+
it("switches tabs", async () => {
|
|
131
|
+
const user = userEvent.setup();
|
|
132
|
+
const { container } = renderTabs();
|
|
133
|
+
const tabLinks = container.querySelectorAll("ul.nav-tabs li a");
|
|
134
|
+
|
|
135
|
+
await user.click(tabLinks[1]);
|
|
136
|
+
|
|
137
|
+
expect(goToTab).toHaveBeenCalledTimes(1);
|
|
138
|
+
expect(goToTab).toHaveBeenCalledWith("test_service_2");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
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 DiagnosticsServiceType from "../../../src/components/DiagnosticsServiceType";
|
|
6
|
+
|
|
7
|
+
describe("DiagnosticsServiceType", () => {
|
|
8
|
+
const ts1 = {
|
|
9
|
+
service: "test_service_1",
|
|
10
|
+
id: "1",
|
|
11
|
+
start: "start_time_string_1",
|
|
12
|
+
duration: "0",
|
|
13
|
+
collection_name: "collection1",
|
|
14
|
+
};
|
|
15
|
+
const ts2 = {
|
|
16
|
+
service: "test_service_2",
|
|
17
|
+
id: "2",
|
|
18
|
+
start: "start_time_string_2",
|
|
19
|
+
duration: "0",
|
|
20
|
+
collection_name: "collection2",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const services: any = {
|
|
24
|
+
test_service_1: { collection1: [ts1] },
|
|
25
|
+
test_service_2: { collection2: [ts2] },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
it("renders service tabs", () => {
|
|
29
|
+
const { container } = render(
|
|
30
|
+
<DiagnosticsServiceType type="monitor" services={services} />
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(container.firstChild).toHaveClass("config");
|
|
34
|
+
expect(container.firstChild).toHaveClass("services");
|
|
35
|
+
|
|
36
|
+
// The service tabs are rendered, one per service, defaulting to the first.
|
|
37
|
+
const tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
38
|
+
expect(tabs).toHaveLength(2);
|
|
39
|
+
expect(tabs[0]).toHaveTextContent("Test_service_1");
|
|
40
|
+
expect(tabs[0]).toHaveClass("active");
|
|
41
|
+
expect(tabs[1]).toHaveTextContent("Test_service_2");
|
|
42
|
+
expect(tabs[1]).not.toHaveClass("active");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("switches tabs", async () => {
|
|
46
|
+
const user = userEvent.setup();
|
|
47
|
+
const { container } = render(
|
|
48
|
+
<DiagnosticsServiceType type="monitor" services={services} />
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Starts on the first service tab.
|
|
52
|
+
let tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
53
|
+
expect(tabs[0]).toHaveClass("active");
|
|
54
|
+
expect(tabs[1]).not.toHaveClass("active");
|
|
55
|
+
|
|
56
|
+
// Clicking the second service tab selects it (goToTab updates the state that
|
|
57
|
+
// drives which tab is active).
|
|
58
|
+
await user.click(container.querySelectorAll("ul.nav-tabs li a")[1]);
|
|
59
|
+
|
|
60
|
+
tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
61
|
+
expect(tabs[0]).not.toHaveClass("active");
|
|
62
|
+
expect(tabs[1]).toHaveClass("active");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("displays a message if there are no services", () => {
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<DiagnosticsServiceType type="monitor" services={null} />
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(container.querySelector("ul.nav-tabs")).toBeNull();
|
|
71
|
+
expect(
|
|
72
|
+
screen.getByText("There are currently no monitor services.")
|
|
73
|
+
).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { installFormDataShim } from "../testUtils/formDataShim";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import LibraryEditForm, {
|
|
7
|
+
LibraryEditFormProps,
|
|
8
|
+
} from "../../../src/components/LibraryEditForm";
|
|
9
|
+
import {
|
|
10
|
+
LibrariesData,
|
|
11
|
+
LibraryData,
|
|
12
|
+
LibrarySettingField,
|
|
13
|
+
} from "../../../src/interfaces";
|
|
14
|
+
installFormDataShim();
|
|
15
|
+
|
|
16
|
+
describe("LibraryEditForm", () => {
|
|
17
|
+
const libraryData = {
|
|
18
|
+
uuid: "uuid",
|
|
19
|
+
name: "name",
|
|
20
|
+
short_name: "short_name",
|
|
21
|
+
// prettier-ignore
|
|
22
|
+
settings: {
|
|
23
|
+
"privacy-policy": "http://privacy",
|
|
24
|
+
"copyright": "http://copyright",
|
|
25
|
+
"featured_lane_size": "20",
|
|
26
|
+
"announcements": "[{\"content\": \"Announcement #1\", \"start\": \"2020-06-15\", \"finish\": \"2020-08-15\", \"id\": \"1\"}]"
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const settingFields = [
|
|
31
|
+
{ key: "name", label: "Name", category: "Basic Information", level: 3 },
|
|
32
|
+
{
|
|
33
|
+
key: "short_name",
|
|
34
|
+
label: "Short Name",
|
|
35
|
+
category: "Basic Information",
|
|
36
|
+
level: 3,
|
|
37
|
+
},
|
|
38
|
+
{ key: "privacy-policy", label: "Privacy Policy", category: "Links" },
|
|
39
|
+
{ key: "copyright", label: "Copyright", category: "Links" },
|
|
40
|
+
{ key: "logo", label: "Logo", category: "Client Interface Customization" },
|
|
41
|
+
{ key: "large_collections", label: "Languages", category: "Languages" },
|
|
42
|
+
{
|
|
43
|
+
key: "featured_lane_size",
|
|
44
|
+
label: "Maximum number of books in the 'featured' lanes",
|
|
45
|
+
category: "Lanes & Filters",
|
|
46
|
+
type: "number",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "service_area",
|
|
50
|
+
label: "Service Area",
|
|
51
|
+
category: "Geographic Areas",
|
|
52
|
+
type: "list",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "announcements",
|
|
56
|
+
label: "Announcements",
|
|
57
|
+
category: "Announcements",
|
|
58
|
+
type: "announcements",
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
type Overrides = Partial<LibraryEditFormProps> & {
|
|
63
|
+
fetchError?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const makeData = (
|
|
67
|
+
settings: LibrarySettingField[] = settingFields as LibrarySettingField[]
|
|
68
|
+
): LibrariesData => ({
|
|
69
|
+
libraries: [libraryData as LibraryData],
|
|
70
|
+
settings,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const makeProps = (overrides: Overrides = {}): LibraryEditFormProps => ({
|
|
74
|
+
data: makeData(),
|
|
75
|
+
disabled: false,
|
|
76
|
+
save: jest.fn(),
|
|
77
|
+
urlBase: "url base",
|
|
78
|
+
listDataKey: "libraries",
|
|
79
|
+
adminLevel: 3,
|
|
80
|
+
...(overrides as LibraryEditFormProps),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const renderForm = (overrides: Overrides = {}) => {
|
|
84
|
+
const props = makeProps(overrides);
|
|
85
|
+
const result = render(<LibraryEditForm {...props} />);
|
|
86
|
+
const rerender = (nextOverrides: Overrides) =>
|
|
87
|
+
result.rerender(<LibraryEditForm {...makeProps(nextOverrides)} />);
|
|
88
|
+
return { ...result, rerender, props };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const inputByName = (container: HTMLElement, name: string) =>
|
|
92
|
+
container.querySelector<HTMLInputElement>(`input[name="${name}"]`);
|
|
93
|
+
|
|
94
|
+
const panelByName = (container: HTMLElement, name: string) =>
|
|
95
|
+
Array.from(container.querySelectorAll<HTMLElement>(".panel")).find(
|
|
96
|
+
(panel) =>
|
|
97
|
+
panel.querySelector(".panel-heading")?.textContent?.startsWith(name) ??
|
|
98
|
+
false
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
describe("rendering", () => {
|
|
102
|
+
it("renders hidden uuid", () => {
|
|
103
|
+
const { container, rerender } = renderForm();
|
|
104
|
+
expect(inputByName(container, "uuid")).toHaveValue("");
|
|
105
|
+
|
|
106
|
+
rerender({ item: libraryData as LibraryData });
|
|
107
|
+
expect(inputByName(container, "uuid")).toHaveValue("uuid");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("renders name", () => {
|
|
111
|
+
const { container, rerender } = renderForm();
|
|
112
|
+
expect(inputByName(container, "name")).toHaveValue("");
|
|
113
|
+
|
|
114
|
+
rerender({ item: libraryData as LibraryData });
|
|
115
|
+
expect(inputByName(container, "name")).toHaveValue("name");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders short name", () => {
|
|
119
|
+
const { container, rerender } = renderForm();
|
|
120
|
+
expect(inputByName(container, "short_name")).toHaveValue("");
|
|
121
|
+
|
|
122
|
+
rerender({ item: libraryData as LibraryData });
|
|
123
|
+
expect(inputByName(container, "short_name")).toHaveValue("short_name");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("renders settings", () => {
|
|
127
|
+
const { container, rerender } = renderForm();
|
|
128
|
+
expect(inputByName(container, "privacy-policy")).toHaveValue("");
|
|
129
|
+
expect(inputByName(container, "copyright")).toHaveValue("");
|
|
130
|
+
|
|
131
|
+
rerender({ item: libraryData as LibraryData });
|
|
132
|
+
expect(inputByName(container, "privacy-policy")).toHaveValue(
|
|
133
|
+
"http://privacy"
|
|
134
|
+
);
|
|
135
|
+
expect(inputByName(container, "copyright")).toHaveValue(
|
|
136
|
+
"http://copyright"
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does not render settings with the skip attribute", () => {
|
|
141
|
+
const skip = { key: "skip", label: "Skip this setting!", skip: true };
|
|
142
|
+
const { container } = renderForm({
|
|
143
|
+
data: makeData([...settingFields, skip]),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// A normal setting still renders, but the skipped one does not.
|
|
147
|
+
expect(inputByName(container, "privacy-policy")).toBeInTheDocument();
|
|
148
|
+
expect(inputByName(container, "skip")).not.toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("renders the PairedMenus component", () => {
|
|
152
|
+
const inputListSetting = {
|
|
153
|
+
key: "inputList",
|
|
154
|
+
label: "Input List",
|
|
155
|
+
paired: "dropdown",
|
|
156
|
+
options: [],
|
|
157
|
+
default: ["opt_1"],
|
|
158
|
+
level: 2,
|
|
159
|
+
};
|
|
160
|
+
const dropdownSetting = {
|
|
161
|
+
key: "dropdown",
|
|
162
|
+
label: "Dropdown List",
|
|
163
|
+
options: [
|
|
164
|
+
{ key: "opt_1", label: "Option 1" },
|
|
165
|
+
{ key: "opt_2", label: "Option 2" },
|
|
166
|
+
],
|
|
167
|
+
type: "select",
|
|
168
|
+
level: 2,
|
|
169
|
+
};
|
|
170
|
+
const { container, rerender } = renderForm({
|
|
171
|
+
data: makeData([inputListSetting, dropdownSetting]),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// These settings render inside a collapsed (aria-hidden) panel, so query
|
|
175
|
+
// the DOM directly rather than via accessible-role queries.
|
|
176
|
+
const pairedMenus = container.querySelector<HTMLElement>(".paired-menus");
|
|
177
|
+
expect(pairedMenus).toBeInTheDocument();
|
|
178
|
+
|
|
179
|
+
// The dropdown inside the paired menus is filtered to the option that is
|
|
180
|
+
// present in the input list (opt_1).
|
|
181
|
+
const dropdown = pairedMenus.querySelector<HTMLSelectElement>(
|
|
182
|
+
'select[name="dropdown"]'
|
|
183
|
+
);
|
|
184
|
+
const options = dropdown.querySelectorAll("option");
|
|
185
|
+
expect(options).toHaveLength(1);
|
|
186
|
+
expect(options[0]).toHaveValue("opt_1");
|
|
187
|
+
|
|
188
|
+
// At admin level 3 the setting is editable, so the input list's remove
|
|
189
|
+
// button is enabled.
|
|
190
|
+
expect(pairedMenus.querySelector(".remove-btn")).toBeEnabled();
|
|
191
|
+
|
|
192
|
+
// A librarian (level 1) is not authorized, so the paired menus become
|
|
193
|
+
// read-only and the remove button is disabled.
|
|
194
|
+
rerender({
|
|
195
|
+
data: makeData([inputListSetting, dropdownSetting]),
|
|
196
|
+
adminLevel: 1,
|
|
197
|
+
});
|
|
198
|
+
const readOnlyPaired =
|
|
199
|
+
container.querySelector<HTMLElement>(".paired-menus");
|
|
200
|
+
expect(readOnlyPaired.querySelector(".remove-btn")).toBeDisabled();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("renders the InputList component", () => {
|
|
204
|
+
const inputListSetting = {
|
|
205
|
+
key: "inputList",
|
|
206
|
+
label: "Input List",
|
|
207
|
+
options: [],
|
|
208
|
+
default: ["opt_1"],
|
|
209
|
+
level: 2,
|
|
210
|
+
menuOptions: [],
|
|
211
|
+
type: "menu",
|
|
212
|
+
};
|
|
213
|
+
const { container, rerender } = renderForm({
|
|
214
|
+
data: makeData([inputListSetting]),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// This setting renders inside a collapsed (aria-hidden) panel, so query
|
|
218
|
+
// the DOM directly rather than via accessible-role queries.
|
|
219
|
+
expect(container.querySelector(".input-list")).toBeInTheDocument();
|
|
220
|
+
// The existing list item is editable and removable for an authorized admin.
|
|
221
|
+
expect(inputByName(container, "inputList_opt_1")).not.toHaveAttribute(
|
|
222
|
+
"readonly"
|
|
223
|
+
);
|
|
224
|
+
expect(container.querySelector(".remove-btn")).toBeEnabled();
|
|
225
|
+
|
|
226
|
+
// A librarian (level 1) is not authorized: the item becomes read-only and
|
|
227
|
+
// the remove button is disabled.
|
|
228
|
+
rerender({ data: makeData([inputListSetting]), adminLevel: 1 });
|
|
229
|
+
expect(inputByName(container, "inputList_opt_1")).toHaveAttribute(
|
|
230
|
+
"readonly"
|
|
231
|
+
);
|
|
232
|
+
expect(container.querySelector(".remove-btn")).toBeDisabled();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("renders the Announcements component", () => {
|
|
236
|
+
const { container } = renderForm({ item: libraryData as LibraryData });
|
|
237
|
+
|
|
238
|
+
// The announcements section renders the library's saved announcement.
|
|
239
|
+
expect(
|
|
240
|
+
container.querySelector(".announcements-section")
|
|
241
|
+
).toBeInTheDocument();
|
|
242
|
+
expect(screen.getByText("Scheduled Announcements:")).toBeInTheDocument();
|
|
243
|
+
expect(screen.getByText(/Announcement #1/)).toBeInTheDocument();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("subdivides fields", () => {
|
|
247
|
+
const { container } = renderForm();
|
|
248
|
+
|
|
249
|
+
const panels = container.querySelectorAll(".panel");
|
|
250
|
+
expect(panels).toHaveLength(7);
|
|
251
|
+
|
|
252
|
+
// The first panel is the (required) Basic Information panel; the rest are
|
|
253
|
+
// optional.
|
|
254
|
+
const basic = panels[0].querySelector(".panel-heading");
|
|
255
|
+
expect(basic).toHaveTextContent("Basic Information");
|
|
256
|
+
Array.from(panels)
|
|
257
|
+
.slice(1)
|
|
258
|
+
.forEach((panel) => {
|
|
259
|
+
expect(panel.querySelector(".panel-heading")).toHaveTextContent(
|
|
260
|
+
"(Optional)"
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// The Links panel holds two settings.
|
|
265
|
+
const links = panelByName(container, "Links");
|
|
266
|
+
expect(inputByName(links, "privacy-policy")).toBeInTheDocument();
|
|
267
|
+
expect(inputByName(links, "copyright")).toBeInTheDocument();
|
|
268
|
+
expect(
|
|
269
|
+
panelByName(container, "Languages").querySelectorAll("input")
|
|
270
|
+
).toHaveLength(1);
|
|
271
|
+
expect(
|
|
272
|
+
panelByName(
|
|
273
|
+
container,
|
|
274
|
+
"Client Interface Customization"
|
|
275
|
+
).querySelectorAll("input")
|
|
276
|
+
).toHaveLength(1);
|
|
277
|
+
|
|
278
|
+
// Geographic Areas is a list input, which renders an add-list-item span
|
|
279
|
+
// and button.
|
|
280
|
+
const geographic = panelByName(container, "Geographic Areas");
|
|
281
|
+
expect(geographic.querySelectorAll(".add-list-item")).toHaveLength(2);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("has a save button", () => {
|
|
285
|
+
renderForm();
|
|
286
|
+
expect(
|
|
287
|
+
screen.getByRole("button", { name: "Submit" })
|
|
288
|
+
).toBeInTheDocument();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("disables some fields if the user isn't a system admin", () => {
|
|
292
|
+
const sysAdminOnly = {
|
|
293
|
+
key: "level-3",
|
|
294
|
+
label: "Level 3",
|
|
295
|
+
category: "Basic Information",
|
|
296
|
+
level: 3,
|
|
297
|
+
};
|
|
298
|
+
const sysAdminAndLibMgr = {
|
|
299
|
+
key: "level-2",
|
|
300
|
+
label: "Level 2",
|
|
301
|
+
category: "Basic Information",
|
|
302
|
+
level: 2,
|
|
303
|
+
};
|
|
304
|
+
const data = makeData([
|
|
305
|
+
...settingFields,
|
|
306
|
+
sysAdminOnly,
|
|
307
|
+
sysAdminAndLibMgr,
|
|
308
|
+
]);
|
|
309
|
+
const { container, rerender } = renderForm({ data, adminLevel: 3 });
|
|
310
|
+
|
|
311
|
+
// System admin: everything is editable.
|
|
312
|
+
["name", "short_name", "level-3", "level-2"].forEach((name) =>
|
|
313
|
+
expect(inputByName(container, name)).not.toHaveAttribute("readonly")
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Library manager (level 2): level-3 (and the always-restricted name/short
|
|
317
|
+
// name) are read-only, level-2 is editable.
|
|
318
|
+
rerender({ data, adminLevel: 2 });
|
|
319
|
+
["name", "short_name", "level-3"].forEach((name) =>
|
|
320
|
+
expect(inputByName(container, name)).toHaveAttribute("readonly")
|
|
321
|
+
);
|
|
322
|
+
expect(inputByName(container, "level-2")).not.toHaveAttribute("readonly");
|
|
323
|
+
|
|
324
|
+
// Librarian (level 1): level-2 is now read-only too.
|
|
325
|
+
rerender({ data, adminLevel: 1 });
|
|
326
|
+
["name", "short_name", "level-3", "level-2"].forEach((name) =>
|
|
327
|
+
expect(inputByName(container, name)).toHaveAttribute("readonly")
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("behavior", () => {
|
|
333
|
+
it("calls save when the save button is clicked", async () => {
|
|
334
|
+
const user = userEvent.setup();
|
|
335
|
+
const save = jest.fn();
|
|
336
|
+
renderForm({ save });
|
|
337
|
+
|
|
338
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
339
|
+
|
|
340
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("submits data", async () => {
|
|
344
|
+
const user = userEvent.setup();
|
|
345
|
+
const save = jest.fn();
|
|
346
|
+
renderForm({ save, item: libraryData as LibraryData });
|
|
347
|
+
|
|
348
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
349
|
+
|
|
350
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
351
|
+
const formData = save.mock.calls[0][0];
|
|
352
|
+
expect(formData.get("uuid")).toBe("uuid");
|
|
353
|
+
expect(formData.get("name")).toBe("name");
|
|
354
|
+
expect(formData.get("short_name")).toBe("short_name");
|
|
355
|
+
expect(formData.get("privacy-policy")).toBe("http://privacy");
|
|
356
|
+
expect(formData.get("copyright")).toBe("http://copyright");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("clears the form when a response comes back without an error", async () => {
|
|
360
|
+
const user = userEvent.setup();
|
|
361
|
+
const { container, rerender } = renderForm();
|
|
362
|
+
|
|
363
|
+
await user.type(inputByName(container, "name"), "new name");
|
|
364
|
+
expect(inputByName(container, "name")).toHaveValue("new name");
|
|
365
|
+
|
|
366
|
+
// A successful response (responseBody with no error) clears the form.
|
|
367
|
+
rerender({ responseBody: "new library" });
|
|
368
|
+
expect(inputByName(container, "name")).toHaveValue("");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("doesn't clear the form if there's an error message", async () => {
|
|
372
|
+
const user = userEvent.setup();
|
|
373
|
+
const { container, rerender } = renderForm();
|
|
374
|
+
|
|
375
|
+
await user.type(inputByName(container, "name"), "new name");
|
|
376
|
+
expect(inputByName(container, "name")).toHaveValue("new name");
|
|
377
|
+
|
|
378
|
+
// An error response leaves the entered value in place.
|
|
379
|
+
rerender({ fetchError: "ERROR" });
|
|
380
|
+
expect(inputByName(container, "name")).toHaveValue("new name");
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|