@thepalaceproject/circulation-admin 1.43.0-post.9 → 1.44.0

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 (73) hide show
  1. package/CLAUDE.md +22 -10
  2. package/README.md +11 -5
  3. package/dist/circulation-admin.js +1 -1
  4. package/jest.config.js +11 -44
  5. package/package.json +1 -5
  6. package/tests/jest/components/AdvancedSearchBuilder.test.tsx +111 -0
  7. package/tests/jest/components/Announcement.test.tsx +76 -0
  8. package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
  9. package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
  10. package/tests/jest/components/Autocomplete.test.tsx +67 -0
  11. package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
  12. package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
  13. package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
  14. package/tests/jest/components/BookEditForm.test.tsx +352 -0
  15. package/tests/jest/components/BookEditor.test.tsx +20 -21
  16. package/tests/jest/components/CatalogPage.test.tsx +158 -0
  17. package/tests/jest/components/CatalogServices.test.tsx +38 -1
  18. package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
  19. package/tests/jest/components/Classifications.test.tsx +166 -0
  20. package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
  21. package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
  22. package/tests/jest/components/Collections.test.tsx +335 -7
  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/ConfigTabContainer.test.tsx +196 -0
  27. package/tests/jest/components/ContextProvider.test.tsx +138 -0
  28. package/tests/jest/components/Contributors.test.tsx +157 -0
  29. package/tests/jest/components/CustomListEditor.test.tsx +367 -0
  30. package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
  31. package/tests/jest/components/CustomLists.test.tsx +621 -4
  32. package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
  33. package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
  34. package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
  35. package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
  36. package/tests/jest/components/EditableConfigList.test.tsx +598 -7
  37. package/tests/jest/components/EditableInput.test.tsx +250 -1
  38. package/tests/jest/components/EditorField.test.tsx +95 -0
  39. package/tests/jest/components/GenreForm.test.tsx +152 -0
  40. package/tests/jest/components/Header.test.tsx +421 -0
  41. package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
  42. package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
  43. package/tests/jest/components/InputList.test.tsx +493 -0
  44. package/tests/jest/components/Lane.test.tsx +132 -10
  45. package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
  46. package/tests/jest/components/LaneEditor.test.tsx +377 -9
  47. package/tests/jest/components/Lanes.test.tsx +446 -0
  48. package/tests/jest/components/LanesSidebar.test.tsx +621 -0
  49. package/tests/jest/components/Libraries.test.tsx +48 -0
  50. package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
  51. package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
  52. package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
  53. package/tests/jest/components/MetadataServices.test.tsx +37 -1
  54. package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
  55. package/tests/jest/components/PairedMenus.test.tsx +220 -0
  56. package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
  57. package/tests/jest/components/ProtocolFormField.test.tsx +443 -7
  58. package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
  59. package/tests/jest/components/SelfTests.test.tsx +601 -0
  60. package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
  61. package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
  62. package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
  63. package/tests/jest/components/Stats.test.tsx +0 -7
  64. package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
  65. package/tests/jest/components/ToolTip.test.tsx +46 -0
  66. package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
  67. package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
  68. package/tests/jest/features/book.test.ts +75 -80
  69. package/tests/jest/fixtures/classificationsData.ts +45 -0
  70. package/tests/jest/fixtures/genreData.ts +811 -0
  71. package/tests/jest/README.md +0 -6
  72. package/tests/jest/legacy-setup.ts +0 -13
  73. package/tests/jest/legacyJsdomEnvironment.js +0 -49
@@ -0,0 +1,134 @@
1
+ import * as React from "react";
2
+ import { installFormDataShim, FormDataShim } from "../testUtils/formDataShim";
3
+ import { screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+
6
+ import { renderWithProviders } from "../testUtils/withProviders";
7
+ import buildStore from "../../../src/store";
8
+ // Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
9
+ // are exercised. The panel fetches its announcements on mount; the fetched data
10
+ // flows back in through the Redux store.
11
+ import SitewideAnnouncements from "../../../src/components/SitewideAnnouncements";
12
+
13
+ const data = {
14
+ announcements: [
15
+ {
16
+ id: "dcf036d9-106b-426c-838f-ce7c57d53801",
17
+ content: "There is nothing to see here.",
18
+ start: "2022-08-31",
19
+ finish: "2022-10-31",
20
+ },
21
+ ],
22
+ settings: [
23
+ {
24
+ category: "Announcements",
25
+ description: "Announcements will be displayed to authenticated patrons.",
26
+ key: "global_announcements",
27
+ label: "Scheduled announcements",
28
+ level: 3,
29
+ type: "announcements",
30
+ },
31
+ ],
32
+ };
33
+ installFormDataShim();
34
+
35
+ const jsonResponse = (body: unknown) =>
36
+ new Response(JSON.stringify(body), {
37
+ status: 200,
38
+ headers: { "Content-Type": "application/json" },
39
+ });
40
+
41
+ const renderConnected = (
42
+ element: React.ReactElement,
43
+ appConfigSettings: Record<string, unknown> = {}
44
+ ) =>
45
+ renderWithProviders(element, {
46
+ reduxProviderProps: { store: buildStore() },
47
+ appConfigSettings,
48
+ });
49
+
50
+ describe("SitewideAnnouncements", () => {
51
+ afterEach(() => {
52
+ jest.restoreAllMocks();
53
+ });
54
+
55
+ it("renders a form containing the announcements section", async () => {
56
+ jest
57
+ .spyOn(globalThis, "fetch")
58
+ .mockImplementation(async () => jsonResponse(data));
59
+ const { container } = renderConnected(
60
+ <SitewideAnnouncements csrfToken="token" />
61
+ );
62
+
63
+ // Once the on-mount fetch resolves, the form renders with the announcement
64
+ // supplied through the store (proving the AnnouncementsSection received the
65
+ // data as its value).
66
+ expect(
67
+ await screen.findByText("There is nothing to see here.")
68
+ ).toBeInTheDocument();
69
+ expect(screen.getByText("Scheduled Announcements:")).toBeInTheDocument();
70
+ expect(container.querySelector("form")).toBeInTheDocument();
71
+ });
72
+
73
+ it("renders a loading indicator instead of a form while data is loading", () => {
74
+ // A never-resolving fetch keeps the panel in its fetching state.
75
+ jest
76
+ .spyOn(globalThis, "fetch")
77
+ .mockImplementation(() => new Promise(() => {}));
78
+ const { container } = renderConnected(
79
+ <SitewideAnnouncements csrfToken="token" />
80
+ );
81
+
82
+ expect(container.querySelector(".loading")).toBeInTheDocument();
83
+ expect(container.querySelector("form")).not.toBeInTheDocument();
84
+ });
85
+
86
+ it("submits the announcements to editItem when the form is submitted", async () => {
87
+ const user = userEvent.setup();
88
+ const fetchSpy = jest
89
+ .spyOn(globalThis, "fetch")
90
+ .mockImplementation(async (_url, opts?: RequestInit) => {
91
+ if (opts?.method === "POST") {
92
+ return new Response("success", { status: 200 });
93
+ }
94
+ return jsonResponse(data);
95
+ });
96
+
97
+ // System admin so the settings' required permission level is satisfied and
98
+ // the submit button is enabled.
99
+ renderConnected(<SitewideAnnouncements csrfToken="token" />, {
100
+ roles: [{ role: "system" }],
101
+ });
102
+
103
+ await screen.findByText("There is nothing to see here.");
104
+
105
+ await user.click(screen.getByRole("button", { name: "Submit" }));
106
+
107
+ // The submit dispatches editSitewideAnnouncements, which POSTs the form.
108
+ await waitFor(() => {
109
+ const postCall = fetchSpy.mock.calls.find(
110
+ ([, opts]) => (opts as RequestInit)?.method === "POST"
111
+ );
112
+ expect(postCall).toBeDefined();
113
+ });
114
+
115
+ const postCall = fetchSpy.mock.calls.find(
116
+ ([, opts]) => (opts as RequestInit)?.method === "POST"
117
+ );
118
+ const body = (postCall[1] as RequestInit).body as unknown as FormDataShim;
119
+ expect(JSON.parse(body.get("announcements"))).toStrictEqual(
120
+ data.announcements
121
+ );
122
+ });
123
+
124
+ it("disables the submit button when the admin lacks the required permission level", async () => {
125
+ jest
126
+ .spyOn(globalThis, "fetch")
127
+ .mockImplementation(async () => jsonResponse(data));
128
+ // No roles => admin level 1, below the settings' required level of 3.
129
+ renderConnected(<SitewideAnnouncements csrfToken="token" />);
130
+
131
+ await screen.findByText("There is nothing to see here.");
132
+ expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled();
133
+ });
134
+ });
@@ -28,13 +28,6 @@ const normalizedData = normalizeStatistics(statisticsApiResponseData);
28
28
  global.ResizeObserver = require("resize-observer-polyfill");
29
29
 
30
30
  describe("Dashboard Statistics", () => {
31
- // NB: This adds test to the already existing tests in:
32
- // - `src/components/__tests__/LibraryStats-test.tsx`.
33
- // - `src/components/__tests__/SingleStatListItem-test.tsx`.
34
- //
35
- // Those tests should eventually be migrated here and
36
- // adapted to the Jest/React Testing Library paradigm.
37
-
38
31
  // Configure standard constructors so that RTK Query works in tests with FetchMockJest
39
32
  Object.assign(fetchMock.config, {
40
33
  fetch,
@@ -0,0 +1,218 @@
1
+ import * as React from "react";
2
+ import { render, screen, act } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import TextWithEditMode from "../../../src/components/TextWithEditMode";
5
+
6
+ describe("TextWithEditMode", () => {
7
+ it("renders text", () => {
8
+ const { rerender } = render(
9
+ <TextWithEditMode
10
+ text="test"
11
+ placeholder="editable thing"
12
+ aria-label="label"
13
+ />
14
+ );
15
+ expect(screen.getByText("test")).toBeInTheDocument();
16
+ expect(
17
+ screen.getByRole("button", { name: /Edit editable thing/ })
18
+ ).toBeInTheDocument();
19
+
20
+ rerender(
21
+ <TextWithEditMode
22
+ text="other text"
23
+ placeholder="editable thing"
24
+ aria-label="label"
25
+ />
26
+ );
27
+ expect(screen.queryByText("test")).not.toBeInTheDocument();
28
+ expect(screen.getByText("other text")).toBeInTheDocument();
29
+ });
30
+
31
+ it("starts in edit mode if there's no text", () => {
32
+ render(
33
+ <TextWithEditMode placeholder="editable thing" aria-label="label" />
34
+ );
35
+ const input = screen.getByRole("textbox");
36
+ expect(input).toHaveAttribute("placeholder", "editable thing");
37
+ expect(input).toHaveValue("");
38
+ expect(
39
+ screen.getByRole("button", { name: /Save editable thing/ })
40
+ ).toBeInTheDocument();
41
+ });
42
+
43
+ it("switches to edit mode", async () => {
44
+ const user = userEvent.setup();
45
+ render(
46
+ <TextWithEditMode
47
+ text="test"
48
+ placeholder="editable thing"
49
+ aria-label="label"
50
+ />
51
+ );
52
+ expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
53
+
54
+ await user.click(
55
+ screen.getByRole("button", { name: /Edit editable thing/ })
56
+ );
57
+
58
+ const input = screen.getByRole("textbox");
59
+ expect(input).toHaveAttribute("placeholder", "editable thing");
60
+ expect(input).toHaveValue("test");
61
+ expect(screen.getByRole("button", { name: /Save/ })).toBeInTheDocument();
62
+ expect(
63
+ screen.queryByRole("button", { name: /Edit/ })
64
+ ).not.toBeInTheDocument();
65
+ });
66
+
67
+ it("switches out of edit mode", async () => {
68
+ const user = userEvent.setup();
69
+ const onUpdate = jest.fn();
70
+ render(
71
+ <TextWithEditMode
72
+ placeholder="editable thing"
73
+ onUpdate={onUpdate}
74
+ aria-label="label"
75
+ />
76
+ );
77
+ // Type the new value into the field; the component reads the real input
78
+ // value back through its ref on save (no prototype stubbing).
79
+ await user.type(screen.getByRole("textbox"), "new value");
80
+ await user.click(
81
+ screen.getByRole("button", { name: /Save editable thing/ })
82
+ );
83
+
84
+ expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
85
+ expect(onUpdate).toHaveBeenCalledTimes(1);
86
+ expect(onUpdate).toHaveBeenCalledWith("new value");
87
+ expect(screen.getByText("new value")).toBeInTheDocument();
88
+ expect(screen.getByRole("button", { name: /Edit/ })).toBeInTheDocument();
89
+ expect(
90
+ screen.queryByRole("button", { name: /Save/ })
91
+ ).not.toBeInTheDocument();
92
+ });
93
+
94
+ it("gets text via the ref API", async () => {
95
+ const user = userEvent.setup();
96
+ // In display mode, getText() returns the current text.
97
+ const displayRef = React.createRef<TextWithEditMode>();
98
+ const { unmount } = render(
99
+ <TextWithEditMode
100
+ text="test"
101
+ placeholder="editable thing"
102
+ aria-label="label"
103
+ ref={displayRef}
104
+ />
105
+ );
106
+ expect(displayRef.current!.getText()).toBe("test");
107
+ unmount();
108
+
109
+ // From edit mode, getText() returns the current input value and exits edit mode.
110
+ const editRef = React.createRef<TextWithEditMode>();
111
+ render(
112
+ <TextWithEditMode
113
+ placeholder="editable thing"
114
+ aria-label="label"
115
+ ref={editRef}
116
+ />
117
+ );
118
+ await user.type(screen.getByRole("textbox"), "new value");
119
+ let value = "";
120
+ act(() => {
121
+ value = editRef.current!.getText();
122
+ });
123
+ expect(value).toBe("new value");
124
+ expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
125
+ });
126
+
127
+ it("resets via the ref API", async () => {
128
+ const user = userEvent.setup();
129
+ const onUpdate = jest.fn();
130
+
131
+ // With no text prop, reset() returns to a blank edit mode.
132
+ const blankRef = React.createRef<TextWithEditMode>();
133
+ const { unmount } = render(
134
+ <TextWithEditMode
135
+ placeholder="editable thing"
136
+ onUpdate={onUpdate}
137
+ aria-label="label"
138
+ ref={blankRef}
139
+ />
140
+ );
141
+ await user.type(screen.getByRole("textbox"), "new value");
142
+ await user.click(
143
+ screen.getByRole("button", { name: /Save editable thing/ })
144
+ );
145
+ expect(screen.getByText("new value")).toBeInTheDocument();
146
+ expect(onUpdate).toHaveBeenCalledTimes(1);
147
+
148
+ act(() => {
149
+ blankRef.current!.reset();
150
+ });
151
+ expect(screen.queryByText("new value")).not.toBeInTheDocument();
152
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
153
+ expect(onUpdate).toHaveBeenCalledTimes(2);
154
+ expect(onUpdate.mock.calls[1][0]).toBeUndefined();
155
+ unmount();
156
+
157
+ // With a text prop, reset() returns to displaying that text.
158
+ const textRef = React.createRef<TextWithEditMode>();
159
+ render(
160
+ <TextWithEditMode
161
+ text="test"
162
+ placeholder="editable thing"
163
+ onUpdate={onUpdate}
164
+ aria-label="label"
165
+ ref={textRef}
166
+ />
167
+ );
168
+ await user.click(
169
+ screen.getByRole("button", { name: /Edit editable thing/ })
170
+ );
171
+ const input = screen.getByRole("textbox");
172
+ await user.clear(input);
173
+ await user.type(input, "new value");
174
+ await user.click(
175
+ screen.getByRole("button", { name: /Save editable thing/ })
176
+ );
177
+ expect(screen.queryByText("test")).not.toBeInTheDocument();
178
+ expect(screen.getByText("new value")).toBeInTheDocument();
179
+ expect(onUpdate).toHaveBeenCalledTimes(3);
180
+
181
+ act(() => {
182
+ textRef.current!.reset();
183
+ });
184
+ expect(screen.queryByText("new value")).not.toBeInTheDocument();
185
+ expect(screen.getByText("test")).toBeInTheDocument();
186
+ expect(onUpdate).toHaveBeenCalledTimes(4);
187
+ expect(onUpdate.mock.calls[3][0]).toBe("test");
188
+ });
189
+
190
+ it("optionally disables the save button if the text is blank", async () => {
191
+ const user = userEvent.setup();
192
+ render(
193
+ <TextWithEditMode
194
+ placeholder="editable thing"
195
+ aria-label="label"
196
+ disableIfBlank
197
+ />
198
+ );
199
+ const saveButton = screen.getByRole("button", {
200
+ name: /Save editable thing/,
201
+ });
202
+ expect(saveButton).toBeDisabled();
203
+
204
+ await user.type(screen.getByRole("textbox"), "test");
205
+ expect(saveButton).toBeEnabled();
206
+ });
207
+
208
+ it("updates the displayed value as the user types", async () => {
209
+ const user = userEvent.setup();
210
+ render(
211
+ <TextWithEditMode placeholder="editable thing" aria-label="label" />
212
+ );
213
+ const input = screen.getByRole("textbox");
214
+ expect(input).toHaveValue("");
215
+ await user.type(input, "test");
216
+ expect(input).toHaveValue("test");
217
+ });
218
+ });
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import ToolTip from "../../../src/components/ToolTip";
5
+
6
+ describe("ToolTip", () => {
7
+ const renderToolTip = () =>
8
+ render(<ToolTip trigger={<h1>Hover</h1>} text="ToolTip Content" />);
9
+
10
+ it("renders the trigger element", () => {
11
+ renderToolTip();
12
+ expect(screen.getByRole("heading", { name: "Hover" })).toBeInTheDocument();
13
+ });
14
+
15
+ it("renders the tooltip", () => {
16
+ const { container } = renderToolTip();
17
+ const tooltip = container.querySelector<HTMLElement>(".tool-tip")!;
18
+ expect(tooltip).toBeInTheDocument();
19
+ expect(tooltip).toHaveTextContent("ToolTip Content");
20
+ expect(tooltip.innerHTML).toContain("ToolTip Content");
21
+ });
22
+
23
+ it("initially hides the tooltip", () => {
24
+ const { container } = renderToolTip();
25
+ expect(container.querySelector(".tool-tip")).toHaveClass("hide");
26
+ });
27
+
28
+ it("shows the tooltip on mouseEnter", async () => {
29
+ const user = userEvent.setup();
30
+ const { container } = renderToolTip();
31
+ await user.hover(screen.getByRole("tooltip"));
32
+ expect(container.querySelector(".tool-tip")).not.toHaveClass("hide");
33
+ });
34
+
35
+ it("hides the tooltip on mouseLeave", async () => {
36
+ const user = userEvent.setup();
37
+ const { container } = renderToolTip();
38
+ const toolTipContainer = screen.getByRole("tooltip");
39
+
40
+ await user.hover(toolTipContainer);
41
+ expect(container.querySelector(".tool-tip")).not.toHaveClass("hide");
42
+
43
+ await user.unhover(toolTipContainer);
44
+ expect(container.querySelector(".tool-tip")).toHaveClass("hide");
45
+ });
46
+ });
@@ -0,0 +1,69 @@
1
+ import * as React from "react";
2
+ import { screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { renderWithProviders } from "../testUtils/withProviders";
6
+
7
+ // The tab containers are connected components that fetch on mount and require
8
+ // react-router context; TroubleshootingCategoryPage only chooses between them
9
+ // and hands them a `tab` and `goToTab`. Mock them to markers that surface the
10
+ // `tab` they receive and let us drive `goToTab`.
11
+ jest.mock("../../../src/components/DiagnosticsTabContainer", () => ({
12
+ __esModule: true,
13
+ default: (props: any) => (
14
+ <div data-testid="diagnostics-tab-container" data-tab={props.tab}>
15
+ <button type="button" onClick={() => props.goToTab("monitor")}>
16
+ go-to-monitor
17
+ </button>
18
+ </div>
19
+ ),
20
+ }));
21
+ jest.mock("../../../src/components/SelfTestsTabContainer", () => ({
22
+ __esModule: true,
23
+ default: (props: any) => (
24
+ <div data-testid="self-tests-tab-container" data-tab={props.tab} />
25
+ ),
26
+ }));
27
+
28
+ import TroubleshootingCategoryPage from "../../../src/components/TroubleshootingCategoryPage";
29
+
30
+ describe("TroubleshootingCategoryPage", () => {
31
+ it("renders a tab container", () => {
32
+ renderWithProviders(<TroubleshootingCategoryPage type="diagnostics" />);
33
+
34
+ expect(screen.getByTestId("diagnostics-tab-container")).toBeInTheDocument();
35
+ expect(
36
+ screen.queryByTestId("self-tests-tab-container")
37
+ ).not.toBeInTheDocument();
38
+ });
39
+
40
+ it("switches tabs", async () => {
41
+ const user = userEvent.setup();
42
+ renderWithProviders(<TroubleshootingCategoryPage type="diagnostics" />);
43
+
44
+ expect(screen.getByTestId("diagnostics-tab-container")).toHaveAttribute(
45
+ "data-tab",
46
+ "coverage_provider"
47
+ );
48
+
49
+ await user.click(screen.getByRole("button", { name: "go-to-monitor" }));
50
+
51
+ expect(screen.getByTestId("diagnostics-tab-container")).toHaveAttribute(
52
+ "data-tab",
53
+ "monitor"
54
+ );
55
+ });
56
+
57
+ it("renders a different tab container", () => {
58
+ const { rerender } = renderWithProviders(
59
+ <TroubleshootingCategoryPage type="diagnostics" />
60
+ );
61
+
62
+ rerender(<TroubleshootingCategoryPage type="self-tests" />);
63
+
64
+ expect(
65
+ screen.queryByTestId("diagnostics-tab-container")
66
+ ).not.toBeInTheDocument();
67
+ expect(screen.getByTestId("self-tests-tab-container")).toBeInTheDocument();
68
+ });
69
+ });
@@ -0,0 +1,94 @@
1
+ import * as React from "react";
2
+ import { screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { renderWithProviders } from "../testUtils/withProviders";
6
+ import title from "../../../src/utils/title";
7
+
8
+ // Header needs react-router v3 context that renderWithProviders does not supply,
9
+ // and both Header and Footer are page chrome, so mock them to markers.
10
+ jest.mock("../../../src/components/Header", () => ({
11
+ __esModule: true,
12
+ default: () => <div data-testid="header" />,
13
+ }));
14
+ jest.mock("../../../src/components/Footer", () => ({
15
+ __esModule: true,
16
+ default: () => <div data-testid="footer" />,
17
+ }));
18
+
19
+ // The real TroubleshootingTabContainer requires router/pathFor context and
20
+ // mounts self-test/diagnostic pages that fetch on mount. TroubleshootingPage's
21
+ // own behavior is to own the active tab and pass it (and a goToTab callback)
22
+ // down, so mock the container to a marker that exposes its `tab`/`subtab` props
23
+ // and a control that invokes `goToTab` — exactly the contract under test.
24
+ jest.mock("../../../src/components/TroubleshootingTabContainer", () => ({
25
+ __esModule: true,
26
+ default: ({
27
+ tab,
28
+ subtab,
29
+ goToTab,
30
+ }: {
31
+ tab: string;
32
+ subtab: string;
33
+ goToTab: (tab: string) => void;
34
+ }) => (
35
+ <div data-testid="tab-container" data-tab={tab} data-subtab={subtab}>
36
+ <button type="button" onClick={() => goToTab("self-tests")}>
37
+ Go to self-tests
38
+ </button>
39
+ </div>
40
+ ),
41
+ }));
42
+
43
+ import TroubleshootingPage from "../../../src/components/TroubleshootingPage";
44
+
45
+ const renderPage = () =>
46
+ renderWithProviders(<TroubleshootingPage params={{ tab: "", subtab: "" }} />);
47
+
48
+ describe("TroubleshootingPage", () => {
49
+ it("renders a header", () => {
50
+ renderPage();
51
+ expect(screen.getByTestId("header")).toBeInTheDocument();
52
+ });
53
+
54
+ it("renders a heading", () => {
55
+ renderPage();
56
+ expect(
57
+ screen.getByRole("heading", { name: "Troubleshooting" })
58
+ ).toBeInTheDocument();
59
+ });
60
+
61
+ it("renders a tab container starting on the diagnostics tab", () => {
62
+ renderPage();
63
+ const tabContainer = screen.getByTestId("tab-container");
64
+ expect(tabContainer).toBeInTheDocument();
65
+ expect(tabContainer).toHaveAttribute("data-tab", "diagnostics");
66
+ });
67
+
68
+ it("switches tabs", async () => {
69
+ const user = userEvent.setup();
70
+ renderPage();
71
+
72
+ expect(screen.getByTestId("tab-container")).toHaveAttribute(
73
+ "data-tab",
74
+ "diagnostics"
75
+ );
76
+
77
+ await user.click(screen.getByRole("button", { name: "Go to self-tests" }));
78
+
79
+ expect(screen.getByTestId("tab-container")).toHaveAttribute(
80
+ "data-tab",
81
+ "self-tests"
82
+ );
83
+ });
84
+
85
+ it("sets the document title", () => {
86
+ renderPage();
87
+ expect(document.title).toBe(title("Troubleshooting"));
88
+ });
89
+
90
+ it("shows a footer", () => {
91
+ renderPage();
92
+ expect(screen.getByTestId("footer")).toBeInTheDocument();
93
+ });
94
+ });