@thepalaceproject/circulation-admin 1.43.0-post.8 → 1.43.0-post.9
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/AdvancedSearchBooleanFilter.test.tsx +231 -0
- 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/CatalogServices.test.tsx +63 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
- package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
- package/tests/jest/components/ErrorMessage.test.tsx +97 -0
- package/tests/jest/components/Footer.test.tsx +141 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/LoadButton.test.tsx +49 -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 +59 -0
- package/tests/jest/components/SelfTestResult.test.tsx +164 -0
- package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
- package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -0
- package/tests/jest/components/WithEditButton.test.tsx +55 -0
- package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
- package/tests/jest/index.test.tsx +50 -0
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +0 -38
|
@@ -0,0 +1,251 @@
|
|
|
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 CustomListsSidebar from "../../../src/components/CustomListsSidebar";
|
|
5
|
+
|
|
6
|
+
// Render react-router's Link as a marker anchor so we can assert its target
|
|
7
|
+
// (`data-to`), text, and className without needing a Router in the test.
|
|
8
|
+
jest.mock("react-router", () => ({
|
|
9
|
+
...jest.requireActual("react-router"),
|
|
10
|
+
Link: (props) => (
|
|
11
|
+
<a
|
|
12
|
+
data-testid="Link"
|
|
13
|
+
data-to={props.to}
|
|
14
|
+
href={props.to}
|
|
15
|
+
className={props.className}
|
|
16
|
+
>
|
|
17
|
+
{props.children}
|
|
18
|
+
</a>
|
|
19
|
+
),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe("CustomListsSidebar", () => {
|
|
23
|
+
const makeLists = () => [
|
|
24
|
+
{
|
|
25
|
+
id: 1,
|
|
26
|
+
name: "First List",
|
|
27
|
+
entry_count: 5,
|
|
28
|
+
is_owner: true,
|
|
29
|
+
is_shared: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 2,
|
|
33
|
+
name: "Second List",
|
|
34
|
+
entry_count: 10,
|
|
35
|
+
is_owner: true,
|
|
36
|
+
is_shared: false,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const baseProps = () => ({
|
|
41
|
+
filter: "owned",
|
|
42
|
+
lists: makeLists(),
|
|
43
|
+
library: "library_name",
|
|
44
|
+
identifier: "123",
|
|
45
|
+
isLibraryManager: true,
|
|
46
|
+
deleteCustomList: jest.fn(),
|
|
47
|
+
changeSort: jest.fn(),
|
|
48
|
+
sortOrder: "asc",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders a sidebar with a header and a create button", () => {
|
|
52
|
+
const { container } = render(<CustomListsSidebar {...baseProps()} />);
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
container.querySelector(".custom-lists-sidebar")
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
|
|
58
|
+
"List Manager"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const createButton = screen.getAllByTestId("Link")[0];
|
|
62
|
+
expect(createButton).toHaveTextContent("Create New List");
|
|
63
|
+
expect(createButton).toHaveAttribute(
|
|
64
|
+
"data-to",
|
|
65
|
+
"/admin/web/lists/library_name/create"
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders filter select", () => {
|
|
70
|
+
const { container } = render(<CustomListsSidebar {...baseProps()} />);
|
|
71
|
+
|
|
72
|
+
const select = container.querySelector<HTMLSelectElement>(
|
|
73
|
+
'select[name="filter"]'
|
|
74
|
+
);
|
|
75
|
+
expect(select).toHaveValue("owned");
|
|
76
|
+
|
|
77
|
+
const options = within(select).getAllByRole("option");
|
|
78
|
+
expect(options).toHaveLength(4);
|
|
79
|
+
expect(options[0]).toHaveValue("");
|
|
80
|
+
expect(options[1]).toHaveValue("owned");
|
|
81
|
+
expect(options[2]).toHaveValue("shared-out");
|
|
82
|
+
expect(options[3]).toHaveValue("shared-in");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("renders sort select", () => {
|
|
86
|
+
const { container } = render(<CustomListsSidebar {...baseProps()} />);
|
|
87
|
+
|
|
88
|
+
const select = container.querySelector<HTMLSelectElement>(
|
|
89
|
+
'select[name="sort"]'
|
|
90
|
+
);
|
|
91
|
+
expect(select).toHaveValue("asc");
|
|
92
|
+
|
|
93
|
+
const options = within(select).getAllByRole("option");
|
|
94
|
+
expect(options).toHaveLength(2);
|
|
95
|
+
expect(options[0]).toHaveValue("asc");
|
|
96
|
+
expect(options[1]).toHaveValue("desc");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders a list of custom list info items", async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
101
|
+
const deleteCustomList = jest.fn();
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<CustomListsSidebar
|
|
104
|
+
{...baseProps()}
|
|
105
|
+
deleteCustomList={deleteCustomList}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const listItems = container.querySelectorAll("ul > li");
|
|
110
|
+
expect(listItems).toHaveLength(2);
|
|
111
|
+
const [firstList, secondList] = Array.from(listItems);
|
|
112
|
+
|
|
113
|
+
const firstInfo = firstList.querySelectorAll(".custom-list-info > div");
|
|
114
|
+
expect(firstInfo[0]).toHaveTextContent("First List");
|
|
115
|
+
expect(firstInfo[1]).toHaveTextContent("Books in list: 5");
|
|
116
|
+
expect(firstInfo[2]).toHaveTextContent("ID-1");
|
|
117
|
+
|
|
118
|
+
const firstButtons = firstList.querySelector<HTMLElement>(
|
|
119
|
+
".custom-list-buttons"
|
|
120
|
+
);
|
|
121
|
+
const firstEdit = within(firstButtons).getByTestId("Link");
|
|
122
|
+
expect(firstEdit).toHaveTextContent("Edit");
|
|
123
|
+
expect(firstEdit).toHaveAttribute(
|
|
124
|
+
"data-to",
|
|
125
|
+
"/admin/web/lists/library_name/edit/1"
|
|
126
|
+
);
|
|
127
|
+
const firstDelete = within(firstButtons).getByRole("button");
|
|
128
|
+
expect(firstDelete).toHaveTextContent("Delete");
|
|
129
|
+
await user.click(firstDelete);
|
|
130
|
+
expect(deleteCustomList).toHaveBeenCalledTimes(1);
|
|
131
|
+
|
|
132
|
+
const secondInfo = secondList.querySelectorAll(".custom-list-info > div");
|
|
133
|
+
expect(secondInfo[0]).toHaveTextContent("Second List");
|
|
134
|
+
expect(secondInfo[1]).toHaveTextContent("Books in list: 10");
|
|
135
|
+
expect(secondInfo[2]).toHaveTextContent("ID-2");
|
|
136
|
+
|
|
137
|
+
const secondButtons = secondList.querySelector<HTMLElement>(
|
|
138
|
+
".custom-list-buttons"
|
|
139
|
+
);
|
|
140
|
+
const secondEdit = within(secondButtons).getByTestId("Link");
|
|
141
|
+
expect(secondEdit).toHaveTextContent("Edit");
|
|
142
|
+
expect(secondEdit).toHaveAttribute(
|
|
143
|
+
"data-to",
|
|
144
|
+
"/admin/web/lists/library_name/edit/2"
|
|
145
|
+
);
|
|
146
|
+
const secondDelete = within(secondButtons).getByRole("button");
|
|
147
|
+
expect(secondDelete).toHaveTextContent("Delete");
|
|
148
|
+
await user.click(secondDelete);
|
|
149
|
+
expect(deleteCustomList).toHaveBeenCalledTimes(2);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("disables the edit button if the list is already being edited", () => {
|
|
153
|
+
const props = baseProps();
|
|
154
|
+
const { container, rerender } = render(<CustomListsSidebar {...props} />);
|
|
155
|
+
|
|
156
|
+
const firstButtons = () =>
|
|
157
|
+
container.querySelectorAll<HTMLElement>(".custom-list-buttons")[0];
|
|
158
|
+
|
|
159
|
+
const editLink = within(firstButtons()).getByTestId("Link");
|
|
160
|
+
expect(editLink).not.toHaveClass("disabled");
|
|
161
|
+
expect(editLink).toHaveTextContent("EditPencil Icon");
|
|
162
|
+
|
|
163
|
+
rerender(<CustomListsSidebar {...props} identifier="1" />);
|
|
164
|
+
|
|
165
|
+
const editingButton = within(firstButtons()).getByRole("button", {
|
|
166
|
+
name: "Editing",
|
|
167
|
+
});
|
|
168
|
+
expect(editingButton).toBeDisabled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("renders a view button instead of an edit button if a list is not owned", () => {
|
|
172
|
+
const { container } = render(
|
|
173
|
+
<CustomListsSidebar
|
|
174
|
+
{...baseProps()}
|
|
175
|
+
lists={[
|
|
176
|
+
{
|
|
177
|
+
id: 1,
|
|
178
|
+
name: "First List",
|
|
179
|
+
entry_count: 5,
|
|
180
|
+
is_owner: false,
|
|
181
|
+
is_shared: false,
|
|
182
|
+
},
|
|
183
|
+
]}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const firstButtons = container.querySelector<HTMLElement>(
|
|
188
|
+
".custom-list-buttons"
|
|
189
|
+
);
|
|
190
|
+
const viewLink = within(firstButtons).getByTestId("Link");
|
|
191
|
+
expect(viewLink).not.toHaveClass("disabled");
|
|
192
|
+
expect(viewLink).toHaveTextContent("ViewVisible Icon");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("displays the delete button only to library managers", () => {
|
|
196
|
+
const props = baseProps();
|
|
197
|
+
const { container, rerender } = render(<CustomListsSidebar {...props} />);
|
|
198
|
+
|
|
199
|
+
expect(
|
|
200
|
+
container.querySelectorAll(".custom-list-buttons button")
|
|
201
|
+
).toHaveLength(2);
|
|
202
|
+
|
|
203
|
+
rerender(<CustomListsSidebar {...props} isLibraryManager={false} />);
|
|
204
|
+
|
|
205
|
+
expect(
|
|
206
|
+
container.querySelectorAll(".custom-list-buttons button")
|
|
207
|
+
).toHaveLength(0);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not render a delete button if a list is not owned", () => {
|
|
211
|
+
const { container } = render(
|
|
212
|
+
<CustomListsSidebar
|
|
213
|
+
{...baseProps()}
|
|
214
|
+
lists={[
|
|
215
|
+
{
|
|
216
|
+
id: 1,
|
|
217
|
+
name: "First List",
|
|
218
|
+
entry_count: 5,
|
|
219
|
+
is_owner: false,
|
|
220
|
+
is_shared: false,
|
|
221
|
+
},
|
|
222
|
+
]}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(
|
|
227
|
+
container.querySelectorAll(".custom-list-buttons button")
|
|
228
|
+
).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does not render a delete button if a list is shared", () => {
|
|
232
|
+
const { container } = render(
|
|
233
|
+
<CustomListsSidebar
|
|
234
|
+
{...baseProps()}
|
|
235
|
+
lists={[
|
|
236
|
+
{
|
|
237
|
+
id: 1,
|
|
238
|
+
name: "First List",
|
|
239
|
+
entry_count: 5,
|
|
240
|
+
is_owner: true,
|
|
241
|
+
is_shared: true,
|
|
242
|
+
},
|
|
243
|
+
]}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(
|
|
248
|
+
container.querySelectorAll(".custom-list-buttons button")
|
|
249
|
+
).toHaveLength(0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
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 buildStore from "../../../src/store";
|
|
6
|
+
|
|
7
|
+
// The active tab renders DiagnosticsServiceType, which recurses into the service
|
|
8
|
+
// tab machinery. The legacy test only asserted the `type`/`services` props it
|
|
9
|
+
// received, so mock it to a marker that echoes those.
|
|
10
|
+
jest.mock("../../../src/components/DiagnosticsServiceType", () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
default: (props: any) => (
|
|
13
|
+
<div
|
|
14
|
+
data-testid="service-type"
|
|
15
|
+
data-type={props.type}
|
|
16
|
+
data-has-services={String(props.services !== undefined)}
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { DiagnosticsTabContainer } from "../../../src/components/DiagnosticsTabContainer";
|
|
22
|
+
|
|
23
|
+
describe("DiagnosticsTabContainer", () => {
|
|
24
|
+
let goToTab: jest.Mock;
|
|
25
|
+
let fetchDiagnostics: jest.Mock;
|
|
26
|
+
|
|
27
|
+
const ts1 = {
|
|
28
|
+
service: "test_service",
|
|
29
|
+
id: "1",
|
|
30
|
+
start: "start_time_string",
|
|
31
|
+
duration: "0",
|
|
32
|
+
collection_name: "collection1",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const diagnostics = {
|
|
36
|
+
monitor: [{ test_service_1: [{ collection1: [ts1] }] }],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
goToTab = jest.fn();
|
|
41
|
+
fetchDiagnostics = jest.fn();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const element = (overrides = {}) => {
|
|
45
|
+
const props = {
|
|
46
|
+
goToTab,
|
|
47
|
+
tab: "coverage_provider",
|
|
48
|
+
store: buildStore(),
|
|
49
|
+
fetchDiagnostics,
|
|
50
|
+
diagnostics: { coverage_provider: [] },
|
|
51
|
+
isLoaded: true,
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
54
|
+
return <DiagnosticsTabContainer {...props} />;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const renderContainer = (overrides = {}) => render(element(overrides));
|
|
58
|
+
|
|
59
|
+
describe("rendering", () => {
|
|
60
|
+
it("renders a tab container", () => {
|
|
61
|
+
const { container } = renderContainer();
|
|
62
|
+
expect(container.firstChild).toHaveClass("tab-container");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("renders tabs and defaults to showing the Coverage Providers tab", () => {
|
|
66
|
+
const { container } = renderContainer();
|
|
67
|
+
const tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
68
|
+
expect(tabs).toHaveLength(4);
|
|
69
|
+
|
|
70
|
+
expect(tabs[0]).toHaveTextContent("Coverage Providers");
|
|
71
|
+
expect(tabs[0]).toHaveClass("active");
|
|
72
|
+
|
|
73
|
+
expect(tabs[1]).toHaveTextContent("Monitors");
|
|
74
|
+
expect(tabs[1]).not.toHaveClass("active");
|
|
75
|
+
|
|
76
|
+
expect(tabs[2]).toHaveTextContent("Scripts");
|
|
77
|
+
expect(tabs[2]).not.toHaveClass("active");
|
|
78
|
+
|
|
79
|
+
expect(tabs[3]).toHaveTextContent("Other");
|
|
80
|
+
expect(tabs[3]).not.toHaveClass("active");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("renders tab content", () => {
|
|
84
|
+
renderContainer({ diagnostics });
|
|
85
|
+
const serviceTypes = screen.getAllByTestId("service-type");
|
|
86
|
+
expect(serviceTypes).toHaveLength(4);
|
|
87
|
+
|
|
88
|
+
expect(serviceTypes[0]).toHaveAttribute("data-type", "coverage_provider");
|
|
89
|
+
expect(serviceTypes[0]).toHaveAttribute("data-has-services", "false");
|
|
90
|
+
|
|
91
|
+
expect(serviceTypes[1]).toHaveAttribute("data-type", "monitor");
|
|
92
|
+
expect(serviceTypes[1]).toHaveAttribute("data-has-services", "true");
|
|
93
|
+
|
|
94
|
+
expect(serviceTypes[2]).toHaveAttribute("data-type", "script");
|
|
95
|
+
expect(serviceTypes[2]).toHaveAttribute("data-has-services", "false");
|
|
96
|
+
|
|
97
|
+
expect(serviceTypes[3]).toHaveAttribute("data-type", "other");
|
|
98
|
+
expect(serviceTypes[3]).toHaveAttribute("data-has-services", "false");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("behavior", () => {
|
|
103
|
+
it("calls fetchDiagnostics on mount", () => {
|
|
104
|
+
renderContainer();
|
|
105
|
+
expect(fetchDiagnostics).toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("shows the loading indicator", () => {
|
|
109
|
+
const { container, rerender } = renderContainer();
|
|
110
|
+
expect(container.querySelectorAll(".loading")).toHaveLength(0);
|
|
111
|
+
rerender(element({ isLoaded: false }));
|
|
112
|
+
expect(container.querySelectorAll(".loading")).toHaveLength(4);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("shows an error message", () => {
|
|
116
|
+
const { container, rerender } = renderContainer();
|
|
117
|
+
expect(container.querySelectorAll(".alert-danger")).toHaveLength(0);
|
|
118
|
+
rerender(
|
|
119
|
+
element({
|
|
120
|
+
fetchError: { status: 401, response: "error fetching diagnostics" },
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
expect(container.querySelectorAll(".alert-danger")).toHaveLength(4);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("calls goToTab when a tab is clicked", async () => {
|
|
127
|
+
const user = userEvent.setup();
|
|
128
|
+
renderContainer({ diagnostics });
|
|
129
|
+
await user.click(screen.getByRole("link", { name: "Monitors" }));
|
|
130
|
+
expect(goToTab).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(goToTab).toHaveBeenCalledWith("monitor");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("switches tabs when the tab prop changes", () => {
|
|
135
|
+
const { container, rerender } = renderContainer();
|
|
136
|
+
let tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
137
|
+
expect(tabs[0]).toHaveClass("active");
|
|
138
|
+
expect(tabs[1]).not.toHaveClass("active");
|
|
139
|
+
|
|
140
|
+
rerender(element({ tab: "monitor" }));
|
|
141
|
+
|
|
142
|
+
tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
143
|
+
expect(tabs[0]).not.toHaveClass("active");
|
|
144
|
+
expect(tabs[1]).toHaveClass("active");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
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 ErrorMessage from "../../../src/components/ErrorMessage";
|
|
6
|
+
|
|
7
|
+
describe("ErrorMessage", () => {
|
|
8
|
+
it("shows logged out message for 401 error", () => {
|
|
9
|
+
render(<ErrorMessage error={{ status: 401, response: "", url: "" }} />);
|
|
10
|
+
|
|
11
|
+
expect(screen.getByRole("alert")).toHaveTextContent("logged out");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("shows detail for problem detail", () => {
|
|
15
|
+
render(
|
|
16
|
+
<ErrorMessage
|
|
17
|
+
error={{
|
|
18
|
+
status: 500,
|
|
19
|
+
response: JSON.stringify({ status: 500, detail: "detail" }),
|
|
20
|
+
url: "",
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(screen.getByRole("alert")).toHaveTextContent("detail");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("shows response for non-json response", () => {
|
|
29
|
+
render(
|
|
30
|
+
<ErrorMessage error={{ status: 500, response: "response", url: "" }} />
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(screen.getByRole("alert")).toHaveTextContent("response");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("parses non-JSON problem detail string", () => {
|
|
37
|
+
// prettier-ignore
|
|
38
|
+
const response = "Remote service returned a problem detail document: {\"status\": 502, \"detail\": problem text, \"title\": TITLE}";
|
|
39
|
+
render(<ErrorMessage error={{ status: 500, response, url: "" }} />);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
42
|
+
"Remote service returned a problem detail document with status 502: problem text"
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByText("TITLE")).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("can handle missing detail property in non-JSON problem detail string", () => {
|
|
48
|
+
// prettier-ignore
|
|
49
|
+
const response = "Remote service returned a problem detail document: {\"status\": 502, \"title\": TITLE}";
|
|
50
|
+
render(<ErrorMessage error={{ status: 500, response, url: "" }} />);
|
|
51
|
+
|
|
52
|
+
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
53
|
+
"Remote service returned a problem detail document with status 502:"
|
|
54
|
+
);
|
|
55
|
+
expect(screen.getByText("TITLE")).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("can handle missing status property in non-JSON problem detail string", () => {
|
|
59
|
+
// prettier-ignore
|
|
60
|
+
const response = "Remote service returned a problem detail document: {\"detail\": problem text, \"title\": TITLE}";
|
|
61
|
+
render(<ErrorMessage error={{ status: 500, response, url: "" }} />);
|
|
62
|
+
|
|
63
|
+
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
64
|
+
"Remote service returned a problem detail document: problem text"
|
|
65
|
+
);
|
|
66
|
+
expect(screen.getByText("TITLE")).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("can handle missing title property in non-JSON problem detail string", () => {
|
|
70
|
+
// prettier-ignore
|
|
71
|
+
const response = "Remote service returned a problem detail document: {\"status\": 502, \"detail\": problem text}";
|
|
72
|
+
render(<ErrorMessage error={{ status: 500, response, url: "" }} />);
|
|
73
|
+
|
|
74
|
+
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
75
|
+
"Remote service returned a problem detail document with status 502:"
|
|
76
|
+
);
|
|
77
|
+
expect(screen.getByText("Error")).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("shows a try again button that fires the callback when clicked", async () => {
|
|
81
|
+
const user = userEvent.setup();
|
|
82
|
+
const tryAgain = jest.fn();
|
|
83
|
+
render(
|
|
84
|
+
<ErrorMessage
|
|
85
|
+
error={{ status: 500, response: "response", url: "" }}
|
|
86
|
+
tryAgain={tryAgain}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const tryAgainButton = screen.getByRole("button", { name: "Try again" });
|
|
91
|
+
expect(tryAgainButton).toBeInTheDocument();
|
|
92
|
+
|
|
93
|
+
await user.click(tryAgainButton);
|
|
94
|
+
|
|
95
|
+
expect(tryAgain).toHaveBeenCalledTimes(1);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
3
|
+
import Footer, {
|
|
4
|
+
makeFooterSupportContactText,
|
|
5
|
+
} from "../../../src/components/Footer";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_SUPPORT_CONTACT_TEXT,
|
|
8
|
+
SupportContactLink,
|
|
9
|
+
} from "../../../src/context/appContext";
|
|
10
|
+
|
|
11
|
+
const expectedTosText = "Terms of Service Text";
|
|
12
|
+
const expectedTosHref = "terms_of_service";
|
|
13
|
+
const testSupportContactEmail = "helpdesk@example.com";
|
|
14
|
+
const expectedSupportContact: SupportContactLink = {
|
|
15
|
+
href: `mailto:${testSupportContactEmail}?subject=Support+request`,
|
|
16
|
+
text: `Email ${testSupportContactEmail}.`,
|
|
17
|
+
};
|
|
18
|
+
const expectedSupportContactText = makeFooterSupportContactText(
|
|
19
|
+
expectedSupportContact.text
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Builds the app config the ContextProvider transforms into terms-of-service and
|
|
23
|
+
// support-contact context, mirroring the legacy test's `getFooterProviders`.
|
|
24
|
+
const footerConfig = ({ hasSupportContactUrl = false } = {}) => ({
|
|
25
|
+
tos_link_text: expectedTosText,
|
|
26
|
+
tos_link_href: expectedTosHref,
|
|
27
|
+
// Include support contact URL only if we ask for it.
|
|
28
|
+
support_contact_url: hasSupportContactUrl
|
|
29
|
+
? "https://example.com/link-from-deprecated-setting"
|
|
30
|
+
: undefined,
|
|
31
|
+
supportContactUrl: hasSupportContactUrl
|
|
32
|
+
? expectedSupportContact.href
|
|
33
|
+
: undefined,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("Footer", () => {
|
|
37
|
+
it("accepts an optional className prop", () => {
|
|
38
|
+
const { container } = renderWithProviders(
|
|
39
|
+
<Footer className="customClass" />,
|
|
40
|
+
{ appConfigSettings: footerConfig() }
|
|
41
|
+
);
|
|
42
|
+
expect(container.querySelector("footer")).toHaveClass("customClass");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("displays a terms of service link", () => {
|
|
46
|
+
const { container } = renderWithProviders(<Footer />, {
|
|
47
|
+
appConfigSettings: footerConfig(),
|
|
48
|
+
});
|
|
49
|
+
const links = container.querySelectorAll("a");
|
|
50
|
+
expect(links).toHaveLength(1);
|
|
51
|
+
|
|
52
|
+
expect(links[0]).toHaveTextContent(expectedTosText);
|
|
53
|
+
expect(links[0]).toHaveAttribute("href", expectedTosHref);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("support contact link", () => {
|
|
57
|
+
it("displays a support contact link, when URL present", () => {
|
|
58
|
+
const { container } = renderWithProviders(<Footer />, {
|
|
59
|
+
appConfigSettings: footerConfig({ hasSupportContactUrl: true }),
|
|
60
|
+
});
|
|
61
|
+
const links = container.querySelectorAll("a");
|
|
62
|
+
expect(links).toHaveLength(2);
|
|
63
|
+
|
|
64
|
+
// The second link should be the support contact link.
|
|
65
|
+
expect(links[1]).toHaveAttribute("href", expectedSupportContact.href);
|
|
66
|
+
expect(links[1]).toHaveTextContent(expectedSupportContactText);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("omits contact link, when no support URL provided", () => {
|
|
70
|
+
const { container } = renderWithProviders(<Footer />, {
|
|
71
|
+
appConfigSettings: footerConfig({ hasSupportContactUrl: false }),
|
|
72
|
+
});
|
|
73
|
+
// There should only be one link....
|
|
74
|
+
const links = container.querySelectorAll("a");
|
|
75
|
+
expect(links).toHaveLength(1);
|
|
76
|
+
|
|
77
|
+
// ... and it should be the TOS link, not a support contact link.
|
|
78
|
+
expect(links[0]).not.toHaveAttribute("href", expectedSupportContact.href);
|
|
79
|
+
expect(links[0]).toHaveAttribute("href", expectedTosHref);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("displays a default label for an https url", () => {
|
|
83
|
+
const { container } = renderWithProviders(<Footer />, {
|
|
84
|
+
appConfigSettings: { supportContactUrl: "https://example.com/support" },
|
|
85
|
+
});
|
|
86
|
+
const links = container.querySelectorAll("a");
|
|
87
|
+
expect(links).toHaveLength(2);
|
|
88
|
+
expect(links[1]).toHaveAttribute("href", "https://example.com/support");
|
|
89
|
+
expect(links[1]).toHaveTextContent(
|
|
90
|
+
makeFooterSupportContactText(DEFAULT_SUPPORT_CONTACT_TEXT)
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("displays a custom label for an https url", () => {
|
|
95
|
+
const { container } = renderWithProviders(<Footer />, {
|
|
96
|
+
appConfigSettings: {
|
|
97
|
+
supportContactUrl: "https://example.com/support",
|
|
98
|
+
supportContactText: "Custom Support Text",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const links = container.querySelectorAll("a");
|
|
102
|
+
expect(links).toHaveLength(2);
|
|
103
|
+
expect(links[1]).toHaveAttribute("href", "https://example.com/support");
|
|
104
|
+
expect(links[1]).toHaveTextContent(
|
|
105
|
+
makeFooterSupportContactText("Custom Support Text")
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("uses the deprecated support_contact_url if supportContactUrl is missing", () => {
|
|
110
|
+
const { container } = renderWithProviders(<Footer />, {
|
|
111
|
+
appConfigSettings: {
|
|
112
|
+
support_contact_url: "https://example.com/deprecated-support",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const links = container.querySelectorAll("a");
|
|
116
|
+
expect(links).toHaveLength(2);
|
|
117
|
+
expect(links[1]).toHaveAttribute(
|
|
118
|
+
"href",
|
|
119
|
+
"https://example.com/deprecated-support"
|
|
120
|
+
);
|
|
121
|
+
expect(links[1]).toHaveTextContent(
|
|
122
|
+
makeFooterSupportContactText(DEFAULT_SUPPORT_CONTACT_TEXT)
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("displays a custom label for a mailto url", () => {
|
|
127
|
+
const { container } = renderWithProviders(<Footer />, {
|
|
128
|
+
appConfigSettings: {
|
|
129
|
+
supportContactUrl: "mailto:custom@example.com",
|
|
130
|
+
supportContactText: "Email Us Now",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const links = container.querySelectorAll("a");
|
|
134
|
+
expect(links).toHaveLength(2);
|
|
135
|
+
expect(links[1]).toHaveAttribute("href", "mailto:custom@example.com");
|
|
136
|
+
expect(links[1]).toHaveTextContent(
|
|
137
|
+
makeFooterSupportContactText("Email Us Now")
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import LanguageField from "../../../src/components/LanguageField";
|
|
5
|
+
|
|
6
|
+
describe("LanguageField", () => {
|
|
7
|
+
const languages = {
|
|
8
|
+
eng: ["English"],
|
|
9
|
+
es: ["Spanish", "Castilian"],
|
|
10
|
+
spa: ["Spanish", "Castilian"],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it("renders an autocomplete field", () => {
|
|
14
|
+
const { container } = render(
|
|
15
|
+
<LanguageField name="language" languages={languages} />
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// An <input> with a `list` attribute has the implicit ARIA role "combobox".
|
|
19
|
+
const input = screen.getByRole("combobox");
|
|
20
|
+
expect(input).toHaveAttribute("name", "language");
|
|
21
|
+
expect(input).toHaveAttribute("list", "language-autocomplete-list");
|
|
22
|
+
expect(
|
|
23
|
+
container.querySelector("datalist#language-autocomplete-list")
|
|
24
|
+
).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders a unique, ordered list of language names as options", () => {
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<LanguageField name="language" languages={languages} />
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const options = Array.from(
|
|
33
|
+
container.querySelectorAll<HTMLOptionElement>(
|
|
34
|
+
"#language-autocomplete-list option"
|
|
35
|
+
)
|
|
36
|
+
).map((option) => option.value);
|
|
37
|
+
|
|
38
|
+
expect(options).toStrictEqual(["English", "Spanish", "Castilian"]);
|
|
39
|
+
});
|
|
40
|
+
});
|