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

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,167 @@
1
+ import * as React from "react";
2
+ import { render } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import NeighborhoodAnalyticsForm from "../../../src/components/NeighborhoodAnalyticsForm";
6
+ import { SettingData } from "../../../src/interfaces";
7
+
8
+ const patronAuthSetting: SettingData = {
9
+ key: "neighborhood_mode",
10
+ label: "Patron Auth",
11
+ options: [
12
+ { key: "disabled", label: "Off" },
13
+ { key: "choice1", label: "Choice 1" },
14
+ { key: "choice2", label: "Choice 2" },
15
+ ],
16
+ };
17
+
18
+ const analyticsSetting: SettingData = {
19
+ key: "location_source",
20
+ label: "Analytics",
21
+ options: [
22
+ { key: "disabled", label: "Off" },
23
+ { key: "choice1", label: "Choice 1" },
24
+ { key: "choice2", label: "Choice 2" },
25
+ ],
26
+ };
27
+
28
+ // The Panel renders its body collapsed and `aria-hidden` by default, so the
29
+ // form is present in the DOM but hidden from the accessibility tree; query it
30
+ // through the container.
31
+ const getSelect = (container: HTMLElement) =>
32
+ container.querySelector("select") as HTMLSelectElement;
33
+ const panelTitle = (container: HTMLElement) =>
34
+ container.querySelector(".panel-title") as HTMLElement;
35
+ const warning = (container: HTMLElement) =>
36
+ container.querySelector(".bg-warning") as HTMLElement | null;
37
+
38
+ describe("NeighborhoodAnalyticsForm", () => {
39
+ it("renders a panel with the feature name", () => {
40
+ const { container } = render(
41
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
42
+ );
43
+ expect(container.querySelector(".panel")).toBeInTheDocument();
44
+ expect(panelTitle(container)).toHaveTextContent(
45
+ "Patron Neighborhood Analytics: Disabled"
46
+ );
47
+ });
48
+
49
+ it("renders a select whose options reflect the setting", () => {
50
+ const { container } = render(
51
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
52
+ );
53
+ const options = Array.from(getSelect(container).querySelectorAll("option"));
54
+ expect(options).toHaveLength(patronAuthSetting.options.length);
55
+ options.forEach((option, idx) => {
56
+ expect(option).toHaveValue(patronAuthSetting.options[idx].key);
57
+ expect(option).toHaveTextContent(patronAuthSetting.options[idx].label);
58
+ });
59
+ });
60
+
61
+ it("renders a warning message pointing at the paired service once enabled", async () => {
62
+ const user = userEvent.setup();
63
+ const { container, rerender } = render(
64
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
65
+ );
66
+
67
+ // No warning while the feature is off.
68
+ expect(warning(container)).not.toBeInTheDocument();
69
+ await user.selectOptions(getSelect(container), "disabled");
70
+ expect(warning(container)).not.toBeInTheDocument();
71
+
72
+ // Enabling it surfaces a warning that links to the analytics config.
73
+ await user.selectOptions(getSelect(container), "choice1");
74
+ expect(warning(container)).toBeInTheDocument();
75
+ expect(warning(container)).toHaveTextContent(
76
+ "This feature will work only if it is also enabled in your local analytics service configuration settings."
77
+ );
78
+ expect(warning(container).querySelector("a")).toHaveAttribute(
79
+ "href",
80
+ "/admin/web/config/analytics"
81
+ );
82
+
83
+ // Switching the setting flips the paired service to patron auth.
84
+ rerender(<NeighborhoodAnalyticsForm setting={analyticsSetting} />);
85
+ expect(warning(container)).toHaveTextContent(
86
+ "This feature will work only if it is also enabled in your patron authentication service configuration settings."
87
+ );
88
+ expect(warning(container).querySelector("a")).toHaveAttribute(
89
+ "href",
90
+ "/admin/web/config/patronAuth"
91
+ );
92
+ });
93
+
94
+ it("updates the header when an option is chosen", async () => {
95
+ const user = userEvent.setup();
96
+ const { container } = render(
97
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
98
+ );
99
+ expect(panelTitle(container)).toHaveTextContent(
100
+ "Patron Neighborhood Analytics: Disabled"
101
+ );
102
+
103
+ await user.selectOptions(getSelect(container), "choice1");
104
+ expect(panelTitle(container)).toHaveTextContent(
105
+ "Patron Neighborhood Analytics: Enabled"
106
+ );
107
+ });
108
+
109
+ it("reflects the currentValue prop as the initial selection", () => {
110
+ const { container } = render(
111
+ <NeighborhoodAnalyticsForm
112
+ setting={patronAuthSetting}
113
+ currentValue="choice1"
114
+ />
115
+ );
116
+ // A non-"disabled" currentValue means the feature starts enabled.
117
+ expect(panelTitle(container)).toHaveTextContent(
118
+ "Patron Neighborhood Analytics: Enabled"
119
+ );
120
+ expect(getSelect(container)).toHaveValue("choice1");
121
+ });
122
+
123
+ it("tells the user whether the feature is currently enabled", async () => {
124
+ const user = userEvent.setup();
125
+ const { container } = render(
126
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
127
+ );
128
+
129
+ expect(panelTitle(container)).toHaveTextContent("Disabled");
130
+ await user.selectOptions(getSelect(container), "choice1");
131
+ expect(panelTitle(container)).toHaveTextContent("Enabled");
132
+ await user.selectOptions(getSelect(container), "choice2");
133
+ expect(panelTitle(container)).toHaveTextContent("Enabled");
134
+ await user.selectOptions(getSelect(container), "disabled");
135
+ expect(panelTitle(container)).toHaveTextContent("Disabled");
136
+ });
137
+
138
+ it("shows the warning only for enabled selections", async () => {
139
+ const user = userEvent.setup();
140
+ const { container } = render(
141
+ <NeighborhoodAnalyticsForm setting={patronAuthSetting} />
142
+ );
143
+
144
+ expect(warning(container)).not.toBeInTheDocument();
145
+ await user.selectOptions(getSelect(container), "choice1");
146
+ expect(warning(container)).toBeInTheDocument();
147
+ await user.selectOptions(getSelect(container), "choice2");
148
+ expect(warning(container)).toBeInTheDocument();
149
+ await user.selectOptions(getSelect(container), "disabled");
150
+ expect(warning(container)).not.toBeInTheDocument();
151
+ });
152
+
153
+ it("exposes the selected value through its getValue ref API", async () => {
154
+ // ServiceEditForm reads the chosen value via this imperative ref API.
155
+ const user = userEvent.setup();
156
+ const ref = React.createRef<NeighborhoodAnalyticsForm>();
157
+ const { container } = render(
158
+ <NeighborhoodAnalyticsForm ref={ref} setting={patronAuthSetting} />
159
+ );
160
+
161
+ expect(ref.current?.getValue()).toBe("");
162
+ await user.selectOptions(getSelect(container), "choice1");
163
+ expect(ref.current?.getValue()).toBe("choice1");
164
+ await user.selectOptions(getSelect(container), "choice2");
165
+ expect(ref.current?.getValue()).toBe("choice2");
166
+ });
167
+ });
@@ -0,0 +1,220 @@
1
+ import * as React from "react";
2
+ import { render, screen, within, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import PairedMenus from "../../../src/components/PairedMenus";
5
+ import { SettingData, LibraryData } from "../../../src/interfaces";
6
+
7
+ describe("PairedMenus", () => {
8
+ const inputListSetting: SettingData = {
9
+ key: "input",
10
+ label: "Input List",
11
+ default: ["a", "b", "c"],
12
+ options: [
13
+ { key: "a", label: "A" },
14
+ { key: "b", label: "B" },
15
+ { key: "c", label: "C" },
16
+ { key: "d", label: "D" },
17
+ { key: "e", label: "E" },
18
+ ],
19
+ paired: "dropdown",
20
+ };
21
+ const dropdownSetting: SettingData = {
22
+ key: "dropdown",
23
+ label: "Dropdown",
24
+ default: "b",
25
+ options: [
26
+ { key: "a", label: "A" },
27
+ { key: "b", label: "B" },
28
+ { key: "c", label: "C" },
29
+ { key: "d", label: "D" },
30
+ { key: "e", label: "E" },
31
+ ],
32
+ type: "select",
33
+ };
34
+
35
+ const renderPairedMenus = (props = {}) =>
36
+ render(
37
+ <PairedMenus
38
+ inputListSetting={inputListSetting}
39
+ dropdownSetting={dropdownSetting}
40
+ disabled={false}
41
+ {...props}
42
+ />
43
+ );
44
+
45
+ // The current InputList items are rendered as read-only text inputs inside the
46
+ // `.input-list-ul` list; return their displayed values.
47
+ const listItemValues = (container: HTMLElement): string[] =>
48
+ within(container.querySelector<HTMLElement>(".input-list-ul")!)
49
+ .getAllByRole("textbox")
50
+ .map((input) => (input as HTMLInputElement).value);
51
+
52
+ const dropdown = (container: HTMLElement): HTMLSelectElement =>
53
+ container.querySelector<HTMLSelectElement>('select[name="dropdown"]')!;
54
+
55
+ it("renders a fieldset with an InputList and a dropdown", () => {
56
+ const { container } = renderPairedMenus();
57
+ expect(
58
+ container.querySelector("fieldset.paired-menus")
59
+ ).toBeInTheDocument();
60
+ expect(container.querySelector(".input-list")).toBeInTheDocument();
61
+ expect(dropdown(container)).toBeInTheDocument();
62
+ });
63
+
64
+ it("renders the InputList items as read-only fields", () => {
65
+ const { container } = renderPairedMenus();
66
+ const listItems = within(
67
+ container.querySelector<HTMLElement>(".input-list-ul")!
68
+ ).getAllByRole("textbox");
69
+ expect(listItems).toHaveLength(3);
70
+ listItems.forEach((input) =>
71
+ expect((input as HTMLInputElement).readOnly).toBe(true)
72
+ );
73
+ expect(listItemValues(container)).toEqual(["A", "B", "C"]);
74
+ });
75
+
76
+ it("renders the dropdown with its default value and available options", () => {
77
+ const { container } = renderPairedMenus();
78
+ const select = dropdown(container);
79
+ expect(select).toHaveValue("b");
80
+ const options = within(select).getAllByRole("option");
81
+ expect(options.map((o) => o.textContent)).toEqual(["A", "B", "C"]);
82
+ expect(options.map((o) => (o as HTMLOptionElement).value)).toEqual([
83
+ "a",
84
+ "b",
85
+ "c",
86
+ ]);
87
+ });
88
+
89
+ it("optionally disables the InputList's button", () => {
90
+ const { rerender } = renderPairedMenus();
91
+ expect(screen.getByRole("button", { name: "Add" })).toBeEnabled();
92
+
93
+ rerender(
94
+ <PairedMenus
95
+ inputListSetting={inputListSetting}
96
+ dropdownSetting={dropdownSetting}
97
+ disabled={false}
98
+ readOnly={true}
99
+ />
100
+ );
101
+ expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
102
+ });
103
+
104
+ it("reflects the default InputList values and dropdown value", () => {
105
+ const { container } = renderPairedMenus();
106
+ expect(listItemValues(container)).toEqual(["A", "B", "C"]);
107
+ expect(dropdown(container)).toHaveValue("b");
108
+ });
109
+
110
+ it("takes an optional item prop", () => {
111
+ const item: LibraryData = {
112
+ name: "Library",
113
+ short_name: "lib",
114
+ uuid: "123",
115
+ settings: {
116
+ input: ["c", "d", "e"],
117
+ dropdown: "d",
118
+ },
119
+ };
120
+ const { container } = renderPairedMenus({ item });
121
+
122
+ const listItems = within(
123
+ container.querySelector<HTMLElement>(".input-list-ul")!
124
+ ).getAllByRole("textbox");
125
+ expect(listItems.map((i) => (i as HTMLInputElement).value)).toEqual([
126
+ "C",
127
+ "D",
128
+ "E",
129
+ ]);
130
+ expect(listItems.map((i) => (i as HTMLInputElement).name)).toEqual([
131
+ "input_c",
132
+ "input_d",
133
+ "input_e",
134
+ ]);
135
+
136
+ const select = dropdown(container);
137
+ expect(select).toHaveValue("d");
138
+ expect(
139
+ within(select)
140
+ .getAllByRole("option")
141
+ .map((o) => o.textContent)
142
+ ).toEqual(["C", "D", "E"]);
143
+ });
144
+
145
+ it("updates the InputList when an item is removed", async () => {
146
+ const user = userEvent.setup();
147
+ const { container } = renderPairedMenus();
148
+ expect(listItemValues(container)).toEqual(["A", "B", "C"]);
149
+
150
+ // Remove the first list item ("A").
151
+ await user.click(screen.getAllByRole("button", { name: /Delete/ })[0]);
152
+
153
+ await waitFor(() =>
154
+ expect(
155
+ within(
156
+ container.querySelector<HTMLElement>(".input-list-ul")!
157
+ ).getAllByRole("textbox")
158
+ ).toHaveLength(2)
159
+ );
160
+ expect(listItemValues(container)).toEqual(["B", "C"]);
161
+ });
162
+
163
+ it("recalculates the dropdown options based on the InputList values", async () => {
164
+ const user = userEvent.setup();
165
+ const { container } = renderPairedMenus();
166
+ expect(
167
+ within(dropdown(container))
168
+ .getAllByRole("option")
169
+ .map((o) => o.textContent)
170
+ ).toEqual(["A", "B", "C"]);
171
+
172
+ // Removing "A" leaves the InputList holding [b, c], so the dropdown may only
173
+ // offer B and C.
174
+ await user.click(screen.getAllByRole("button", { name: /Delete/ })[0]);
175
+
176
+ await waitFor(() =>
177
+ expect(
178
+ within(dropdown(container))
179
+ .getAllByRole("option")
180
+ .map((o) => o.textContent)
181
+ ).toEqual(["B", "C"])
182
+ );
183
+ });
184
+
185
+ it("updates the dropdown selection", async () => {
186
+ const user = userEvent.setup();
187
+ const { container } = renderPairedMenus();
188
+ const select = dropdown(container);
189
+ expect(select).toHaveValue("b");
190
+
191
+ await user.selectOptions(select, "c");
192
+ expect(select).toHaveValue("c");
193
+ });
194
+
195
+ it("displays a message when the dropdown has no available values", async () => {
196
+ const user = userEvent.setup();
197
+ const { container } = renderPairedMenus();
198
+
199
+ // Remove every item from the InputList.
200
+ for (let remaining = 3; remaining > 0; remaining--) {
201
+ const removeButtons = screen.getAllByRole("button", { name: /Delete/ });
202
+ expect(removeButtons).toHaveLength(remaining);
203
+ await user.click(removeButtons[0]);
204
+ await waitFor(() =>
205
+ expect(
206
+ screen.queryAllByRole("button", { name: /Delete/ })
207
+ ).toHaveLength(remaining - 1)
208
+ );
209
+ }
210
+
211
+ const message =
212
+ "In order to set this value, you must add at least one option from the menu above.";
213
+ expect(screen.getByText(message)).toBeInTheDocument();
214
+ expect(container.querySelector(".bg-warning")).toHaveTextContent(message);
215
+
216
+ // The dropdown select is replaced by the warning; only the add menu remains.
217
+ expect(dropdown(container)).not.toBeInTheDocument();
218
+ expect(container.querySelectorAll("select")).toHaveLength(1);
219
+ });
220
+ });
@@ -0,0 +1,114 @@
1
+ import * as React from "react";
2
+ import { screen, waitFor } from "@testing-library/react";
3
+
4
+ import { renderWithProviders } from "../testUtils/withProviders";
5
+ import buildStore from "../../../src/store";
6
+ // Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
7
+ // are exercised: the panel fetches its data on mount, the fetch populates the
8
+ // Redux store, and mapStateToProps feeds it back in as props.
9
+ import PatronAuthServices from "../../../src/components/PatronAuthServices";
10
+
11
+ const listData = {
12
+ patron_auth_services: [
13
+ {
14
+ id: 2,
15
+ protocol: "test protocol",
16
+ settings: { test_setting: "test setting" },
17
+ libraries: [
18
+ { short_name: "nypl", test_library_setting: "test library setting" },
19
+ ],
20
+ name: "nypl protocol",
21
+ },
22
+ ],
23
+ protocols: [
24
+ { name: "test protocol", label: "test protocol label", settings: [] },
25
+ ],
26
+ allLibraries: [{ short_name: "nypl" }],
27
+ };
28
+
29
+ /**
30
+ * Stub `fetch` so the panel's on-mount fetch resolves with `body`. Returning a
31
+ * fresh Response per call avoids "body already used" when several fetches run.
32
+ */
33
+ const stubFetch = (body: unknown) =>
34
+ jest.spyOn(globalThis, "fetch").mockImplementation(
35
+ async () =>
36
+ new Response(JSON.stringify(body), {
37
+ status: 200,
38
+ headers: { "Content-Type": "application/json" },
39
+ })
40
+ );
41
+
42
+ // A fresh store per test keeps the fetched data isolated between tests.
43
+ const renderConnected = (element: React.ReactElement) =>
44
+ renderWithProviders(element, {
45
+ reduxProviderProps: { store: buildStore() },
46
+ });
47
+
48
+ describe("PatronAuthServices", () => {
49
+ afterEach(() => {
50
+ jest.restoreAllMocks();
51
+ });
52
+
53
+ it("shows the patron auth service list", async () => {
54
+ stubFetch(listData);
55
+ const { container } = renderConnected(
56
+ <PatronAuthServices csrfToken="token" />
57
+ );
58
+
59
+ // The list appears once the connected component's on-mount fetch resolves.
60
+ // Its heading is the protocol-aware label built by PatronAuthServices.label().
61
+ expect(
62
+ await screen.findByRole("heading", {
63
+ level: 3,
64
+ name: /nypl protocol: test protocol label/,
65
+ })
66
+ ).toBeInTheDocument();
67
+
68
+ const editLink = container.querySelector("a.edit-item");
69
+ expect(editLink).not.toBeNull();
70
+ expect(editLink.getAttribute("href")).toBe(
71
+ "/admin/web/config/patronAuth/edit/2"
72
+ );
73
+
74
+ // No neighborhood analytics panel is shown in list mode.
75
+ expect(
76
+ screen.queryByText("Patron Neighborhood Analytics: Disabled")
77
+ ).not.toBeInTheDocument();
78
+ });
79
+
80
+ it("shows the neighborhood analytics panel when creating a service whose protocol has a neighborhood_mode setting", async () => {
81
+ const neighborhoodSetting = {
82
+ key: "neighborhood_mode",
83
+ label: "Neighborhood Mode",
84
+ description: "",
85
+ options: [],
86
+ };
87
+ const createData = {
88
+ patron_auth_services: [],
89
+ protocols: [
90
+ {
91
+ name: "test protocol",
92
+ label: "test protocol label",
93
+ settings: [neighborhoodSetting],
94
+ },
95
+ ],
96
+ allLibraries: [{ short_name: "nypl" }],
97
+ };
98
+ stubFetch(createData);
99
+
100
+ const { container } = renderConnected(
101
+ <PatronAuthServices csrfToken="token" editOrCreate="create" />
102
+ );
103
+
104
+ // Once the protocol data loads, the create form's extra section (the
105
+ // NeighborhoodAnalyticsForm) renders because the selected protocol carries a
106
+ // "neighborhood_mode" setting.
107
+ await waitFor(() =>
108
+ expect(container.querySelector("#neighborhood-panel")).toBeInTheDocument()
109
+ );
110
+ expect(
111
+ screen.getByText("Patron Neighborhood Analytics: Disabled")
112
+ ).toBeInTheDocument();
113
+ });
114
+ });