@thepalaceproject/circulation-admin 1.42.2 → 1.43.0-post.10

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 (78) hide show
  1. package/.node-version +1 -1
  2. package/.nvmrc +1 -1
  3. package/CLAUDE.md +17 -8
  4. package/README.md +7 -7
  5. package/dist/circulation-admin.js +1 -1
  6. package/eslint.config.mjs +9 -1
  7. package/jest.config.js +56 -9
  8. package/package.json +10 -20
  9. package/tests/jest/actions.test.ts +962 -0
  10. package/tests/jest/components/AccountPage.test.tsx +41 -0
  11. package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
  12. package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
  13. package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
  14. package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
  15. package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
  16. package/tests/jest/components/BookDetails.test.tsx +133 -0
  17. package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
  18. package/tests/jest/components/CatalogServices.test.tsx +63 -0
  19. package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
  20. package/tests/jest/components/ConfigPage.test.tsx +58 -0
  21. package/tests/jest/components/CustomListPage.test.tsx +57 -0
  22. package/tests/jest/components/CustomListSearch.test.tsx +275 -0
  23. package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
  24. package/tests/jest/components/DashboardPage.test.tsx +76 -0
  25. package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
  26. package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
  27. package/tests/jest/components/EditableConfigList.test.tsx +8 -2
  28. package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
  29. package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
  30. package/tests/jest/components/ErrorMessage.test.tsx +97 -0
  31. package/tests/jest/components/Footer.test.tsx +141 -0
  32. package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
  33. package/tests/jest/components/JsonField.test.tsx +20 -0
  34. package/tests/jest/components/LaneEditor.test.tsx +2 -6
  35. package/tests/jest/components/LanePage.test.tsx +68 -0
  36. package/tests/jest/components/LanguageField.test.tsx +40 -0
  37. package/tests/jest/components/LoadButton.test.tsx +49 -0
  38. package/tests/jest/components/ManagePatrons.test.tsx +73 -0
  39. package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
  40. package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
  41. package/tests/jest/components/MetadataServices.test.tsx +59 -0
  42. package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
  43. package/tests/jest/components/PatronInfo.test.tsx +54 -0
  44. package/tests/jest/components/ProtocolFormField.test.tsx +36 -0
  45. package/tests/jest/components/SaveButton.test.tsx +47 -0
  46. package/tests/jest/components/SelfTestResult.test.tsx +164 -0
  47. package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
  48. package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
  49. package/tests/jest/components/SetupPage.test.tsx +41 -0
  50. package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
  51. package/tests/jest/components/Stats.test.tsx +4 -8
  52. package/tests/jest/components/TabContainer.test.tsx +68 -0
  53. package/tests/jest/components/Timestamp.test.tsx +74 -0
  54. package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
  55. package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
  56. package/tests/jest/components/WelcomePage.test.tsx +43 -0
  57. package/tests/jest/components/WithEditButton.test.tsx +55 -0
  58. package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
  59. package/tests/jest/editorAdapter.test.ts +245 -0
  60. package/tests/jest/index.test.tsx +50 -0
  61. package/tests/jest/legacy-setup.ts +13 -0
  62. package/tests/jest/legacyJsdomEnvironment.js +49 -0
  63. package/tests/jest/models/Admin.test.ts +206 -0
  64. package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
  65. package/tests/jest/reducers/classifications.test.ts +161 -0
  66. package/tests/jest/reducers/collections.test.ts +96 -0
  67. package/tests/jest/reducers/complaints.test.ts +166 -0
  68. package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
  69. package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
  70. package/tests/jest/reducers/customListDetails.test.ts +120 -0
  71. package/tests/jest/reducers/customListEditor.test.ts +2823 -0
  72. package/tests/jest/reducers/managePatrons.test.ts +71 -0
  73. package/tests/jest/testUtils/formDataShim.test.ts +135 -0
  74. package/tests/jest/testUtils/formDataShim.ts +112 -0
  75. package/tests/jest/utils/sharedFunctions.test.ts +121 -0
  76. package/tsconfig.json +3 -4
  77. package/testReporter.js +0 -29
  78. package/tests/jest/components/AdvancedSearchBuilder.test.tsx +0 -38
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ // Header pulls in the react-router v3 context a plain render does not supply, and
5
+ // both Header and Footer are page chrome with no behavior worth exercising here.
6
+ // Mock them to markers so the test stays about what WelcomePage composes.
7
+ jest.mock("../../../src/components/Header", () => ({
8
+ __esModule: true,
9
+ default: () => <div data-testid="header" />,
10
+ }));
11
+ jest.mock("../../../src/components/Footer", () => ({
12
+ __esModule: true,
13
+ default: () => <div data-testid="footer" />,
14
+ }));
15
+
16
+ import WelcomePage from "../../../src/components/WelcomePage";
17
+
18
+ describe("WelcomePage", () => {
19
+ it("shows the header", () => {
20
+ render(<WelcomePage />);
21
+ expect(screen.getByTestId("header")).toBeInTheDocument();
22
+ });
23
+
24
+ it("shows the welcome message", () => {
25
+ render(<WelcomePage />);
26
+ expect(
27
+ screen.getByRole("heading", {
28
+ name: "Welcome to the Circulation Admin Interface!",
29
+ })
30
+ ).toBeInTheDocument();
31
+ });
32
+
33
+ it("shows a link to the library create form", () => {
34
+ render(<WelcomePage />);
35
+ const link = screen.getByRole("link", { name: /create a new library/i });
36
+ expect(link).toHaveAttribute("href", "/admin/web/config/libraries/create");
37
+ });
38
+
39
+ it("shows the footer", () => {
40
+ render(<WelcomePage />);
41
+ expect(screen.getByTestId("footer")).toBeInTheDocument();
42
+ });
43
+ });
@@ -0,0 +1,55 @@
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 WithEditButton from "../../../src/components/WithEditButton";
6
+
7
+ describe("WithEditButton", () => {
8
+ it("shows children", () => {
9
+ render(
10
+ <WithEditButton disabled={false} onEdit={jest.fn()}>
11
+ child
12
+ </WithEditButton>
13
+ );
14
+
15
+ expect(screen.getByText("child")).toBeInTheDocument();
16
+ });
17
+
18
+ it("shows an edit button", () => {
19
+ render(
20
+ <WithEditButton disabled={false} onEdit={jest.fn()}>
21
+ child
22
+ </WithEditButton>
23
+ );
24
+
25
+ expect(screen.getByRole("button")).toHaveClass("edit-btn");
26
+ });
27
+
28
+ it("calls onEdit when clicked", async () => {
29
+ const user = userEvent.setup();
30
+ const onEdit = jest.fn();
31
+ render(
32
+ <WithEditButton disabled={false} onEdit={onEdit}>
33
+ child
34
+ </WithEditButton>
35
+ );
36
+
37
+ await user.click(screen.getByRole("button"));
38
+
39
+ expect(onEdit).toHaveBeenCalledTimes(1);
40
+ });
41
+
42
+ it("does nothing when disabled", async () => {
43
+ const user = userEvent.setup();
44
+ const onEdit = jest.fn();
45
+ render(
46
+ <WithEditButton disabled={true} onEdit={onEdit}>
47
+ child
48
+ </WithEditButton>
49
+ );
50
+
51
+ await user.click(screen.getByRole("button"));
52
+
53
+ expect(onEdit).not.toHaveBeenCalled();
54
+ });
55
+ });
@@ -0,0 +1,95 @@
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 WithRemoveButton from "../../../src/components/WithRemoveButton";
6
+
7
+ describe("WithRemoveButton", () => {
8
+ it("shows children", () => {
9
+ render(
10
+ <WithRemoveButton disabled={false} onRemove={jest.fn()}>
11
+ child
12
+ </WithRemoveButton>
13
+ );
14
+
15
+ expect(screen.getByText("child")).toBeInTheDocument();
16
+ });
17
+
18
+ it("shows a remove button", () => {
19
+ render(
20
+ <WithRemoveButton disabled={false} onRemove={jest.fn()}>
21
+ child
22
+ </WithRemoveButton>
23
+ );
24
+
25
+ expect(screen.getByRole("button")).toHaveClass("remove-btn");
26
+ });
27
+
28
+ it("calls onRemove when clicked", async () => {
29
+ const user = userEvent.setup();
30
+ const onRemove = jest.fn();
31
+ render(
32
+ <WithRemoveButton disabled={false} onRemove={onRemove}>
33
+ child
34
+ </WithRemoveButton>
35
+ );
36
+
37
+ await user.click(screen.getByRole("button"));
38
+
39
+ expect(onRemove).toHaveBeenCalledTimes(1);
40
+ });
41
+
42
+ it("does nothing when disabled", async () => {
43
+ const user = userEvent.setup();
44
+ const onRemove = jest.fn();
45
+ render(
46
+ <WithRemoveButton disabled={true} onRemove={onRemove}>
47
+ child
48
+ </WithRemoveButton>
49
+ );
50
+
51
+ await user.click(screen.getByRole("button"));
52
+
53
+ expect(onRemove).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it("calls onRemove when confirmation returns true", async () => {
57
+ const user = userEvent.setup();
58
+ const onRemove = jest.fn();
59
+ const confirmRemoval = jest.fn().mockReturnValue(true);
60
+ render(
61
+ <WithRemoveButton
62
+ disabled={false}
63
+ onRemove={onRemove}
64
+ confirmRemoval={confirmRemoval}
65
+ >
66
+ child
67
+ </WithRemoveButton>
68
+ );
69
+
70
+ await user.click(screen.getByRole("button"));
71
+
72
+ expect(confirmRemoval).toHaveBeenCalledTimes(1);
73
+ expect(onRemove).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ it("does not call onRemove when confirmation returns false", async () => {
77
+ const user = userEvent.setup();
78
+ const onRemove = jest.fn();
79
+ const confirmRemoval = jest.fn().mockReturnValue(false);
80
+ render(
81
+ <WithRemoveButton
82
+ disabled={false}
83
+ onRemove={onRemove}
84
+ confirmRemoval={confirmRemoval}
85
+ >
86
+ child
87
+ </WithRemoveButton>
88
+ );
89
+
90
+ await user.click(screen.getByRole("button"));
91
+
92
+ expect(confirmRemoval).toHaveBeenCalledTimes(1);
93
+ expect(onRemove).not.toHaveBeenCalled();
94
+ });
95
+ });
@@ -0,0 +1,245 @@
1
+ import adapter from "../../src/editorAdapter";
2
+ import {
3
+ OPDSEntry,
4
+ Contributor,
5
+ Series,
6
+ Category,
7
+ Summary,
8
+ } from "opds-feed-parser";
9
+
10
+ describe("editorAdapter", () => {
11
+ it("adapts valid OPDS entry", () => {
12
+ const entry = new OPDSEntry({
13
+ id: "id",
14
+ updated: "updated",
15
+ title: "title",
16
+ authors: [new Contributor({ name: "name", uri: "uri", role: "role" })],
17
+ subtitle: "subtitle",
18
+ contributors: [
19
+ new Contributor({ name: "name2", uri: "uri2", role: "role2" }),
20
+ ],
21
+ series: new Series({ name: "series", position: 2 }),
22
+ categories: [
23
+ new Category({ term: "term", scheme: "scheme", label: "label" }),
24
+ new Category({
25
+ term: "13-16",
26
+ scheme: "http://schema.org/typicalAgeRange",
27
+ label: "age",
28
+ }),
29
+ new Category({
30
+ term: "ya",
31
+ scheme: "http://schema.org/audience",
32
+ label: "ya",
33
+ }),
34
+ new Category({
35
+ term: "fiction",
36
+ scheme: "http://librarysimplified.org/terms/fiction/",
37
+ label: "Fiction",
38
+ }),
39
+ ],
40
+ identifiers: [],
41
+ links: [
42
+ {
43
+ href: "hide",
44
+ rel: "http://librarysimplified.org/terms/rel/hide",
45
+ type: "type",
46
+ title: "title",
47
+ role: "role",
48
+ },
49
+ {
50
+ href: "restore",
51
+ rel: "http://librarysimplified.org/terms/rel/restore",
52
+ type: "type",
53
+ title: "title",
54
+ role: "role",
55
+ },
56
+ {
57
+ href: "refresh",
58
+ rel: "http://librarysimplified.org/terms/rel/refresh",
59
+ type: "type",
60
+ title: "title",
61
+ role: "role",
62
+ },
63
+ {
64
+ href: "edit",
65
+ rel: "edit",
66
+ type: "type",
67
+ title: "title",
68
+ role: "role",
69
+ },
70
+ {
71
+ href: "issues",
72
+ rel: "issues",
73
+ type: "type",
74
+ title: "title",
75
+ role: "role",
76
+ },
77
+ {
78
+ href: "change-cover",
79
+ rel: "http://librarysimplified.org/terms/rel/change_cover",
80
+ type: "type",
81
+ title: "title",
82
+ role: "role",
83
+ },
84
+ {
85
+ href: "cover",
86
+ rel: "http://opds-spec.org/image",
87
+ type: "image/png",
88
+ title: "title",
89
+ role: "role",
90
+ },
91
+ ],
92
+ issued: "issued",
93
+ language: "language",
94
+ rights: "rights",
95
+ publisher: "publisher",
96
+ published: "published",
97
+ summary: new Summary({ content: "content", link: "link" }),
98
+ unparsed: {
99
+ "schema:alternativeHeadline": [{ _: "subtitle" }],
100
+ $: {
101
+ "schema:additionalType": { value: "medium" },
102
+ },
103
+ "bib:publisherImprint": [{ _: "imprint" }],
104
+ "schema:Rating": [
105
+ {
106
+ $: {
107
+ "schema:ratingValue": { value: "0.3" },
108
+ "schema:additionalType": {
109
+ value: "http://librarysimplified.org/terms/rel/quality",
110
+ },
111
+ },
112
+ },
113
+ {
114
+ $: {
115
+ "schema:ratingValue": { value: "4" },
116
+ "schema:additionalType": {
117
+ value: "http://schema.org/ratingValue",
118
+ },
119
+ },
120
+ },
121
+ ],
122
+ },
123
+ });
124
+
125
+ const adapted = adapter(entry);
126
+ expect(adapted.title).toBe("title");
127
+ expect(adapted.authors).toStrictEqual([
128
+ { name: "name", uri: "uri", role: "aut" },
129
+ ]);
130
+ // The adapter flattens `Contributor` instances into plain objects, so assert
131
+ // the shape it produces rather than comparing against `entry.contributors`.
132
+ expect(adapted.contributors).toStrictEqual([
133
+ { name: "name2", uri: "uri2", role: "role2" },
134
+ ]);
135
+ expect(adapted.subtitle).toBe("subtitle");
136
+ expect(adapted.summary).toBe("content");
137
+ expect(adapted.audience).toBe("ya");
138
+ expect(adapted.targetAgeRange).toStrictEqual(["13", "16"]);
139
+ expect(adapted.fiction).toBe(true);
140
+ expect(adapted.categories).toStrictEqual(["label", "age", "ya", "Fiction"]);
141
+ expect(adapted.hideLink).toStrictEqual(entry.links[0]);
142
+ expect(adapted.restoreLink).toStrictEqual(entry.links[1]);
143
+ expect(adapted.refreshLink).toStrictEqual(entry.links[2]);
144
+ expect(adapted.editLink).toStrictEqual(entry.links[3]);
145
+ expect(adapted.issuesLink).toStrictEqual(entry.links[4]);
146
+ expect(adapted.changeCoverLink).toStrictEqual(entry.links[5]);
147
+ expect(adapted.coverUrl).toBe(entry.links[6].href);
148
+ expect(adapted.series).toBe("series");
149
+ expect(adapted.seriesPosition).toBe(2);
150
+ expect(adapted.medium).toBe("medium");
151
+ expect(adapted.language).toBe("language");
152
+ expect(adapted.publisher).toBe("publisher");
153
+ expect(adapted.imprint).toBe("imprint");
154
+ expect(adapted.issued).toBe("issued");
155
+ expect(adapted.rating).toBe("4");
156
+ });
157
+
158
+ it("adapts visibility status from categories", () => {
159
+ const entry = new OPDSEntry({
160
+ id: "id",
161
+ updated: "updated",
162
+ title: "title",
163
+ subtitle: "subtitle",
164
+ authors: [],
165
+ contributors: [],
166
+ series: new Series({ name: "series", position: 2 }),
167
+ categories: [
168
+ new Category({ term: "term", scheme: "scheme", label: "label" }),
169
+ new Category({
170
+ term: "manually-suppressed",
171
+ scheme: "http://palaceproject.io/terms/visibility-status",
172
+ label: "Manually Suppressed",
173
+ }),
174
+ ],
175
+ identifiers: [],
176
+ links: [],
177
+ issued: "issued",
178
+ language: "language",
179
+ rights: "rights",
180
+ publisher: "publisher",
181
+ published: "published",
182
+ summary: new Summary({ content: "content", link: "link" }),
183
+ unparsed: {},
184
+ });
185
+
186
+ const adapted = adapter(entry);
187
+ expect(adapted.visibilityStatus).toBe("manually-suppressed");
188
+ });
189
+
190
+ it("adapts policy-filtered visibility status", () => {
191
+ const entry = new OPDSEntry({
192
+ id: "id",
193
+ updated: "updated",
194
+ title: "title",
195
+ subtitle: "subtitle",
196
+ authors: [],
197
+ contributors: [],
198
+ series: new Series({ name: "series", position: 2 }),
199
+ categories: [
200
+ new Category({
201
+ term: "policy-filtered",
202
+ scheme: "http://palaceproject.io/terms/visibility-status",
203
+ label: "Policy Filtered",
204
+ }),
205
+ ],
206
+ identifiers: [],
207
+ links: [],
208
+ issued: "issued",
209
+ language: "language",
210
+ rights: "rights",
211
+ publisher: "publisher",
212
+ published: "published",
213
+ summary: new Summary({ content: "content", link: "link" }),
214
+ unparsed: {},
215
+ });
216
+
217
+ const adapted = adapter(entry);
218
+ expect(adapted.visibilityStatus).toBe("policy-filtered");
219
+ });
220
+
221
+ it("doesn't crash when expected data is missing", () => {
222
+ const entry = new OPDSEntry({
223
+ id: "id",
224
+ updated: "updated",
225
+ title: "title",
226
+ subtitle: "subtitle",
227
+ authors: [],
228
+ contributors: [],
229
+ series: new Series({ name: "series", position: 2 }),
230
+ categories: [],
231
+ identifiers: [],
232
+ links: [],
233
+ issued: "issued",
234
+ language: "language",
235
+ rights: "rights",
236
+ publisher: "publisher",
237
+ published: "published",
238
+ summary: new Summary({ content: "content", link: "link" }),
239
+ unparsed: {},
240
+ });
241
+
242
+ const adapted = adapter(entry);
243
+ expect(adapted.title).toBe("title");
244
+ });
245
+ });
@@ -0,0 +1,50 @@
1
+ import * as React from "react";
2
+ import * as ReactDOM from "react-dom";
3
+ import { Router } from "react-router";
4
+
5
+ import SetupPage from "../../src/components/SetupPage";
6
+
7
+ // index.tsx does `export = CirculationAdmin`.
8
+ const CirculationAdmin = require("../../src/index");
9
+
10
+ /** Walk a React element tree for an element of the given type — the structural
11
+ * analog of enzyme's `wrapper.find(Type)`, used where the tree is the app root
12
+ * and there is no rendered DOM to query against. */
13
+ function containsType(node: any, type: React.ElementType): boolean {
14
+ if (!node || typeof node !== "object") return false;
15
+ if (Array.isArray(node)) return node.some((n) => containsType(n, type));
16
+ if (node.type === type) return true;
17
+ return containsType(node.props && node.props.children, type);
18
+ }
19
+
20
+ describe("CirculationAdmin", () => {
21
+ let renderSpy: jest.SpyInstance;
22
+
23
+ beforeEach(() => {
24
+ // Capture what the bootstrap hands to ReactDOM.render without letting it
25
+ // paint into the jsdom document. We assert on the captured element tree
26
+ // (the structural analog of the original enzyme `wrapper.find(...)`); what
27
+ // SetupPage and the routed pages render is covered by their own tests.
28
+ renderSpy = jest
29
+ .spyOn(ReactDOM, "render")
30
+ .mockImplementation(() => undefined);
31
+ });
32
+
33
+ afterEach(() => {
34
+ jest.restoreAllMocks();
35
+ });
36
+
37
+ it("renders the setup page when setting up", () => {
38
+ new CirculationAdmin({ settingUp: true, csrfToken: "token" });
39
+
40
+ expect(renderSpy).toHaveBeenCalledTimes(1);
41
+ expect(containsType(renderSpy.mock.calls[0][0], SetupPage)).toBe(true);
42
+ });
43
+
44
+ it("renders the router otherwise", () => {
45
+ new CirculationAdmin({ csrfToken: "token" });
46
+
47
+ expect(renderSpy).toHaveBeenCalledTimes(1);
48
+ expect(containsType(renderSpy.mock.calls[0][0], Router)).toBe(true);
49
+ });
50
+ });
@@ -0,0 +1,13 @@
1
+ import { configure } from "enzyme";
2
+ import * as Adapter from "enzyme-adapter-react-16";
3
+
4
+ // Setup for the legacy enzyme + chai + sinon tests under `src/**/__tests__/`.
5
+ // Replaces what `src/testHelper.ts` did for mocha. Delete alongside the last
6
+ // enzyme test.
7
+
8
+ configure({ adapter: new Adapter() });
9
+
10
+ // Unhandled promise rejections are tolerated for these tests — see the comment in
11
+ // `legacyJsdomEnvironment.js`. It cannot be done from here: jest hands test code a
12
+ // *copy* of `process`, so a listener registered in this file never reaches the real
13
+ // one that Node checks before throwing.
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ const JSDOMEnvironment = require("jest-environment-jsdom").default;
4
+
5
+ const warnUnhandledRejection = (reason) => {
6
+ console.warn("Unhandled promise rejection in a legacy test:", reason);
7
+ };
8
+
9
+ /**
10
+ * jsdom environment for the legacy enzyme tests. Delete it with the last one.
11
+ *
12
+ * Two things it restores that `src/testHelper.ts` used to provide under mocha:
13
+ *
14
+ * 1. `global.jsdom`. A handful of tests call `global.jsdom.reconfigure({ url })` to
15
+ * change the page origin mid-test. `history.replaceState` is not a substitute —
16
+ * they navigate across origins (`http://example.com`), which it cannot do.
17
+ *
18
+ * 2. Tolerance for unhandled promise rejections. Several suites stub a `fetch` that
19
+ * rejects, and nothing awaits the result; the rejection surfaces after the test
20
+ * has already passed. Mocha shrugged those off because its runner keeps an
21
+ * `unhandledRejection` listener installed, and Node only throws when there are
22
+ * none. Under jest they kill the worker and take the whole suite with them.
23
+ *
24
+ * Re-arming has to happen on the `teardown` event, not in the constructor.
25
+ * `jest-circus/globalErrorHandlers` reads `process.listeners()` off the sandbox's
26
+ * *copied* `process` (always empty) but calls `removeAllListeners()` on the real
27
+ * one. So any listener registered earlier is dropped at `setup` and "restored"
28
+ * from an empty list, leaving zero listeners during the post-run window where
29
+ * these rejections actually land.
30
+ *
31
+ * Scoping this to the legacy environment keeps the modern `tests/jest/` suite
32
+ * strict: jest clears listeners at each file's `setup`, and nothing re-arms one
33
+ * for a project that does not use this environment.
34
+ */
35
+ module.exports = class LegacyJSDOMEnvironment extends JSDOMEnvironment {
36
+ constructor(config, context) {
37
+ super(config, context);
38
+ this.global.jsdom = this.dom;
39
+ }
40
+
41
+ handleTestEvent(event) {
42
+ if (
43
+ event.name === "teardown" &&
44
+ process.listenerCount("unhandledRejection") === 0
45
+ ) {
46
+ process.on("unhandledRejection", warnUnhandledRejection);
47
+ }
48
+ }
49
+ };