@thepalaceproject/circulation-admin 1.43.0-post.2 → 1.43.0-post.20

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 (132) hide show
  1. package/CLAUDE.md +26 -12
  2. package/README.md +11 -5
  3. package/dist/circulation-admin.js +1 -1
  4. package/jest.config.js +17 -18
  5. package/package.json +4 -22
  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/AdvancedSearchBuilder.test.tsx +93 -20
  10. package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
  11. package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
  12. package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
  13. package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
  14. package/tests/jest/components/Announcement.test.tsx +76 -0
  15. package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
  16. package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
  17. package/tests/jest/components/Autocomplete.test.tsx +67 -0
  18. package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
  19. package/tests/jest/components/BookDetails.test.tsx +133 -0
  20. package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
  21. package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
  22. package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
  23. package/tests/jest/components/BookEditForm.test.tsx +352 -0
  24. package/tests/jest/components/BookEditor.test.tsx +20 -21
  25. package/tests/jest/components/CatalogPage.test.tsx +158 -0
  26. package/tests/jest/components/CatalogServices.test.tsx +100 -0
  27. package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
  28. package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
  29. package/tests/jest/components/Classifications.test.tsx +166 -0
  30. package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
  31. package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
  32. package/tests/jest/components/Collections.test.tsx +335 -7
  33. package/tests/jest/components/ColorPicker.test.tsx +58 -0
  34. package/tests/jest/components/ComplaintForm.test.tsx +171 -0
  35. package/tests/jest/components/Complaints.test.tsx +259 -0
  36. package/tests/jest/components/ConfigPage.test.tsx +58 -0
  37. package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
  38. package/tests/jest/components/ContextProvider.test.tsx +138 -0
  39. package/tests/jest/components/Contributors.test.tsx +157 -0
  40. package/tests/jest/components/CustomListEditor.test.tsx +367 -0
  41. package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
  42. package/tests/jest/components/CustomListPage.test.tsx +57 -0
  43. package/tests/jest/components/CustomListSearch.test.tsx +275 -0
  44. package/tests/jest/components/CustomLists.test.tsx +621 -4
  45. package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
  46. package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
  47. package/tests/jest/components/DashboardPage.test.tsx +76 -0
  48. package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
  49. package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
  50. package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
  51. package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
  52. package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
  53. package/tests/jest/components/EditableConfigList.test.tsx +606 -9
  54. package/tests/jest/components/EditableInput.test.tsx +250 -1
  55. package/tests/jest/components/EditorField.test.tsx +95 -0
  56. package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
  57. package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
  58. package/tests/jest/components/ErrorMessage.test.tsx +97 -0
  59. package/tests/jest/components/Footer.test.tsx +141 -0
  60. package/tests/jest/components/GenreForm.test.tsx +152 -0
  61. package/tests/jest/components/Header.test.tsx +421 -0
  62. package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
  63. package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
  64. package/tests/jest/components/InputList.test.tsx +493 -0
  65. package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
  66. package/tests/jest/components/Lane.test.tsx +132 -10
  67. package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
  68. package/tests/jest/components/LaneEditor.test.tsx +377 -13
  69. package/tests/jest/components/LanePage.test.tsx +68 -0
  70. package/tests/jest/components/Lanes.test.tsx +446 -0
  71. package/tests/jest/components/LanesSidebar.test.tsx +621 -0
  72. package/tests/jest/components/LanguageField.test.tsx +40 -0
  73. package/tests/jest/components/Libraries.test.tsx +48 -0
  74. package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
  75. package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
  76. package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
  77. package/tests/jest/components/LoadButton.test.tsx +49 -0
  78. package/tests/jest/components/ManagePatrons.test.tsx +73 -0
  79. package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
  80. package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
  81. package/tests/jest/components/MetadataServices.test.tsx +95 -0
  82. package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
  83. package/tests/jest/components/PairedMenus.test.tsx +220 -0
  84. package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
  85. package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
  86. package/tests/jest/components/PatronInfo.test.tsx +54 -0
  87. package/tests/jest/components/ProtocolFormField.test.tsx +443 -7
  88. package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
  89. package/tests/jest/components/SaveButton.test.tsx +47 -0
  90. package/tests/jest/components/SelfTestResult.test.tsx +164 -0
  91. package/tests/jest/components/SelfTests.test.tsx +601 -0
  92. package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
  93. package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
  94. package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
  95. package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
  96. package/tests/jest/components/SetupPage.test.tsx +41 -0
  97. package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
  98. package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
  99. package/tests/jest/components/Stats.test.tsx +4 -15
  100. package/tests/jest/components/TabContainer.test.tsx +68 -0
  101. package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
  102. package/tests/jest/components/Timestamp.test.tsx +74 -0
  103. package/tests/jest/components/ToolTip.test.tsx +46 -0
  104. package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
  105. package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
  106. package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
  107. package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
  108. package/tests/jest/components/WelcomePage.test.tsx +43 -0
  109. package/tests/jest/components/WithEditButton.test.tsx +55 -0
  110. package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
  111. package/tests/jest/editorAdapter.test.ts +245 -0
  112. package/tests/jest/features/book.test.ts +75 -80
  113. package/tests/jest/fixtures/classificationsData.ts +45 -0
  114. package/tests/jest/fixtures/genreData.ts +811 -0
  115. package/tests/jest/index.test.tsx +50 -0
  116. package/tests/jest/models/Admin.test.ts +206 -0
  117. package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
  118. package/tests/jest/reducers/classifications.test.ts +161 -0
  119. package/tests/jest/reducers/collections.test.ts +96 -0
  120. package/tests/jest/reducers/complaints.test.ts +166 -0
  121. package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
  122. package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
  123. package/tests/jest/reducers/customListDetails.test.ts +120 -0
  124. package/tests/jest/reducers/customListEditor.test.ts +2823 -0
  125. package/tests/jest/reducers/managePatrons.test.ts +71 -0
  126. package/tests/jest/testUtils/formDataShim.test.ts +135 -0
  127. package/tests/jest/testUtils/formDataShim.ts +112 -0
  128. package/tests/jest/utils/sharedFunctions.test.ts +121 -0
  129. package/tsconfig.json +3 -4
  130. package/.nycrc.json +0 -8
  131. package/testReporter.js +0 -29
  132. package/tests/jest/README.md +0 -6
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
- import { render, screen } from "@testing-library/react";
2
+ import { render, screen, act, fireEvent } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
3
4
  import EditableInput from "../../../src/components/EditableInput";
4
5
 
5
6
  describe("EditableInput", () => {
@@ -62,3 +63,251 @@ describe("EditableInput", () => {
62
63
  expect(textboxes[5]).toHaveAccessibleDescription("");
63
64
  });
64
65
  });
66
+
67
+ // Value/change handling, the prop-sync in componentWillReceiveProps, and the
68
+ // imperative ref API (getValue/getChecked/setValue/clear) that parents like
69
+ // ProtocolFormField use.
70
+ describe("EditableInput - value, change, and imperative API", () => {
71
+ it("updates as the user types and reports the value via getValue()", async () => {
72
+ const user = userEvent.setup();
73
+ const onChange = jest.fn();
74
+ const ref = React.createRef<EditableInput>();
75
+ render(
76
+ <EditableInput ref={ref} label="Field" value="" onChange={onChange} />
77
+ );
78
+
79
+ const input = screen.getByRole("textbox", { name: "Field" });
80
+ await user.type(input, "hello");
81
+
82
+ expect(input).toHaveValue("hello");
83
+ expect(ref.current?.getValue()).toBe("hello");
84
+ expect(onChange).toHaveBeenCalled();
85
+ });
86
+
87
+ it("toggles a checkbox and reports it via getChecked()", async () => {
88
+ const user = userEvent.setup();
89
+ const ref = React.createRef<EditableInput>();
90
+ render(
91
+ <EditableInput ref={ref} type="checkbox" label="Check" checked={false} />
92
+ );
93
+
94
+ const box = screen.getByRole("checkbox", { name: "Check" });
95
+ await user.click(box);
96
+
97
+ expect(box).toBeChecked();
98
+ expect(ref.current?.getChecked()).toBe(true);
99
+ });
100
+
101
+ it("does not fire onChange when readOnly", () => {
102
+ const onChange = jest.fn();
103
+ render(<EditableInput label="RO" value="x" readOnly onChange={onChange} />);
104
+
105
+ // `userEvent.type` honors the DOM's read-only semantics and never dispatches
106
+ // a change event, so it would never reach handleChange. Dispatch directly, so
107
+ // the component's own readOnly guard is what stops onChange.
108
+ fireEvent.change(screen.getByRole("textbox", { name: "RO" }), {
109
+ target: { value: "y" },
110
+ });
111
+
112
+ expect(onChange).not.toHaveBeenCalled();
113
+ });
114
+
115
+ it("syncs its value when the value prop changes", () => {
116
+ const { rerender } = render(<EditableInput label="F" value="a" />);
117
+ const input = screen.getByRole("textbox", { name: "F" });
118
+ expect(input).toHaveValue("a");
119
+
120
+ rerender(<EditableInput label="F" value="b" />);
121
+ expect(input).toHaveValue("b");
122
+ });
123
+
124
+ it("syncs its checked state when the checked prop changes", () => {
125
+ const { rerender } = render(
126
+ <EditableInput type="checkbox" label="C" checked={false} />
127
+ );
128
+ const box = screen.getByRole("checkbox", { name: "C" });
129
+ expect(box).not.toBeChecked();
130
+
131
+ rerender(<EditableInput type="checkbox" label="C" checked={true} />);
132
+ expect(box).toBeChecked();
133
+ });
134
+
135
+ it("setValue and clear update the value imperatively", () => {
136
+ const ref = React.createRef<EditableInput>();
137
+ render(<EditableInput ref={ref} label="F" value="a" />);
138
+ const input = screen.getByRole("textbox", { name: "F" });
139
+
140
+ act(() => ref.current?.setValue("z"));
141
+ expect(input).toHaveValue("z");
142
+
143
+ act(() => ref.current?.clear());
144
+ expect(input).toHaveValue("");
145
+ });
146
+
147
+ it("clear also resets a checked box", () => {
148
+ const ref = React.createRef<EditableInput>();
149
+ render(
150
+ <EditableInput ref={ref} type="checkbox" label="C" checked={true} />
151
+ );
152
+ const box = screen.getByRole("checkbox", { name: "C" });
153
+ expect(box).toBeChecked();
154
+
155
+ act(() => ref.current?.clear());
156
+ expect(box).not.toBeChecked();
157
+ });
158
+
159
+ // A falsy-but-valid value must survive: this is what defaultValueIfMissing
160
+ // guards, and what a naive `props.value || ""` would silently break.
161
+ it("keeps a zero value rather than falling back to the empty default", () => {
162
+ const { rerender } = render(<EditableInput label="F" value={0} />);
163
+ const input = screen.getByRole("textbox", { name: "F" });
164
+ expect(input).toHaveValue("0");
165
+
166
+ rerender(<EditableInput label="F" value="a" />);
167
+ expect(input).toHaveValue("a");
168
+
169
+ rerender(<EditableInput label="F" value={0} />);
170
+ expect(input).toHaveValue("0");
171
+ });
172
+ });
173
+
174
+ describe("EditableInput - labels, children, and pass-through props", () => {
175
+ it("labels a radio input and toggles it", async () => {
176
+ const user = userEvent.setup();
177
+ const ref = React.createRef<EditableInput>();
178
+ render(
179
+ <EditableInput
180
+ ref={ref}
181
+ type="radio"
182
+ label="Radio label"
183
+ name="r"
184
+ checked={false}
185
+ />
186
+ );
187
+
188
+ const radio = screen.getByRole("radio", { name: "Radio label" });
189
+ expect(radio).not.toBeChecked();
190
+
191
+ await user.click(radio);
192
+
193
+ expect(radio).toBeChecked();
194
+ expect(ref.current?.getChecked()).toBe(true);
195
+ });
196
+
197
+ it("renders children into the element", () => {
198
+ render(
199
+ <EditableInput elementType="select" label="S" name="s" value="b">
200
+ <option value="a">option a</option>
201
+ <option value="b">option b</option>
202
+ </EditableInput>
203
+ );
204
+
205
+ const select = screen.getByRole("combobox", { name: "S" });
206
+ expect(select).toHaveValue("b");
207
+ expect(
208
+ Array.from(select.querySelectorAll("option")).map((o) => o.textContent)
209
+ ).toEqual(["option a", "option b"]);
210
+ });
211
+
212
+ it("forwards the disabled prop to the element", () => {
213
+ render(<EditableInput label="F" name="name" disabled={true} value="v" />);
214
+ expect(screen.getByRole("textbox", { name: "F" })).toBeDisabled();
215
+ });
216
+
217
+ // renderDescription uses dangerouslySetInnerHTML, and callers pass markup.
218
+ it("renders the description as HTML", () => {
219
+ const { container } = render(
220
+ <EditableInput
221
+ label="F"
222
+ optionalText={false}
223
+ description="<p>description</p>"
224
+ />
225
+ );
226
+
227
+ const description = container.querySelector(".description");
228
+ expect(description.innerHTML).toContain("<p>description</p>");
229
+ });
230
+
231
+ it("shows extra content in an add-on wrapper only when supplied", () => {
232
+ const { container, rerender } = render(
233
+ <EditableInput label="F" name="name" value="v" />
234
+ );
235
+ expect(container.querySelector(".with-add-on")).toBeNull();
236
+
237
+ rerender(
238
+ <EditableInput
239
+ label="F"
240
+ name="name"
241
+ value="v"
242
+ extraContent={<span>Extra content!</span>}
243
+ />
244
+ );
245
+
246
+ const extra = container.querySelector(".with-add-on");
247
+ expect(extra).not.toBeNull();
248
+ expect(extra).toHaveTextContent("Extra content!");
249
+ });
250
+ });
251
+
252
+ // The error styling is a compound predicate:
253
+ // clientError || (error && error.status >= 400 && !value && required)
254
+ describe("EditableInput - error styling", () => {
255
+ const formGroup = (container: HTMLElement) =>
256
+ container.querySelector(".form-group");
257
+
258
+ it("applies no error class by default", () => {
259
+ const { container } = render(
260
+ <EditableInput label="F" name="name" value="initial value" />
261
+ );
262
+ expect(formGroup(container)).not.toHaveClass("field-error");
263
+ });
264
+
265
+ it("applies the error class on a client error", () => {
266
+ const { container } = render(
267
+ <EditableInput
268
+ label="F"
269
+ name="name"
270
+ value="initial value"
271
+ clientError={true}
272
+ />
273
+ );
274
+ expect(formGroup(container)).toHaveClass("field-error");
275
+ });
276
+
277
+ it("applies the error class for a blank required field after a server error", () => {
278
+ const { container } = render(
279
+ <EditableInput
280
+ label="F"
281
+ name="name"
282
+ error={{ status: 400, response: "", url: "" }}
283
+ required={true}
284
+ />
285
+ );
286
+ expect(formGroup(container)).toHaveClass("field-error");
287
+ });
288
+
289
+ it("does not apply the error class when the required field has a value", () => {
290
+ const { container } = render(
291
+ <EditableInput
292
+ label="F"
293
+ name="name"
294
+ value="a value"
295
+ error={{ status: 400, response: "", url: "" }}
296
+ required={true}
297
+ />
298
+ );
299
+ expect(formGroup(container)).not.toHaveClass("field-error");
300
+ });
301
+
302
+ it("does not apply the error class for a server error on an optional field", () => {
303
+ const { container } = render(
304
+ <EditableInput
305
+ label="F"
306
+ name="name"
307
+ error={{ status: 400, response: "", url: "" }}
308
+ required={false}
309
+ />
310
+ );
311
+ expect(formGroup(container)).not.toHaveClass("field-error");
312
+ });
313
+ });
@@ -0,0 +1,95 @@
1
+ import * as React from "react";
2
+ import { render, fireEvent } from "@testing-library/react";
3
+ import { RichUtils } from "draft-js";
4
+
5
+ import EditorField from "../../../src/components/EditorField";
6
+
7
+ describe("EditorField", () => {
8
+ afterEach(() => {
9
+ jest.restoreAllMocks();
10
+ });
11
+
12
+ it("renders the three inline-style buttons and toggles the matching style on mouse-down", () => {
13
+ // There is no DOM-observable effect of toggling an inline style over a
14
+ // collapsed (unfocused) selection, so we drive the real mouse-down interaction
15
+ // and observe the actual `RichUtils.toggleInlineStyle` call the button triggers.
16
+ const toggleSpy = jest.spyOn(RichUtils, "toggleInlineStyle");
17
+
18
+ const { container } = render(
19
+ <EditorField content="This is a summary." disabled={false} />
20
+ );
21
+
22
+ const buttons = Array.from(container.querySelectorAll("button"));
23
+ expect(buttons).toHaveLength(3);
24
+ const [bold, italic, underline] = buttons;
25
+
26
+ expect(bold).toHaveTextContent("Bold");
27
+ expect(bold.querySelector("b")).toBeInTheDocument();
28
+ expect(italic).toHaveTextContent("Italic");
29
+ expect(italic.querySelector("i")).toBeInTheDocument();
30
+ expect(underline).toHaveTextContent("Underline");
31
+ expect(underline.querySelector("u")).toBeInTheDocument();
32
+
33
+ expect(toggleSpy).not.toHaveBeenCalled();
34
+
35
+ fireEvent.mouseDown(bold);
36
+ expect(toggleSpy).toHaveBeenCalledTimes(1);
37
+ expect(toggleSpy.mock.calls[0][1]).toBe("BOLD");
38
+
39
+ fireEvent.mouseDown(italic);
40
+ expect(toggleSpy).toHaveBeenCalledTimes(2);
41
+ expect(toggleSpy.mock.calls[1][1]).toBe("ITALIC");
42
+
43
+ fireEvent.mouseDown(underline);
44
+ expect(toggleSpy).toHaveBeenCalledTimes(3);
45
+ expect(toggleSpy.mock.calls[2][1]).toBe("UNDERLINE");
46
+ });
47
+
48
+ it("exposes the current content as HTML through getValue()", () => {
49
+ // `getValue()` is EditorField's real imperative API (BookEditForm reads it via a
50
+ // ref), so mirror that: render with a ref and call it.
51
+ const ref = React.createRef<EditorField>();
52
+ render(
53
+ <EditorField ref={ref} content="This is a summary." disabled={false} />
54
+ );
55
+ expect(ref.current?.getValue()).toBe("<p>This is a summary.</p>");
56
+ });
57
+
58
+ it("falls back to the default content when the content is empty", () => {
59
+ const expected = "<p>Update the content for this field.</p>";
60
+
61
+ const emptyParagraphRef = React.createRef<EditorField>();
62
+ render(
63
+ <EditorField ref={emptyParagraphRef} content="<p></p>" disabled={false} />
64
+ );
65
+ expect(emptyParagraphRef.current?.getValue()).toBe(expected);
66
+
67
+ const emptyStringRef = React.createRef<EditorField>();
68
+ render(<EditorField ref={emptyStringRef} content="" disabled={false} />);
69
+ expect(emptyStringRef.current?.getValue()).toBe(expected);
70
+
71
+ const undefinedRef = React.createRef<EditorField>();
72
+ render(
73
+ <EditorField ref={undefinedRef} content={undefined} disabled={false} />
74
+ );
75
+ expect(undefinedRef.current?.getValue()).toBe(expected);
76
+ });
77
+
78
+ it("disables the toolbar buttons and the editor when disabled", () => {
79
+ const { container, rerender } = render(
80
+ <EditorField content="This is a summary." disabled={false} />
81
+ );
82
+
83
+ const buttons = () => Array.from(container.querySelectorAll("button"));
84
+ const editable = () => container.querySelector("[contenteditable]");
85
+
86
+ expect(buttons()).toHaveLength(3);
87
+ buttons().forEach((button) => expect(button).not.toBeDisabled());
88
+ expect(editable()).toHaveAttribute("contenteditable", "true");
89
+
90
+ rerender(<EditorField content="This is a summary." disabled={true} />);
91
+
92
+ buttons().forEach((button) => expect(button).toBeDisabled());
93
+ expect(editable()).toHaveAttribute("contenteditable", "false");
94
+ });
95
+ });
@@ -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
+ });