@thepalaceproject/circulation-admin 1.43.0 → 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.
- package/CLAUDE.md +28 -7
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +22 -8
- package/package.json +6 -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/AdvancedSearchBuilder.test.tsx +93 -20
- 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/Announcement.test.tsx +76 -0
- package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
- package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
- package/tests/jest/components/Autocomplete.test.tsx +67 -0
- package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
- package/tests/jest/components/BookDetails.test.tsx +133 -0
- package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
- package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/BookEditor.test.tsx +20 -21
- package/tests/jest/components/CatalogPage.test.tsx +158 -0
- package/tests/jest/components/CatalogServices.test.tsx +100 -0
- package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
- package/tests/jest/components/Classifications.test.tsx +166 -0
- package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
- package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
- package/tests/jest/components/Collections.test.tsx +335 -7
- package/tests/jest/components/ColorPicker.test.tsx +58 -0
- package/tests/jest/components/ComplaintForm.test.tsx +171 -0
- package/tests/jest/components/Complaints.test.tsx +259 -0
- package/tests/jest/components/ConfigPage.test.tsx +58 -0
- package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/Contributors.test.tsx +157 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
- package/tests/jest/components/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomLists.test.tsx +621 -4
- package/tests/jest/components/CustomListsForBook.test.tsx +353 -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/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
- package/tests/jest/components/EditableConfigList.test.tsx +606 -9
- package/tests/jest/components/EditableInput.test.tsx +250 -1
- package/tests/jest/components/EditorField.test.tsx +95 -0
- 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/GenreForm.test.tsx +152 -0
- package/tests/jest/components/Header.test.tsx +421 -0
- package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
- package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/JsonField.test.tsx +20 -0
- package/tests/jest/components/Lane.test.tsx +132 -10
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +377 -13
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/Libraries.test.tsx +48 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -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 +95 -0
- package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -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 +479 -7
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SaveButton.test.tsx +47 -0
- package/tests/jest/components/SelfTestResult.test.tsx +164 -0
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -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/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +4 -15
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -0
- package/tests/jest/components/ToolTip.test.tsx +46 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -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/features/book.test.ts +75 -80
- package/tests/jest/fixtures/classificationsData.ts +45 -0
- package/tests/jest/fixtures/genreData.ts +811 -0
- package/tests/jest/index.test.tsx +50 -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/README.md +0 -6
|
@@ -0,0 +1,157 @@
|
|
|
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 Contributors from "../../../src/components/Contributors";
|
|
6
|
+
import { RolesData, ContributorData } from "../../../src/interfaces";
|
|
7
|
+
|
|
8
|
+
const roles: RolesData = {
|
|
9
|
+
aut: "Author",
|
|
10
|
+
ill: "Illustrator",
|
|
11
|
+
nrt: "Narrator",
|
|
12
|
+
trl: "Translator",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const contributors: ContributorData[] = [
|
|
16
|
+
{ name: "A Translator", role: "trl" },
|
|
17
|
+
{ name: "A Narrator", role: "nrt" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const renderContributors = (
|
|
21
|
+
props: Partial<React.ComponentProps<typeof Contributors>> = {}
|
|
22
|
+
) =>
|
|
23
|
+
render(
|
|
24
|
+
<Contributors
|
|
25
|
+
roles={roles}
|
|
26
|
+
contributors={contributors}
|
|
27
|
+
disabled={false}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const existingRows = (container: HTMLElement) =>
|
|
33
|
+
Array.from(container.querySelectorAll<HTMLElement>(".with-remove-button"));
|
|
34
|
+
|
|
35
|
+
const newContributorForm = () =>
|
|
36
|
+
screen
|
|
37
|
+
.getByRole("button", { name: "Add" })
|
|
38
|
+
.closest(".contributor-form") as HTMLElement;
|
|
39
|
+
|
|
40
|
+
const roleSelect = (row: HTMLElement) =>
|
|
41
|
+
row.querySelector('select[name="contributor-role"]') as HTMLSelectElement;
|
|
42
|
+
const nameInput = (row: HTMLElement) =>
|
|
43
|
+
row.querySelector('input[name="contributor-name"]') as HTMLInputElement;
|
|
44
|
+
|
|
45
|
+
describe("Contributors", () => {
|
|
46
|
+
it("displays a label", () => {
|
|
47
|
+
renderContributors();
|
|
48
|
+
expect(screen.getByText("Authors and Contributors")).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("displays a list of existing contributors", () => {
|
|
52
|
+
const { container } = renderContributors();
|
|
53
|
+
const rows = existingRows(container);
|
|
54
|
+
expect(rows).toHaveLength(2);
|
|
55
|
+
|
|
56
|
+
expect(roleSelect(rows[0])).toHaveValue("Translator");
|
|
57
|
+
expect(nameInput(rows[0])).toHaveValue("A Translator");
|
|
58
|
+
expect(roleSelect(rows[1])).toHaveValue("Narrator");
|
|
59
|
+
expect(nameInput(rows[1])).toHaveValue("A Narrator");
|
|
60
|
+
|
|
61
|
+
// Each role menu offers the full set of roles.
|
|
62
|
+
const options = within(roleSelect(rows[0]))
|
|
63
|
+
.getAllByRole("option")
|
|
64
|
+
.map((o) => (o as HTMLOptionElement).value);
|
|
65
|
+
expect(options).toStrictEqual(Object.values(roles));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("displays a blank field for adding a new contributor", () => {
|
|
69
|
+
renderContributors();
|
|
70
|
+
const form = newContributorForm();
|
|
71
|
+
expect(roleSelect(form)).toHaveValue("Author");
|
|
72
|
+
expect(nameInput(form)).toHaveValue("");
|
|
73
|
+
|
|
74
|
+
const button = within(form).getByRole("button", { name: "Add" });
|
|
75
|
+
expect(button).toHaveClass("add-contributor");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("deletes a contributor", async () => {
|
|
79
|
+
const user = userEvent.setup();
|
|
80
|
+
const { container } = renderContributors();
|
|
81
|
+
|
|
82
|
+
let rows = existingRows(container);
|
|
83
|
+
expect(rows).toHaveLength(2);
|
|
84
|
+
|
|
85
|
+
// Remove the first (Translator) row.
|
|
86
|
+
await user.click(within(rows[0]).getByRole("button"));
|
|
87
|
+
|
|
88
|
+
rows = existingRows(container);
|
|
89
|
+
expect(rows).toHaveLength(1);
|
|
90
|
+
expect(roleSelect(rows[0])).toHaveValue("Narrator");
|
|
91
|
+
expect(nameInput(rows[0])).toHaveValue("A Narrator");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("adds a contributor", async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
const { container } = renderContributors();
|
|
97
|
+
expect(existingRows(container)).toHaveLength(2);
|
|
98
|
+
|
|
99
|
+
const form = newContributorForm();
|
|
100
|
+
await user.selectOptions(roleSelect(form), "Illustrator");
|
|
101
|
+
await user.type(nameInput(form), "An Illustrator");
|
|
102
|
+
await user.click(within(form).getByRole("button", { name: "Add" }));
|
|
103
|
+
|
|
104
|
+
const rows = existingRows(container);
|
|
105
|
+
expect(rows).toHaveLength(3);
|
|
106
|
+
expect(roleSelect(rows[2])).toHaveValue("Illustrator");
|
|
107
|
+
expect(nameInput(rows[2])).toHaveValue("An Illustrator");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("does not add an empty string as a contributor's name", async () => {
|
|
111
|
+
const user = userEvent.setup();
|
|
112
|
+
const { container } = renderContributors();
|
|
113
|
+
|
|
114
|
+
// The Add button starts disabled, so clicking it does nothing.
|
|
115
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
|
|
116
|
+
await user.click(screen.getByRole("button", { name: "Add" }));
|
|
117
|
+
expect(existingRows(container)).toHaveLength(2);
|
|
118
|
+
|
|
119
|
+
// Typing a name enables the button and lets us add.
|
|
120
|
+
await user.type(nameInput(newContributorForm()), "An Author");
|
|
121
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeEnabled();
|
|
122
|
+
await user.click(screen.getByRole("button", { name: "Add" }));
|
|
123
|
+
expect(existingRows(container)).toHaveLength(3);
|
|
124
|
+
|
|
125
|
+
// Clearing the name disables the button again.
|
|
126
|
+
await user.clear(nameInput(newContributorForm()));
|
|
127
|
+
expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
|
|
128
|
+
await user.click(screen.getByRole("button", { name: "Add" }));
|
|
129
|
+
expect(existingRows(container)).toHaveLength(3);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("shows the full name of a contributor's known role", () => {
|
|
133
|
+
// "trl" and "nrt" resolve to their full names in the role menus.
|
|
134
|
+
const { container } = renderContributors();
|
|
135
|
+
const rows = existingRows(container);
|
|
136
|
+
expect(roleSelect(rows[0])).toHaveValue("Translator");
|
|
137
|
+
expect(roleSelect(rows[1])).toHaveValue("Narrator");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("resets the form after adding a contributor", async () => {
|
|
141
|
+
const user = userEvent.setup();
|
|
142
|
+
renderContributors();
|
|
143
|
+
|
|
144
|
+
const form = newContributorForm();
|
|
145
|
+
await user.selectOptions(roleSelect(form), "Illustrator");
|
|
146
|
+
await user.type(nameInput(form), "An Illustrator");
|
|
147
|
+
await user.click(within(form).getByRole("button", { name: "Add" }));
|
|
148
|
+
|
|
149
|
+
// The add form is blank again, defaulted to "Author", with a disabled button.
|
|
150
|
+
const resetForm = newContributorForm();
|
|
151
|
+
expect(nameInput(resetForm)).toHaveValue("");
|
|
152
|
+
expect(roleSelect(resetForm)).toHaveValue("Author");
|
|
153
|
+
expect(
|
|
154
|
+
within(resetForm).getByRole("button", { name: "Add" })
|
|
155
|
+
).toBeDisabled();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,367 @@
|
|
|
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 CustomListEditor, {
|
|
6
|
+
CustomListEditorProps,
|
|
7
|
+
} from "../../../src/components/CustomListEditor";
|
|
8
|
+
|
|
9
|
+
// Each list entry renders a web-opds-client CatalogLink whose `collectionUrl` is
|
|
10
|
+
// the list's opdsFeedUrl. Mock it to a marker that echoes that URL, so the
|
|
11
|
+
// crawlable-feed derivation can be asserted without router/pathFor context.
|
|
12
|
+
jest.mock(
|
|
13
|
+
"@thepalaceproject/web-opds-client/lib/components/CatalogLink",
|
|
14
|
+
() => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: (props: any) => (
|
|
17
|
+
<a
|
|
18
|
+
href={props.bookUrl}
|
|
19
|
+
data-testid="catalog-link"
|
|
20
|
+
data-collection-url={props.collectionUrl}
|
|
21
|
+
>
|
|
22
|
+
{props.children}
|
|
23
|
+
</a>
|
|
24
|
+
),
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe("CustomListEditor", () => {
|
|
29
|
+
// CustomListEntriesEditor calls Element.scrollTo on mount, which JSDOM does
|
|
30
|
+
// not implement, so stub it.
|
|
31
|
+
Element.prototype.scrollTo = () => {};
|
|
32
|
+
|
|
33
|
+
const searchResults = {
|
|
34
|
+
id: "id",
|
|
35
|
+
url: "url",
|
|
36
|
+
title: "title",
|
|
37
|
+
lanes: [],
|
|
38
|
+
books: [],
|
|
39
|
+
navigationLinks: [],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const entries = {
|
|
43
|
+
baseline: [],
|
|
44
|
+
baselineTotalCount: 0,
|
|
45
|
+
added: {},
|
|
46
|
+
removed: {},
|
|
47
|
+
current: [],
|
|
48
|
+
currentTotalCount: 0,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const entryPoints = ["Book", "Audio"];
|
|
52
|
+
|
|
53
|
+
const library = {
|
|
54
|
+
uuid: "uuid",
|
|
55
|
+
name: "name",
|
|
56
|
+
short_name: "library",
|
|
57
|
+
settings: {
|
|
58
|
+
large_collections: ["eng", "fre", "spa"],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const languages = {
|
|
63
|
+
eng: ["English"],
|
|
64
|
+
spa: ["Spanish", "Castilian"],
|
|
65
|
+
fre: ["French"],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const properties = {
|
|
69
|
+
name: "Listy McList",
|
|
70
|
+
collections: [2],
|
|
71
|
+
autoUpdate: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const searchParams = {
|
|
75
|
+
entryPoint: "All",
|
|
76
|
+
terms: "",
|
|
77
|
+
sort: null,
|
|
78
|
+
language: "all",
|
|
79
|
+
advanced: {
|
|
80
|
+
include: {
|
|
81
|
+
query: null,
|
|
82
|
+
selectedQueryId: null,
|
|
83
|
+
clearFilters: null,
|
|
84
|
+
},
|
|
85
|
+
exclude: {
|
|
86
|
+
query: null,
|
|
87
|
+
selectedQueryId: null,
|
|
88
|
+
clearFilters: null,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const makeProps = (
|
|
94
|
+
overrides: Partial<CustomListEditorProps> = {}
|
|
95
|
+
): CustomListEditorProps => ({
|
|
96
|
+
entries,
|
|
97
|
+
entryPoints,
|
|
98
|
+
isFetchingMoreCustomListEntries: false,
|
|
99
|
+
isFetchingSearchResults: false,
|
|
100
|
+
isFetchingMoreSearchResults: false,
|
|
101
|
+
isLoaded: false,
|
|
102
|
+
isModified: true,
|
|
103
|
+
isOwner: true,
|
|
104
|
+
isValid: true,
|
|
105
|
+
languages,
|
|
106
|
+
library: library as CustomListEditorProps["library"],
|
|
107
|
+
listId: "1",
|
|
108
|
+
properties: properties as CustomListEditorProps["properties"],
|
|
109
|
+
searchParams: searchParams as CustomListEditorProps["searchParams"],
|
|
110
|
+
searchResults: searchResults as CustomListEditorProps["searchResults"],
|
|
111
|
+
loadMoreEntries: jest.fn(),
|
|
112
|
+
loadMoreSearchResults: jest.fn(),
|
|
113
|
+
reset: jest.fn(),
|
|
114
|
+
save: jest.fn().mockResolvedValue(undefined),
|
|
115
|
+
search: jest.fn(),
|
|
116
|
+
share: jest.fn(),
|
|
117
|
+
updateProperty: jest.fn(),
|
|
118
|
+
updateSearchParam: jest.fn(),
|
|
119
|
+
...overrides,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const renderEditor = (overrides: Partial<CustomListEditorProps> = {}) => {
|
|
123
|
+
const props = makeProps(overrides);
|
|
124
|
+
const result = render(<CustomListEditor {...props} />);
|
|
125
|
+
const rerender = (nextOverrides: Partial<CustomListEditorProps>) =>
|
|
126
|
+
result.rerender(<CustomListEditor {...makeProps(nextOverrides)} />);
|
|
127
|
+
return { ...result, rerender, props };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
it("shows the list title with an edit button", () => {
|
|
131
|
+
renderEditor();
|
|
132
|
+
|
|
133
|
+
// The editable title (TextWithEditMode) shows the name and an edit button.
|
|
134
|
+
expect(screen.getByText("Listy McList")).toBeInTheDocument();
|
|
135
|
+
expect(
|
|
136
|
+
screen.getByRole("button", { name: /Edit list title/ })
|
|
137
|
+
).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("shows the list title without an edit button when isOwner is false", () => {
|
|
141
|
+
renderEditor({ isOwner: false });
|
|
142
|
+
|
|
143
|
+
const title = screen.getByRole("heading", { level: 3 });
|
|
144
|
+
expect(title).toHaveTextContent("Listy McList");
|
|
145
|
+
expect(
|
|
146
|
+
screen.queryByRole("button", { name: /Edit list title/ })
|
|
147
|
+
).not.toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("shows the list id", () => {
|
|
151
|
+
renderEditor();
|
|
152
|
+
|
|
153
|
+
const listId = screen.getByRole("heading", { level: 4, name: /ID-1/ });
|
|
154
|
+
expect(listId).toHaveTextContent("1");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("shows an entries editor with list entries and search results", () => {
|
|
158
|
+
renderEditor();
|
|
159
|
+
|
|
160
|
+
// The entries editor renders both the search results and list-entries columns.
|
|
161
|
+
expect(
|
|
162
|
+
screen.getByRole("heading", { name: "Search Results" })
|
|
163
|
+
).toBeInTheDocument();
|
|
164
|
+
expect(
|
|
165
|
+
screen.getByRole("heading", {
|
|
166
|
+
name: /List Entries:.*No books in this list/,
|
|
167
|
+
})
|
|
168
|
+
).toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("gives each entry of a saved list a crawlable OPDS feed URL", () => {
|
|
172
|
+
// opdsFeedUrl is `${short_name}/lists/${savedName}/crawlable`, but only when
|
|
173
|
+
// the list has both a listId and a savedName (CustomListEditor.tsx:124). It
|
|
174
|
+
// reaches the DOM as the collectionUrl of each list entry's catalog link.
|
|
175
|
+
renderEditor({
|
|
176
|
+
savedName: "saved-list",
|
|
177
|
+
entries: {
|
|
178
|
+
...entries,
|
|
179
|
+
current: [{ id: "book1", title: "A Book", url: "/works/book1" }],
|
|
180
|
+
currentTotalCount: 1,
|
|
181
|
+
} as CustomListEditorProps["entries"],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(screen.getByTestId("catalog-link")).toHaveAttribute(
|
|
185
|
+
"data-collection-url",
|
|
186
|
+
"library/lists/saved-list/crawlable"
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("disables entry point options when isOwner is false", () => {
|
|
191
|
+
const { container } = renderEditor({ isOwner: false });
|
|
192
|
+
|
|
193
|
+
const entryPointsSection =
|
|
194
|
+
container.querySelector<HTMLElement>(".entry-points");
|
|
195
|
+
within(entryPointsSection)
|
|
196
|
+
.getAllByRole("radio")
|
|
197
|
+
.forEach((radio) => expect(radio).toBeDisabled());
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("shows entry point options", () => {
|
|
201
|
+
const { container } = renderEditor();
|
|
202
|
+
|
|
203
|
+
const entryPointsSection =
|
|
204
|
+
container.querySelector<HTMLElement>(".entry-points");
|
|
205
|
+
|
|
206
|
+
const all = within(entryPointsSection).getByRole("radio", { name: "All" });
|
|
207
|
+
expect(all).toHaveAttribute("value", "All");
|
|
208
|
+
expect(all).toBeChecked();
|
|
209
|
+
|
|
210
|
+
const book = within(entryPointsSection).getByRole("radio", {
|
|
211
|
+
name: "Book",
|
|
212
|
+
});
|
|
213
|
+
expect(book).toHaveAttribute("value", "Book");
|
|
214
|
+
expect(book).not.toBeChecked();
|
|
215
|
+
|
|
216
|
+
const audio = within(entryPointsSection).getByRole("radio", {
|
|
217
|
+
name: "Audio",
|
|
218
|
+
});
|
|
219
|
+
expect(audio).toHaveAttribute("value", "Audio");
|
|
220
|
+
expect(audio).not.toBeChecked();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("enables the save button when the list has changes and is valid", () => {
|
|
224
|
+
renderEditor();
|
|
225
|
+
|
|
226
|
+
expect(
|
|
227
|
+
screen.getByRole("button", { name: "Save this list" })
|
|
228
|
+
).toBeEnabled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("disables the save button when the list does not have changes", () => {
|
|
232
|
+
renderEditor({ isModified: false });
|
|
233
|
+
|
|
234
|
+
expect(
|
|
235
|
+
screen.getByRole("button", { name: "Save this list" })
|
|
236
|
+
).toBeDisabled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("disables the save button when the list is invalid", () => {
|
|
240
|
+
renderEditor({ isValid: false });
|
|
241
|
+
|
|
242
|
+
expect(
|
|
243
|
+
screen.getByRole("button", { name: "Save this list" })
|
|
244
|
+
).toBeDisabled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("calls save when the save button is clicked", async () => {
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
const save = jest.fn().mockResolvedValue(undefined);
|
|
250
|
+
renderEditor({ save });
|
|
251
|
+
|
|
252
|
+
await user.click(screen.getByRole("button", { name: "Save this list" }));
|
|
253
|
+
|
|
254
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("disables the cancel button when the list does not have changes", () => {
|
|
258
|
+
renderEditor({ isModified: false });
|
|
259
|
+
|
|
260
|
+
expect(
|
|
261
|
+
screen.getByRole("button", { name: "Cancel changes" })
|
|
262
|
+
).toBeDisabled();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("calls reset when the cancel button is clicked", async () => {
|
|
266
|
+
const user = userEvent.setup();
|
|
267
|
+
const reset = jest.fn();
|
|
268
|
+
renderEditor({ reset });
|
|
269
|
+
|
|
270
|
+
await user.click(screen.getByRole("button", { name: "Cancel changes" }));
|
|
271
|
+
|
|
272
|
+
expect(reset).toHaveBeenCalledTimes(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("does not render save/cancel buttons when isOwner is false", () => {
|
|
276
|
+
renderEditor({ isOwner: false });
|
|
277
|
+
|
|
278
|
+
expect(
|
|
279
|
+
screen.queryByRole("button", { name: "Save this list" })
|
|
280
|
+
).not.toBeInTheDocument();
|
|
281
|
+
expect(
|
|
282
|
+
screen.queryByRole("button", { name: "Cancel changes" })
|
|
283
|
+
).not.toBeInTheDocument();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("renders a CustomListSearch component", () => {
|
|
287
|
+
const { container } = renderEditor();
|
|
288
|
+
|
|
289
|
+
// CustomListSearch renders its search panel (a collapsible Panel whose
|
|
290
|
+
// header is a button) with the entry point options.
|
|
291
|
+
expect(
|
|
292
|
+
screen.getByRole("button", { name: "Search for titles" })
|
|
293
|
+
).toBeInTheDocument();
|
|
294
|
+
expect(container.querySelector(".entry-points")).toBeInTheDocument();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("updates the autoUpdate property from the search component", async () => {
|
|
298
|
+
const user = userEvent.setup();
|
|
299
|
+
// The auto-update controls only render when auto-update is enabled and the
|
|
300
|
+
// list is new (listId null), which is when the radios are interactive.
|
|
301
|
+
const updateProperty = jest.fn();
|
|
302
|
+
renderEditor({
|
|
303
|
+
updateProperty,
|
|
304
|
+
isAutoUpdateEnabled: true,
|
|
305
|
+
listId: null,
|
|
306
|
+
properties: { ...properties, autoUpdate: false },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await user.click(
|
|
310
|
+
screen.getByRole("radio", { name: "Automatically update this list" })
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
expect(updateProperty).toHaveBeenCalledTimes(1);
|
|
314
|
+
expect(updateProperty).toHaveBeenCalledWith("autoUpdate", true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("calls search when listId changes", () => {
|
|
318
|
+
const search = jest.fn();
|
|
319
|
+
const { rerender } = renderEditor({ search });
|
|
320
|
+
|
|
321
|
+
expect(search).toHaveBeenCalledTimes(1);
|
|
322
|
+
|
|
323
|
+
rerender({ search, listId: "2" });
|
|
324
|
+
|
|
325
|
+
expect(search).toHaveBeenCalledTimes(2);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("calls search when isLoaded changes", () => {
|
|
329
|
+
const search = jest.fn();
|
|
330
|
+
const { rerender } = renderEditor({ search });
|
|
331
|
+
|
|
332
|
+
expect(search).toHaveBeenCalledTimes(1);
|
|
333
|
+
|
|
334
|
+
rerender({ search, isLoaded: true });
|
|
335
|
+
|
|
336
|
+
expect(search).toHaveBeenCalledTimes(2);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("calls share when the share button is clicked", async () => {
|
|
340
|
+
const user = userEvent.setup();
|
|
341
|
+
const share = jest.fn();
|
|
342
|
+
const { container } = renderEditor({ share });
|
|
343
|
+
|
|
344
|
+
const shareButton = container.querySelector<HTMLButtonElement>(
|
|
345
|
+
".sharing-info button"
|
|
346
|
+
);
|
|
347
|
+
expect(shareButton).toBeInTheDocument();
|
|
348
|
+
|
|
349
|
+
await user.click(shareButton);
|
|
350
|
+
|
|
351
|
+
expect(share).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("does not render a share button when listId is undefined", () => {
|
|
355
|
+
const { container } = renderEditor({ listId: undefined });
|
|
356
|
+
|
|
357
|
+
expect(container.querySelector(".sharing-info")).not.toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("does not render a share button when isOwner is false", () => {
|
|
361
|
+
const { container } = renderEditor({ isOwner: false });
|
|
362
|
+
|
|
363
|
+
expect(
|
|
364
|
+
container.querySelector(".sharing-info button")
|
|
365
|
+
).not.toBeInTheDocument();
|
|
366
|
+
});
|
|
367
|
+
});
|