@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,41 @@
1
+ import * as React from "react";
2
+ import { render, screen, within } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import CirculationEventsDownloadForm from "../../../src/components/CirculationEventsDownloadForm";
6
+
7
+ describe("CirculationEventsDownloadForm", () => {
8
+ let hide: jest.Mock;
9
+
10
+ beforeEach(() => {
11
+ hide = jest.fn();
12
+ render(<CirculationEventsDownloadForm show={true} hide={hide} />);
13
+ });
14
+
15
+ it("renders start date and end date inputs", () => {
16
+ // The modal renders into a portal, so query the whole document.
17
+ const dates =
18
+ document.querySelectorAll<HTMLInputElement>("input[type='date']");
19
+ expect(dates).toHaveLength(2);
20
+ expect(dates[0].name).toBe("dateStart");
21
+ expect(dates[1].name).toBe("dateEnd");
22
+ });
23
+
24
+ it("renders download and close buttons", async () => {
25
+ const user = userEvent.setup();
26
+
27
+ expect(
28
+ screen.getByRole("button", { name: "Download" })
29
+ ).toBeInTheDocument();
30
+
31
+ // The modal header's dismiss control is also labeled "Close" (its visible
32
+ // "×" is aria-hidden), so scope to the footer for the reusable Close button.
33
+ const footer = document.querySelector<HTMLElement>(".modal-footer");
34
+ const closeButton = within(footer).getByRole("button", { name: "Close" });
35
+ expect(closeButton).toBeInTheDocument();
36
+
37
+ await user.click(closeButton);
38
+
39
+ expect(hide).toHaveBeenCalledTimes(1);
40
+ });
41
+ });
@@ -0,0 +1,166 @@
1
+ import * as React from "react";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { renderWithProviders } from "../testUtils/withProviders";
6
+ import buildStore from "../../../src/store";
7
+ import genreData from "../../../src/components/__tests__/genreData";
8
+ import classificationsData from "../../../src/components/__tests__/classificationsData";
9
+ // Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
10
+ // are exercised. The genre tree and classifications are fetched on mount and fed
11
+ // back in through the Redux store; `book` and `refreshCatalog` are own props.
12
+ import Classifications, {
13
+ Classifications as UnconnectedClassifications,
14
+ } from "../../../src/components/Classifications";
15
+
16
+ // A `targetAgeRange` is required: ClassificationsForm reads it in bookChanged on
17
+ // re-render (the legacy enzyme test used shallow, so the child never re-rendered).
18
+ const bookData = {
19
+ title: "title",
20
+ fiction: true,
21
+ categories: ["Space Opera"],
22
+ targetAgeRange: [null, null],
23
+ };
24
+ const bookUrl = "works/1234";
25
+ const classificationsUrl = "admin/works/1234/classifications";
26
+
27
+ const jsonResponse = (body: unknown) =>
28
+ new Response(JSON.stringify(body), {
29
+ status: 200,
30
+ headers: { "Content-Type": "application/json" },
31
+ });
32
+
33
+ // Resolves each fetch with the appropriate body based on the request URL/method.
34
+ const stubFetch = () =>
35
+ jest
36
+ .spyOn(globalThis, "fetch")
37
+ .mockImplementation(async (url, opts?: RequestInit) => {
38
+ const u = String(url);
39
+ if (opts?.method === "POST") {
40
+ return new Response("success", { status: 200 });
41
+ }
42
+ if (u === "/admin/genres") {
43
+ return jsonResponse(genreData);
44
+ }
45
+ if (u.endsWith("/classifications")) {
46
+ return jsonResponse({ classifications: classificationsData });
47
+ }
48
+ // getBookData (OPDS) on refresh — non-OPDS body so it settles quietly.
49
+ return new Response("{}", { status: 200 });
50
+ });
51
+
52
+ const renderConnected = (
53
+ overrides: Partial<{ refreshCatalog: () => Promise<any> }> = {}
54
+ ) => {
55
+ const store = buildStore();
56
+ const refreshCatalog =
57
+ overrides.refreshCatalog ?? jest.fn().mockResolvedValue(undefined);
58
+ const result = renderWithProviders(
59
+ <Classifications
60
+ store={store}
61
+ csrfToken="token"
62
+ bookUrl={bookUrl}
63
+ book={bookData as any}
64
+ refreshCatalog={refreshCatalog}
65
+ />,
66
+ { reduxProviderProps: { store } }
67
+ );
68
+ return { ...result, refreshCatalog };
69
+ };
70
+
71
+ describe("Classifications", () => {
72
+ afterEach(() => {
73
+ jest.restoreAllMocks();
74
+ });
75
+
76
+ it("shows the book title", async () => {
77
+ stubFetch();
78
+ renderConnected();
79
+ expect(
80
+ await screen.findByRole("heading", { level: 2, name: "title" })
81
+ ).toBeInTheDocument();
82
+ });
83
+
84
+ it("shows the updating indicator while fetching", () => {
85
+ // A never-resolving fetch keeps the panel in its fetching state.
86
+ jest
87
+ .spyOn(globalThis, "fetch")
88
+ .mockImplementation(() => new Promise(() => {}));
89
+ const { container } = renderConnected();
90
+ expect(container.querySelector(".updating-loader")).toBeInTheDocument();
91
+ });
92
+
93
+ it("shows the classifications form and table once the data loads, and hides the updating indicator", async () => {
94
+ stubFetch();
95
+ const { container } = renderConnected();
96
+
97
+ // The form renders once the genre tree loads.
98
+ expect(
99
+ await screen.findByRole("button", { name: "Save" })
100
+ ).toBeInTheDocument();
101
+ // The table renders once the classifications load.
102
+ expect(
103
+ screen.getByRole("heading", { name: "Related Classifications" })
104
+ ).toBeInTheDocument();
105
+ // A sample classification from the fixture shows in the table.
106
+ expect(screen.getByText("FICTION / Ghost")).toBeInTheDocument();
107
+ // Fetching is done, so the updating indicator is gone.
108
+ expect(container.querySelector(".updating-loader")).not.toBeInTheDocument();
109
+ });
110
+
111
+ it("shows a fetch error", () => {
112
+ // A network failure on mount would reject the (uncaught) fetch promise and
113
+ // crash the worker, so drive the error through the prop that connect derives.
114
+ render(
115
+ <UnconnectedClassifications
116
+ store={buildStore() as any}
117
+ csrfToken="token"
118
+ bookUrl={bookUrl}
119
+ book={bookData as any}
120
+ refreshCatalog={jest.fn().mockResolvedValue(undefined)}
121
+ bookAdminUrl="book admin url"
122
+ fetchError={{ status: 500, url: "url", response: "response" } as any}
123
+ fetchGenreTree={jest.fn()}
124
+ fetchClassifications={jest.fn()}
125
+ fetchBook={jest.fn()}
126
+ editClassifications={jest.fn().mockResolvedValue(undefined)}
127
+ genreTree={null}
128
+ classifications={null}
129
+ isFetching={false}
130
+ />
131
+ );
132
+ expect(screen.getByText(/Error: response/)).toBeInTheDocument();
133
+ });
134
+
135
+ it("fetches the genre tree and classifications on mount", async () => {
136
+ const fetchSpy = stubFetch();
137
+ renderConnected();
138
+ await screen.findByRole("button", { name: "Save" });
139
+
140
+ const urls = fetchSpy.mock.calls.map(([u]) => String(u));
141
+ expect(urls).toContain("/admin/genres");
142
+ expect(urls).toContain(classificationsUrl);
143
+ });
144
+
145
+ it("refreshes the book, classifications, and catalog after editing classifications", async () => {
146
+ const user = userEvent.setup();
147
+ const fetchSpy = stubFetch();
148
+ const { container, refreshCatalog } = renderConnected();
149
+
150
+ await screen.findByRole("button", { name: "Save" });
151
+
152
+ // The form requires an audience selection before it will submit.
153
+ await user.selectOptions(
154
+ container.querySelector('select[name="audience"]') as HTMLSelectElement,
155
+ "Adult"
156
+ );
157
+ await user.click(screen.getByRole("button", { name: "Save" }));
158
+
159
+ // Editing POSTs the edit_classifications URL, then refreshes the catalog.
160
+ await waitFor(() => expect(refreshCatalog).toHaveBeenCalledTimes(1));
161
+ const postCall = fetchSpy.mock.calls.find(
162
+ ([, opts]) => (opts as RequestInit)?.method === "POST"
163
+ );
164
+ expect(String(postCall[0])).toBe("admin/works/1234/edit_classifications");
165
+ });
166
+ });
@@ -0,0 +1,57 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ import ClassificationsTable from "../../../src/components/ClassificationsTable";
5
+ import classificationsData from "../../../src/components/__tests__/classificationsData";
6
+
7
+ // Mirrors ClassificationsTable's own `readableType`: it strips the genres URL
8
+ // prefix so the displayed type is human-readable.
9
+ const readableType = (type: string) =>
10
+ type.replace(
11
+ /http:\/\/librarysimplified\.org\/terms\/genres\/([^/]+)\//,
12
+ "$1"
13
+ );
14
+
15
+ describe("ClassificationsTable", () => {
16
+ describe("rendering", () => {
17
+ it("should show header", () => {
18
+ render(<ClassificationsTable classifications={classificationsData} />);
19
+
20
+ expect(
21
+ screen.getByRole("heading", {
22
+ level: 3,
23
+ name: "Related Classifications",
24
+ })
25
+ ).toBeInTheDocument();
26
+ });
27
+
28
+ it("should show column headers", () => {
29
+ render(<ClassificationsTable classifications={classificationsData} />);
30
+
31
+ const headers = screen.getAllByRole("columnheader");
32
+ expect(headers.map((header) => header.textContent)).toStrictEqual([
33
+ "Type",
34
+ "Name",
35
+ "Source",
36
+ "Weight",
37
+ ]);
38
+ });
39
+
40
+ it("should show one row for each classification", () => {
41
+ const { container } = render(
42
+ <ClassificationsTable classifications={classificationsData} />
43
+ );
44
+
45
+ const rows = container.querySelectorAll("tbody tr");
46
+ expect(rows).toHaveLength(classificationsData.length);
47
+ rows.forEach((row, i) => {
48
+ const cells = row.querySelectorAll("td");
49
+ const c = classificationsData[i];
50
+ expect(cells[0].textContent).toBe(readableType(c.type));
51
+ expect(cells[1].textContent).toBe(c.name);
52
+ expect(cells[2].textContent).toBe(c.source);
53
+ expect(parseInt(cells[3].textContent, 10)).toBe(c.weight);
54
+ });
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,58 @@
1
+ import * as React from "react";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import ColorPicker from "../../../src/components/ColorPicker";
6
+
7
+ describe("ColorPicker", () => {
8
+ const setting = {
9
+ key: "key",
10
+ label: "label",
11
+ };
12
+
13
+ it("should show the compact picker", () => {
14
+ const { container } = render(
15
+ <ColorPicker setting={setting} value="#123456" />
16
+ );
17
+
18
+ // The react-color CompactPicker renders a `.compact-picker` container whose
19
+ // hex field reflects the color it was given.
20
+ expect(container.querySelector(".compact-picker")).toBeInTheDocument();
21
+ expect(screen.getByLabelText("hex")).toHaveValue("#123456");
22
+ });
23
+
24
+ it("should have a hidden input with the value", () => {
25
+ const { container } = render(
26
+ <ColorPicker setting={setting} value="#123456" />
27
+ );
28
+
29
+ const input = container.querySelector<HTMLInputElement>(
30
+ 'input[type="hidden"]'
31
+ );
32
+ expect(input).toHaveAttribute("name", "key");
33
+ expect(input).toHaveValue("#123456");
34
+ });
35
+
36
+ it("should change the value", async () => {
37
+ const user = userEvent.setup();
38
+ // getValue() is the imperative API ProtocolFormField calls via a ref in
39
+ // production, so exercise it through a real ref here.
40
+ const ref = React.createRef<ColorPicker>();
41
+ const { container } = render(
42
+ <ColorPicker ref={ref} setting={setting} value="#123456" />
43
+ );
44
+ const hiddenInput = container.querySelector<HTMLInputElement>(
45
+ 'input[type="hidden"]'
46
+ );
47
+
48
+ expect(ref.current.getValue()).toBe("#123456");
49
+ expect(hiddenInput).toHaveValue("#123456");
50
+
51
+ // Clicking a palette swatch fires the picker's onChangeComplete, which
52
+ // updates the component's value (react-color debounces that callback).
53
+ await user.click(screen.getByTitle("#FE9200"));
54
+
55
+ await waitFor(() => expect(ref.current.getValue()).toBe("#fe9200"));
56
+ expect(hiddenInput).toHaveValue("#fe9200");
57
+ });
58
+ });
@@ -0,0 +1,171 @@
1
+ import * as React from "react";
2
+ import { installFormDataShim } from "../testUtils/formDataShim";
3
+ import { render, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+
6
+ import ComplaintForm from "../../../src/components/ComplaintForm";
7
+ installFormDataShim();
8
+
9
+ // ComplaintForm's default export is a plain (unconnected) class component, so a
10
+ // bare `render` is sufficient — no providers/store/context are required.
11
+ const renderForm = (
12
+ props: Partial<React.ComponentProps<typeof ComplaintForm>> = {}
13
+ ) =>
14
+ render(
15
+ <ComplaintForm
16
+ disabled={false}
17
+ complaintUrl="complaint url"
18
+ postComplaint={jest.fn().mockResolvedValue(undefined)}
19
+ refreshComplaints={jest.fn()}
20
+ {...props}
21
+ />
22
+ );
23
+
24
+ const getSelect = (container: HTMLElement) =>
25
+ container.querySelector("select") as HTMLSelectElement;
26
+
27
+ describe("ComplaintForm", () => {
28
+ describe("rendering", () => {
29
+ it("shows a select field with a default value", () => {
30
+ const { container } = renderForm();
31
+ const select = getSelect(container);
32
+ expect(select).toBeInTheDocument();
33
+ expect(select).not.toBeDisabled();
34
+ expect(screen.getAllByRole("option")[0]).toHaveTextContent(
35
+ "Complaint type"
36
+ );
37
+ });
38
+
39
+ it("shows complaint type options", () => {
40
+ renderForm();
41
+ const values = screen
42
+ .getAllByRole("option")
43
+ .map((option) => (option as HTMLOptionElement).value);
44
+ expect(values).toStrictEqual([
45
+ "",
46
+ "cannot-issue-loan",
47
+ "cannot-render",
48
+ "wrong-title",
49
+ "wrong-author",
50
+ "wrong-audience",
51
+ "cannot-fulfill-loan",
52
+ "bad-description",
53
+ "cannot-return",
54
+ "bad-cover-image",
55
+ "wrong-medium",
56
+ "wrong-age-range",
57
+ "wrong-genre",
58
+ ]);
59
+ });
60
+
61
+ it("formats complaint type options", () => {
62
+ renderForm();
63
+ const texts = screen
64
+ .getAllByRole("option")
65
+ .map((option) => option.textContent);
66
+ expect(texts).toStrictEqual([
67
+ "Complaint type",
68
+ "Cannot issue loan",
69
+ "Cannot render",
70
+ "Wrong title",
71
+ "Wrong author",
72
+ "Wrong audience",
73
+ "Cannot fulfill loan",
74
+ "Bad description",
75
+ "Cannot return",
76
+ "Bad cover image",
77
+ "Wrong medium",
78
+ "Wrong age range",
79
+ "Wrong genre",
80
+ ]);
81
+ });
82
+
83
+ it("shows a submit button", () => {
84
+ renderForm();
85
+ expect(
86
+ screen.getByRole("button", { name: "Submit" })
87
+ ).toBeInTheDocument();
88
+ });
89
+
90
+ it("disables the select and submit button when disabled", () => {
91
+ const { container } = renderForm({ disabled: true });
92
+ expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled();
93
+ expect(getSelect(container)).toBeDisabled();
94
+ });
95
+ });
96
+
97
+ describe("behavior", () => {
98
+ it("posts complaints", async () => {
99
+ const user = userEvent.setup();
100
+ const postComplaint = jest.fn().mockResolvedValue(undefined);
101
+ const { container } = renderForm({ postComplaint });
102
+
103
+ await user.selectOptions(getSelect(container), "bad-description");
104
+ await user.click(screen.getByRole("button", { name: "Submit" }));
105
+
106
+ expect(postComplaint).toHaveBeenCalledTimes(1);
107
+ expect(postComplaint.mock.calls[0][0]).toBe("complaint url");
108
+ expect(postComplaint.mock.calls[0][1].type).toBe(
109
+ "http://librarysimplified.org/terms/problem/bad-description"
110
+ );
111
+ });
112
+
113
+ it("refreshes complaints after a successful post", async () => {
114
+ const user = userEvent.setup();
115
+ const refreshComplaints = jest.fn();
116
+ const { container } = renderForm({
117
+ postComplaint: jest.fn().mockResolvedValue(undefined),
118
+ refreshComplaints,
119
+ });
120
+
121
+ await user.selectOptions(getSelect(container), "bad-description");
122
+ await user.click(screen.getByRole("button", { name: "Submit" }));
123
+
124
+ await waitFor(() => expect(refreshComplaints).toHaveBeenCalledTimes(1));
125
+ });
126
+
127
+ it("clears the selected complaint type after a successful post", async () => {
128
+ const user = userEvent.setup();
129
+ const { container } = renderForm({
130
+ postComplaint: jest.fn().mockResolvedValue(undefined),
131
+ });
132
+
133
+ const select = getSelect(container);
134
+ await user.selectOptions(select, "bad-description");
135
+ expect(select).toHaveValue("bad-description");
136
+
137
+ await user.click(screen.getByRole("button", { name: "Submit" }));
138
+
139
+ await waitFor(() => expect(getSelect(container)).toHaveValue(""));
140
+ });
141
+
142
+ it("displays an error if no type is selected", async () => {
143
+ const user = userEvent.setup();
144
+ const postComplaint = jest.fn().mockResolvedValue(undefined);
145
+ renderForm({ postComplaint });
146
+
147
+ await user.click(screen.getByRole("button", { name: "Submit" }));
148
+
149
+ expect(
150
+ await screen.findByText("You must select a complaint type!")
151
+ ).toBeInTheDocument();
152
+ expect(screen.getByRole("alert")).toHaveTextContent(
153
+ "You must select a complaint type!"
154
+ );
155
+ expect(postComplaint).not.toHaveBeenCalled();
156
+ });
157
+
158
+ it("shows an error message if the post fails", async () => {
159
+ const user = userEvent.setup();
160
+ const postComplaint = jest.fn().mockRejectedValue(new Error("nope"));
161
+ const { container } = renderForm({ postComplaint });
162
+
163
+ await user.selectOptions(getSelect(container), "bad-description");
164
+ await user.click(screen.getByRole("button", { name: "Submit" }));
165
+
166
+ expect(
167
+ await screen.findByText("Couldn't post complaint.")
168
+ ).toBeInTheDocument();
169
+ });
170
+ });
171
+ });