@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.
Files changed (94) hide show
  1. package/CLAUDE.md +16 -7
  2. package/dist/circulation-admin.js +1 -1
  3. package/eslint.config.mjs +9 -1
  4. package/jest.config.js +56 -9
  5. package/package.json +6 -16
  6. package/tests/jest/actions.test.ts +962 -0
  7. package/tests/jest/components/AccountPage.test.tsx +41 -0
  8. package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
  9. package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
  10. package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
  11. package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
  12. package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
  13. package/tests/jest/components/Announcement.test.tsx +76 -0
  14. package/tests/jest/components/Autocomplete.test.tsx +67 -0
  15. package/tests/jest/components/BookDetails.test.tsx +133 -0
  16. package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
  17. package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
  18. package/tests/jest/components/BookEditForm.test.tsx +352 -0
  19. package/tests/jest/components/CatalogServices.test.tsx +63 -0
  20. package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
  21. package/tests/jest/components/Classifications.test.tsx +166 -0
  22. package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
  23. package/tests/jest/components/ColorPicker.test.tsx +58 -0
  24. package/tests/jest/components/ComplaintForm.test.tsx +171 -0
  25. package/tests/jest/components/Complaints.test.tsx +259 -0
  26. package/tests/jest/components/ConfigPage.test.tsx +58 -0
  27. package/tests/jest/components/ContextProvider.test.tsx +138 -0
  28. package/tests/jest/components/CustomListEditor.test.tsx +367 -0
  29. package/tests/jest/components/CustomListPage.test.tsx +57 -0
  30. package/tests/jest/components/CustomListSearch.test.tsx +275 -0
  31. package/tests/jest/components/CustomListsForBook.test.tsx +315 -0
  32. package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
  33. package/tests/jest/components/DashboardPage.test.tsx +76 -0
  34. package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
  35. package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
  36. package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
  37. package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
  38. package/tests/jest/components/EditableConfigList.test.tsx +8 -2
  39. package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
  40. package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
  41. package/tests/jest/components/ErrorMessage.test.tsx +97 -0
  42. package/tests/jest/components/Footer.test.tsx +141 -0
  43. package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
  44. package/tests/jest/components/LaneEditor.test.tsx +2 -6
  45. package/tests/jest/components/LanePage.test.tsx +68 -0
  46. package/tests/jest/components/LanguageField.test.tsx +40 -0
  47. package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
  48. package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
  49. package/tests/jest/components/LoadButton.test.tsx +49 -0
  50. package/tests/jest/components/ManagePatrons.test.tsx +73 -0
  51. package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
  52. package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
  53. package/tests/jest/components/MetadataServices.test.tsx +59 -0
  54. package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
  55. package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
  56. package/tests/jest/components/PatronInfo.test.tsx +54 -0
  57. package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
  58. package/tests/jest/components/SaveButton.test.tsx +47 -0
  59. package/tests/jest/components/SelfTestResult.test.tsx +164 -0
  60. package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
  61. package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
  62. package/tests/jest/components/SetupPage.test.tsx +41 -0
  63. package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
  64. package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
  65. package/tests/jest/components/Stats.test.tsx +4 -8
  66. package/tests/jest/components/TabContainer.test.tsx +68 -0
  67. package/tests/jest/components/Timestamp.test.tsx +74 -0
  68. package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
  69. package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
  70. package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
  71. package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
  72. package/tests/jest/components/WelcomePage.test.tsx +43 -0
  73. package/tests/jest/components/WithEditButton.test.tsx +55 -0
  74. package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
  75. package/tests/jest/editorAdapter.test.ts +245 -0
  76. package/tests/jest/index.test.tsx +50 -0
  77. package/tests/jest/legacy-setup.ts +13 -0
  78. package/tests/jest/legacyJsdomEnvironment.js +49 -0
  79. package/tests/jest/models/Admin.test.ts +206 -0
  80. package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
  81. package/tests/jest/reducers/classifications.test.ts +161 -0
  82. package/tests/jest/reducers/collections.test.ts +96 -0
  83. package/tests/jest/reducers/complaints.test.ts +166 -0
  84. package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
  85. package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
  86. package/tests/jest/reducers/customListDetails.test.ts +120 -0
  87. package/tests/jest/reducers/customListEditor.test.ts +2823 -0
  88. package/tests/jest/reducers/managePatrons.test.ts +71 -0
  89. package/tests/jest/testUtils/formDataShim.test.ts +135 -0
  90. package/tests/jest/testUtils/formDataShim.ts +112 -0
  91. package/tests/jest/utils/sharedFunctions.test.ts +121 -0
  92. package/tsconfig.json +3 -4
  93. package/testReporter.js +0 -29
  94. 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={{ services: [{ id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] }] }}
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("nypl");
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
+ });