@thepalaceproject/circulation-admin 1.43.0-post.1 → 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/CLAUDE.md +16 -7
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +56 -9
- package/package.json +6 -16
- package/tests/jest/actions.test.ts +962 -0
- package/tests/jest/components/AccountPage.test.tsx +41 -0
- package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
- package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
- package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
- package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
- package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
- package/tests/jest/components/Announcement.test.tsx +76 -0
- package/tests/jest/components/Autocomplete.test.tsx +67 -0
- package/tests/jest/components/BookDetails.test.tsx +133 -0
- package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
- package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/CatalogServices.test.tsx +63 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -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/ConfigPage.test.tsx +58 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomListsForBook.test.tsx +315 -0
- package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
- package/tests/jest/components/DashboardPage.test.tsx +76 -0
- package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
- package/tests/jest/components/EditableConfigList.test.tsx +8 -2
- package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
- package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
- package/tests/jest/components/ErrorMessage.test.tsx +97 -0
- package/tests/jest/components/Footer.test.tsx +141 -0
- package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/LaneEditor.test.tsx +2 -6
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/LoadButton.test.tsx +49 -0
- package/tests/jest/components/ManagePatrons.test.tsx +73 -0
- package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
- package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
- package/tests/jest/components/MetadataServices.test.tsx +59 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
- package/tests/jest/components/PatronInfo.test.tsx +54 -0
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SaveButton.test.tsx +47 -0
- package/tests/jest/components/SelfTestResult.test.tsx +164 -0
- package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
- package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
- package/tests/jest/components/SetupPage.test.tsx +41 -0
- package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +4 -8
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
- package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
- package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
- package/tests/jest/components/WelcomePage.test.tsx +43 -0
- package/tests/jest/components/WithEditButton.test.tsx +55 -0
- package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
- package/tests/jest/editorAdapter.test.ts +245 -0
- package/tests/jest/index.test.tsx +50 -0
- package/tests/jest/legacy-setup.ts +13 -0
- package/tests/jest/legacyJsdomEnvironment.js +49 -0
- package/tests/jest/models/Admin.test.ts +206 -0
- package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
- package/tests/jest/reducers/classifications.test.ts +161 -0
- package/tests/jest/reducers/collections.test.ts +96 -0
- package/tests/jest/reducers/complaints.test.ts +166 -0
- package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
- package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
- package/tests/jest/reducers/customListDetails.test.ts +120 -0
- package/tests/jest/reducers/customListEditor.test.ts +2823 -0
- package/tests/jest/reducers/managePatrons.test.ts +71 -0
- package/tests/jest/testUtils/formDataShim.test.ts +135 -0
- package/tests/jest/testUtils/formDataShim.ts +112 -0
- package/tests/jest/utils/sharedFunctions.test.ts +121 -0
- package/tsconfig.json +3 -4
- package/testReporter.js +0 -29
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +0 -38
|
@@ -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,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
|
+
});
|
|
@@ -383,7 +383,11 @@ describe("EditableConfigList - library association disclosure", () => {
|
|
|
383
383
|
it("falls back to short_name when allLibraries is absent from the data", () => {
|
|
384
384
|
const { container } = renderWithContext(
|
|
385
385
|
<TestServiceList
|
|
386
|
-
data={{
|
|
386
|
+
data={{
|
|
387
|
+
services: [
|
|
388
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] },
|
|
389
|
+
],
|
|
390
|
+
}}
|
|
387
391
|
fetchData={jest.fn()}
|
|
388
392
|
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
389
393
|
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
@@ -393,7 +397,9 @@ describe("EditableConfigList - library association disclosure", () => {
|
|
|
393
397
|
config
|
|
394
398
|
);
|
|
395
399
|
fireEvent.click(container.querySelector(".association-toggle"));
|
|
396
|
-
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
400
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
401
|
+
"nypl"
|
|
402
|
+
);
|
|
397
403
|
});
|
|
398
404
|
});
|
|
399
405
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
|
|
6
|
+
// The wrapped `Collection` (from web-opds-client) is incidental here — the
|
|
7
|
+
// container's job is to pull the "Formats" facets out and hand them to
|
|
8
|
+
// `EntryPointsTabs`. Mock Collection to a marker so we can render the real
|
|
9
|
+
// EntryPointsTabs and assert the tabs it produces.
|
|
10
|
+
jest.mock(
|
|
11
|
+
"@thepalaceproject/web-opds-client/lib/components/Collection",
|
|
12
|
+
() => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: () => <div data-testid="collection" />,
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// EntryPointsTabs renders CatalogLink -> React Router `Link`, which needs router
|
|
19
|
+
// context renderWithProviders does not supply. Mock `Link` to a marker.
|
|
20
|
+
jest.mock("react-router", () => ({
|
|
21
|
+
...jest.requireActual("react-router"),
|
|
22
|
+
Link: (props) => (
|
|
23
|
+
<div data-testid="Link" data-to={props.to}>
|
|
24
|
+
{props.children}
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import EntryPointsContainer from "../../../src/components/EntryPointsContainer";
|
|
30
|
+
|
|
31
|
+
// Stands in for the OPDS Collection child whose `collection` prop the container
|
|
32
|
+
// reads. It is never rendered by the container (only its props are inspected).
|
|
33
|
+
class CollectionChild extends React.Component<{ collection: unknown }> {
|
|
34
|
+
render() {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("EntryPointsContainer", () => {
|
|
40
|
+
it("should not render entry-point tabs if no facets were passed", () => {
|
|
41
|
+
const collection = {
|
|
42
|
+
facetGroups: [],
|
|
43
|
+
lanes: [],
|
|
44
|
+
books: [],
|
|
45
|
+
navigationLinks: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const { container } = renderWithProviders(
|
|
49
|
+
<EntryPointsContainer>
|
|
50
|
+
<CollectionChild collection={collection} />
|
|
51
|
+
</EntryPointsContainer>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
container.querySelector(".entry-points-tab-container")
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
// EntryPointsTabs renders nothing when there are no format facets.
|
|
58
|
+
expect(container.querySelector(".nav-tabs")).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows a tab container with facets when the label is 'Formats'", () => {
|
|
62
|
+
const facetGroups = [
|
|
63
|
+
{
|
|
64
|
+
facets: [],
|
|
65
|
+
label: "Some other facets",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
facets: [
|
|
69
|
+
{
|
|
70
|
+
label: "Ebooks",
|
|
71
|
+
href: "http://circulation.librarysimplified.org/groups/?entrypoint=Book",
|
|
72
|
+
active: false,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "Audiobooks",
|
|
76
|
+
href: "http://circulation.librarysimplified.org/groups/?entrypoint=Audio",
|
|
77
|
+
active: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
label: "Formats",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
const collection = { facetGroups };
|
|
84
|
+
|
|
85
|
+
const { container } = renderWithProviders(
|
|
86
|
+
<EntryPointsContainer>
|
|
87
|
+
<CollectionChild collection={collection} />
|
|
88
|
+
</EntryPointsContainer>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const list = container.querySelector("ul.nav-tabs");
|
|
92
|
+
expect(list).toBeInTheDocument();
|
|
93
|
+
expect(list.querySelectorAll("li")).toHaveLength(2);
|
|
94
|
+
expect(screen.getByText("Ebooks")).toBeInTheDocument();
|
|
95
|
+
expect(screen.getByText("Audiobooks")).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
import { FacetData } from "@thepalaceproject/web-opds-client/lib/interfaces";
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
|
+
|
|
7
|
+
// Each tab is a CatalogLink, which renders a React Router `Link`. Router `Link`
|
|
8
|
+
// needs router context that `renderWithProviders` does not supply, so mock it to
|
|
9
|
+
// a marker that exposes its navigation target and children (matching Lane.test.tsx).
|
|
10
|
+
jest.mock("react-router", () => ({
|
|
11
|
+
...jest.requireActual("react-router"),
|
|
12
|
+
Link: (props) => (
|
|
13
|
+
<div data-testid="Link" data-to={props.to}>
|
|
14
|
+
{props.children}
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { EntryPointsTabs } from "../../../src/components/EntryPointsTabs";
|
|
20
|
+
|
|
21
|
+
const twoFacets: FacetData[] = [
|
|
22
|
+
{
|
|
23
|
+
label: "Ebooks",
|
|
24
|
+
href: "http://circulation.librarysimplified.org/groups/?entrypoint=Book",
|
|
25
|
+
active: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "Audiobooks",
|
|
29
|
+
href: "http://circulation.librarysimplified.org/groups/?entrypoint=Audio",
|
|
30
|
+
active: false,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
describe("EntryPointsTabs", () => {
|
|
35
|
+
it("should not generate any tabs", () => {
|
|
36
|
+
const { container } = renderWithProviders(<EntryPointsTabs />);
|
|
37
|
+
|
|
38
|
+
expect(container.querySelector(".nav-tabs")).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should generate two tabs with two entry points", () => {
|
|
42
|
+
const { container } = renderWithProviders(
|
|
43
|
+
<EntryPointsTabs facets={twoFacets} />
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const list = container.querySelector("ul.nav-tabs");
|
|
47
|
+
expect(list).toBeInTheDocument();
|
|
48
|
+
|
|
49
|
+
const items = list.querySelectorAll("li");
|
|
50
|
+
expect(items).toHaveLength(2);
|
|
51
|
+
expect(items[0]).toHaveClass("active");
|
|
52
|
+
expect(items[1]).not.toHaveClass("active");
|
|
53
|
+
|
|
54
|
+
expect(screen.getByText("Ebooks")).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByText("Audiobooks")).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("uses router to navigate when a tab is clicked", () => {
|
|
59
|
+
renderWithProviders(<EntryPointsTabs facets={twoFacets} />);
|
|
60
|
+
|
|
61
|
+
const links = screen.getAllByTestId("Link");
|
|
62
|
+
expect(links).toHaveLength(2);
|
|
63
|
+
expect(links[0]).toHaveTextContent("Ebooks");
|
|
64
|
+
expect(links[1]).toHaveTextContent("Audiobooks");
|
|
65
|
+
// Each tab links somewhere for the router to navigate to.
|
|
66
|
+
expect(links[0]).toHaveAttribute("data-to");
|
|
67
|
+
expect(links[1]).toHaveAttribute("data-to");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should generate one tab with an active entry point", () => {
|
|
71
|
+
const oneFacet: FacetData[] = [
|
|
72
|
+
{
|
|
73
|
+
label: "Audiobooks",
|
|
74
|
+
href: "http://circulation.librarysimplified.org/groups/?entrypoint=Audio",
|
|
75
|
+
active: true,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
const { container } = renderWithProviders(
|
|
79
|
+
<EntryPointsTabs facets={oneFacet} />
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const items = container.querySelectorAll("ul.nav-tabs li");
|
|
83
|
+
expect(items).toHaveLength(1);
|
|
84
|
+
expect(items[0]).toHaveClass("active");
|
|
85
|
+
|
|
86
|
+
expect(screen.getByText("Audiobooks")).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -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
|
+
});
|