@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.
- package/.node-version +1 -1
- package/.nvmrc +1 -1
- package/CLAUDE.md +17 -8
- package/README.md +7 -7
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +56 -9
- package/package.json +10 -20
- package/tests/jest/actions.test.ts +962 -0
- package/tests/jest/components/AccountPage.test.tsx +41 -0
- package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
- package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
- package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
- package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
- package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
- package/tests/jest/components/BookDetails.test.tsx +133 -0
- package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
- package/tests/jest/components/CatalogServices.test.tsx +63 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
- package/tests/jest/components/ConfigPage.test.tsx +58 -0
- package/tests/jest/components/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
- package/tests/jest/components/DashboardPage.test.tsx +76 -0
- package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
- package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
- package/tests/jest/components/EditableConfigList.test.tsx +8 -2
- package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
- package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
- package/tests/jest/components/ErrorMessage.test.tsx +97 -0
- package/tests/jest/components/Footer.test.tsx +141 -0
- package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/JsonField.test.tsx +20 -0
- package/tests/jest/components/LaneEditor.test.tsx +2 -6
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/LoadButton.test.tsx +49 -0
- package/tests/jest/components/ManagePatrons.test.tsx +73 -0
- package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
- package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
- package/tests/jest/components/MetadataServices.test.tsx +59 -0
- package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
- package/tests/jest/components/PatronInfo.test.tsx +54 -0
- package/tests/jest/components/ProtocolFormField.test.tsx +36 -0
- package/tests/jest/components/SaveButton.test.tsx +47 -0
- package/tests/jest/components/SelfTestResult.test.tsx +164 -0
- package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
- package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
- package/tests/jest/components/SetupPage.test.tsx +41 -0
- package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
- package/tests/jest/components/Stats.test.tsx +4 -8
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -0
- package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
- package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
- package/tests/jest/components/WelcomePage.test.tsx +43 -0
- package/tests/jest/components/WithEditButton.test.tsx +55 -0
- package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
- package/tests/jest/editorAdapter.test.ts +245 -0
- package/tests/jest/index.test.tsx +50 -0
- package/tests/jest/legacy-setup.ts +13 -0
- package/tests/jest/legacyJsdomEnvironment.js +49 -0
- package/tests/jest/models/Admin.test.ts +206 -0
- package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
- package/tests/jest/reducers/classifications.test.ts +161 -0
- package/tests/jest/reducers/collections.test.ts +96 -0
- package/tests/jest/reducers/complaints.test.ts +166 -0
- package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
- package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
- package/tests/jest/reducers/customListDetails.test.ts +120 -0
- package/tests/jest/reducers/customListEditor.test.ts +2823 -0
- package/tests/jest/reducers/managePatrons.test.ts +71 -0
- package/tests/jest/testUtils/formDataShim.test.ts +135 -0
- package/tests/jest/testUtils/formDataShim.ts +112 -0
- package/tests/jest/utils/sharedFunctions.test.ts +121 -0
- package/tsconfig.json +3 -4
- package/testReporter.js +0 -29
- 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
|
+
};
|