@thepalaceproject/circulation-admin 1.43.0 → 1.44.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/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,110 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { DiscoveryServicesData } from "../../../src/interfaces";
|
|
5
|
+
import ServiceWithRegistrationsEditForm from "../../../src/components/ServiceWithRegistrationsEditForm";
|
|
6
|
+
|
|
7
|
+
// `ServiceWithRegistrationsEditForm` is generic/abstract over its data type;
|
|
8
|
+
// the legacy test subclassed it to bind the discovery-services data type.
|
|
9
|
+
class DiscoveryServiceEditForm extends ServiceWithRegistrationsEditForm<DiscoveryServicesData> {}
|
|
10
|
+
|
|
11
|
+
const serviceData = {
|
|
12
|
+
id: 1,
|
|
13
|
+
protocol: "protocol 1",
|
|
14
|
+
};
|
|
15
|
+
const protocolsData = [
|
|
16
|
+
{
|
|
17
|
+
name: "protocol 1",
|
|
18
|
+
label: "protocol 1 label",
|
|
19
|
+
supports_registration: true,
|
|
20
|
+
settings: [],
|
|
21
|
+
library_settings: [],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const allLibraries = [
|
|
25
|
+
{ short_name: "nypl", name: "New York Public Library" },
|
|
26
|
+
{ short_name: "bpl", name: "Brooklyn Public Library" },
|
|
27
|
+
{ short_name: "qpl", name: "Queens Public Library" },
|
|
28
|
+
];
|
|
29
|
+
const libraryRegistrationsData = [
|
|
30
|
+
{
|
|
31
|
+
id: 1,
|
|
32
|
+
libraries: [
|
|
33
|
+
{ short_name: "nypl", status: "success", stage: "production" },
|
|
34
|
+
{ short_name: "bpl", status: "failure", stage: "testing" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const servicesData: any = {
|
|
39
|
+
discovery_services: [serviceData],
|
|
40
|
+
protocols: protocolsData,
|
|
41
|
+
allLibraries: allLibraries,
|
|
42
|
+
libraryRegistrations: libraryRegistrationsData,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const renderForm = (save = jest.fn()) =>
|
|
46
|
+
render(
|
|
47
|
+
<DiscoveryServiceEditForm
|
|
48
|
+
disabled={false}
|
|
49
|
+
data={servicesData}
|
|
50
|
+
item={serviceData as any}
|
|
51
|
+
save={save}
|
|
52
|
+
urlBase="url base"
|
|
53
|
+
listDataKey="discovery_services"
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
describe("ServiceWithRegistrationsEditForm", () => {
|
|
58
|
+
it("renders the underlying service edit form fields", () => {
|
|
59
|
+
renderForm();
|
|
60
|
+
|
|
61
|
+
// The base ServiceEditForm (via `super.render()`) contributes the Name text
|
|
62
|
+
// field and the Required Fields section.
|
|
63
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
64
|
+
expect(
|
|
65
|
+
screen.getByRole("heading", { name: "Required Fields" })
|
|
66
|
+
).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders a registration section reflecting each library's status", () => {
|
|
70
|
+
renderForm();
|
|
71
|
+
|
|
72
|
+
// The registration UI only renders when the protocol supports registration
|
|
73
|
+
// and the data carries libraries — i.e. those props flowed to LibraryRegistration.
|
|
74
|
+
expect(
|
|
75
|
+
screen.getByRole("heading", { name: "Register libraries" })
|
|
76
|
+
).toBeInTheDocument();
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
screen.getByRole("link", { name: "New York Public Library" })
|
|
80
|
+
).toBeInTheDocument();
|
|
81
|
+
expect(
|
|
82
|
+
screen.getByRole("link", { name: "Brooklyn Public Library" })
|
|
83
|
+
).toBeInTheDocument();
|
|
84
|
+
expect(
|
|
85
|
+
screen.getByRole("link", { name: "Queens Public Library" })
|
|
86
|
+
).toBeInTheDocument();
|
|
87
|
+
|
|
88
|
+
// Statuses come from `data.libraryRegistrations`.
|
|
89
|
+
expect(screen.getByText("Registered")).toHaveClass("bg-success");
|
|
90
|
+
expect(screen.getByText("Registration failed")).toHaveClass("bg-danger");
|
|
91
|
+
expect(screen.getByText("Not registered")).toHaveClass("bg-warning");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("enables the per-library registration actions when not disabled", () => {
|
|
95
|
+
renderForm();
|
|
96
|
+
|
|
97
|
+
// `disabled={false}` flows through to each registration button.
|
|
98
|
+
const updateButton = screen.getByRole("button", {
|
|
99
|
+
name: "Update registration",
|
|
100
|
+
});
|
|
101
|
+
const retryButton = screen.getByRole("button", {
|
|
102
|
+
name: "Retry registration",
|
|
103
|
+
});
|
|
104
|
+
const registerButton = screen.getByRole("button", { name: "Register" });
|
|
105
|
+
|
|
106
|
+
expect(updateButton).not.toBeDisabled();
|
|
107
|
+
expect(retryButton).not.toBeDisabled();
|
|
108
|
+
expect(registerButton).not.toBeDisabled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
import SetupPage from "../../../src/components/SetupPage";
|
|
6
|
+
|
|
7
|
+
describe("SetupPage", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// SetupPage renders the connected IndividualAdmins list, which fetches the
|
|
10
|
+
// admin list on mount. Stub fetch so mounting does not hit the network.
|
|
11
|
+
jest.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
12
|
+
new Response(JSON.stringify({ individualAdmins: [] }), {
|
|
13
|
+
status: 200,
|
|
14
|
+
headers: { "Content-Type": "application/json" },
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("shows the individual-admins setup (create) form", async () => {
|
|
24
|
+
renderWithProviders(<SetupPage />);
|
|
25
|
+
|
|
26
|
+
// SetupPage wires IndividualAdmins with settingUp + editOrCreate="create".
|
|
27
|
+
// That combination renders the "Welcome!" heading and the
|
|
28
|
+
// "Set up your system admin account" create-form heading; their presence in
|
|
29
|
+
// the real DOM proves the child received those props (the original shallow
|
|
30
|
+
// test asserted the props directly). No identifier is passed, so there is no
|
|
31
|
+
// per-item "Edit ..." heading — only the create form shows.
|
|
32
|
+
expect(
|
|
33
|
+
await screen.findByRole("heading", { name: "Welcome!" })
|
|
34
|
+
).toBeInTheDocument();
|
|
35
|
+
expect(
|
|
36
|
+
screen.getByRole("heading", {
|
|
37
|
+
name: "Set up your system admin account",
|
|
38
|
+
})
|
|
39
|
+
).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import SingleStatListItem, {
|
|
5
|
+
SingleStatListItemProps,
|
|
6
|
+
} from "../../../src/components/SingleStatListItem";
|
|
7
|
+
|
|
8
|
+
describe("SingleStatListItem", () => {
|
|
9
|
+
const statWithTooltip: SingleStatListItemProps = {
|
|
10
|
+
label: "Number of Widgets",
|
|
11
|
+
value: 357892,
|
|
12
|
+
tooltip: "Total Widget Count",
|
|
13
|
+
};
|
|
14
|
+
const humanReadableValue = "357.9k";
|
|
15
|
+
const formattedValue = "357,892";
|
|
16
|
+
|
|
17
|
+
it("renders a top-level <li>, with or without a tooltip", () => {
|
|
18
|
+
const { container, rerender } = render(
|
|
19
|
+
<SingleStatListItem {...statWithTooltip} />
|
|
20
|
+
);
|
|
21
|
+
expect(container.firstElementChild?.tagName).toBe("LI");
|
|
22
|
+
expect(container.firstElementChild).toHaveClass("single-stat");
|
|
23
|
+
|
|
24
|
+
// Without a tooltip the <li> is still the top-level element.
|
|
25
|
+
rerender(
|
|
26
|
+
<SingleStatListItem
|
|
27
|
+
label={statWithTooltip.label}
|
|
28
|
+
value={statWithTooltip.value}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
expect(container.firstElementChild?.tagName).toBe("LI");
|
|
32
|
+
expect(container.firstElementChild).toHaveClass("single-stat");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("shows a tooltip wrapping the value and label when one is provided", () => {
|
|
36
|
+
const { container } = render(<SingleStatListItem {...statWithTooltip} />);
|
|
37
|
+
|
|
38
|
+
const tooltip = container.querySelector('[data-toggle="tooltip"]');
|
|
39
|
+
expect(tooltip).not.toBeNull();
|
|
40
|
+
|
|
41
|
+
const title = tooltip?.getAttribute("title") ?? "";
|
|
42
|
+
expect(title).toContain(statWithTooltip.tooltip);
|
|
43
|
+
expect(title).toContain(formattedValue);
|
|
44
|
+
|
|
45
|
+
// When there's a tooltip, it wraps the (rounded) value and the label.
|
|
46
|
+
expect(tooltip).toHaveTextContent(humanReadableValue);
|
|
47
|
+
expect(tooltip).toHaveTextContent(statWithTooltip.label);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("omits the tooltip when none is provided, showing value and label directly", () => {
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<SingleStatListItem
|
|
53
|
+
label={statWithTooltip.label}
|
|
54
|
+
value={statWithTooltip.value}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(container.querySelector('[data-toggle="tooltip"]')).toBeNull();
|
|
59
|
+
expect(screen.getByText(humanReadableValue)).toBeInTheDocument();
|
|
60
|
+
expect(screen.getByText(statWithTooltip.label)).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { installFormDataShim, FormDataShim } from "../testUtils/formDataShim";
|
|
3
|
+
import { screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
7
|
+
import buildStore from "../../../src/store";
|
|
8
|
+
// Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
|
|
9
|
+
// are exercised. The panel fetches its announcements on mount; the fetched data
|
|
10
|
+
// flows back in through the Redux store.
|
|
11
|
+
import SitewideAnnouncements from "../../../src/components/SitewideAnnouncements";
|
|
12
|
+
|
|
13
|
+
const data = {
|
|
14
|
+
announcements: [
|
|
15
|
+
{
|
|
16
|
+
id: "dcf036d9-106b-426c-838f-ce7c57d53801",
|
|
17
|
+
content: "There is nothing to see here.",
|
|
18
|
+
start: "2022-08-31",
|
|
19
|
+
finish: "2022-10-31",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
settings: [
|
|
23
|
+
{
|
|
24
|
+
category: "Announcements",
|
|
25
|
+
description: "Announcements will be displayed to authenticated patrons.",
|
|
26
|
+
key: "global_announcements",
|
|
27
|
+
label: "Scheduled announcements",
|
|
28
|
+
level: 3,
|
|
29
|
+
type: "announcements",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
installFormDataShim();
|
|
34
|
+
|
|
35
|
+
const jsonResponse = (body: unknown) =>
|
|
36
|
+
new Response(JSON.stringify(body), {
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const renderConnected = (
|
|
42
|
+
element: React.ReactElement,
|
|
43
|
+
appConfigSettings: Record<string, unknown> = {}
|
|
44
|
+
) =>
|
|
45
|
+
renderWithProviders(element, {
|
|
46
|
+
reduxProviderProps: { store: buildStore() },
|
|
47
|
+
appConfigSettings,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("SitewideAnnouncements", () => {
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
jest.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders a form containing the announcements section", async () => {
|
|
56
|
+
jest
|
|
57
|
+
.spyOn(globalThis, "fetch")
|
|
58
|
+
.mockImplementation(async () => jsonResponse(data));
|
|
59
|
+
const { container } = renderConnected(
|
|
60
|
+
<SitewideAnnouncements csrfToken="token" />
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Once the on-mount fetch resolves, the form renders with the announcement
|
|
64
|
+
// supplied through the store (proving the AnnouncementsSection received the
|
|
65
|
+
// data as its value).
|
|
66
|
+
expect(
|
|
67
|
+
await screen.findByText("There is nothing to see here.")
|
|
68
|
+
).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByText("Scheduled Announcements:")).toBeInTheDocument();
|
|
70
|
+
expect(container.querySelector("form")).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("renders a loading indicator instead of a form while data is loading", () => {
|
|
74
|
+
// A never-resolving fetch keeps the panel in its fetching state.
|
|
75
|
+
jest
|
|
76
|
+
.spyOn(globalThis, "fetch")
|
|
77
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
78
|
+
const { container } = renderConnected(
|
|
79
|
+
<SitewideAnnouncements csrfToken="token" />
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(container.querySelector(".loading")).toBeInTheDocument();
|
|
83
|
+
expect(container.querySelector("form")).not.toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("submits the announcements to editItem when the form is submitted", async () => {
|
|
87
|
+
const user = userEvent.setup();
|
|
88
|
+
const fetchSpy = jest
|
|
89
|
+
.spyOn(globalThis, "fetch")
|
|
90
|
+
.mockImplementation(async (_url, opts?: RequestInit) => {
|
|
91
|
+
if (opts?.method === "POST") {
|
|
92
|
+
return new Response("success", { status: 200 });
|
|
93
|
+
}
|
|
94
|
+
return jsonResponse(data);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// System admin so the settings' required permission level is satisfied and
|
|
98
|
+
// the submit button is enabled.
|
|
99
|
+
renderConnected(<SitewideAnnouncements csrfToken="token" />, {
|
|
100
|
+
roles: [{ role: "system" }],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await screen.findByText("There is nothing to see here.");
|
|
104
|
+
|
|
105
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
106
|
+
|
|
107
|
+
// The submit dispatches editSitewideAnnouncements, which POSTs the form.
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
const postCall = fetchSpy.mock.calls.find(
|
|
110
|
+
([, opts]) => (opts as RequestInit)?.method === "POST"
|
|
111
|
+
);
|
|
112
|
+
expect(postCall).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const postCall = fetchSpy.mock.calls.find(
|
|
116
|
+
([, opts]) => (opts as RequestInit)?.method === "POST"
|
|
117
|
+
);
|
|
118
|
+
const body = (postCall[1] as RequestInit).body as unknown as FormDataShim;
|
|
119
|
+
expect(JSON.parse(body.get("announcements"))).toStrictEqual(
|
|
120
|
+
data.announcements
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("disables the submit button when the admin lacks the required permission level", async () => {
|
|
125
|
+
jest
|
|
126
|
+
.spyOn(globalThis, "fetch")
|
|
127
|
+
.mockImplementation(async () => jsonResponse(data));
|
|
128
|
+
// No roles => admin level 1, below the settings' required level of 3.
|
|
129
|
+
renderConnected(<SitewideAnnouncements csrfToken="token" />);
|
|
130
|
+
|
|
131
|
+
await screen.findByText("There is nothing to see here.");
|
|
132
|
+
expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -28,13 +28,6 @@ const normalizedData = normalizeStatistics(statisticsApiResponseData);
|
|
|
28
28
|
global.ResizeObserver = require("resize-observer-polyfill");
|
|
29
29
|
|
|
30
30
|
describe("Dashboard Statistics", () => {
|
|
31
|
-
// NB: This adds test to the already existing tests in:
|
|
32
|
-
// - `src/components/__tests__/LibraryStats-test.tsx`.
|
|
33
|
-
// - `src/components/__tests__/SingleStatListItem-test.tsx`.
|
|
34
|
-
//
|
|
35
|
-
// Those tests should eventually be migrated here and
|
|
36
|
-
// adapted to the Jest/React Testing Library paradigm.
|
|
37
|
-
|
|
38
31
|
// Configure standard constructors so that RTK Query works in tests with FetchMockJest
|
|
39
32
|
Object.assign(fetchMock.config, {
|
|
40
33
|
fetch,
|
|
@@ -382,14 +375,10 @@ describe("Dashboard Statistics", () => {
|
|
|
382
375
|
roles,
|
|
383
376
|
quicksightPagePath: fakeQuickSightHref,
|
|
384
377
|
};
|
|
385
|
-
const {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
queryByText,
|
|
390
|
-
} = renderWithProviders(<Stats library={sampleLibraryKey} />, {
|
|
391
|
-
appConfigSettings,
|
|
392
|
-
});
|
|
378
|
+
const { container, getByRole, queryByRole, queryByText } =
|
|
379
|
+
renderWithProviders(<Stats library={sampleLibraryKey} />, {
|
|
380
|
+
appConfigSettings,
|
|
381
|
+
});
|
|
393
382
|
|
|
394
383
|
// We should always render a Usage reports group when a library is specified.
|
|
395
384
|
getByRole("heading", {
|
|
@@ -0,0 +1,68 @@
|
|
|
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 {
|
|
6
|
+
TabContainer,
|
|
7
|
+
TabContainerProps,
|
|
8
|
+
} from "../../../src/components/TabContainer";
|
|
9
|
+
|
|
10
|
+
describe("TabContainer", () => {
|
|
11
|
+
let handleSelectSpy: jest.Mock;
|
|
12
|
+
|
|
13
|
+
// A concrete subclass of the abstract TabContainer, mirroring the legacy test's
|
|
14
|
+
// MockTabContainer: it supplies two trivial tabs and records the selected tab
|
|
15
|
+
// key when handleSelect runs.
|
|
16
|
+
class MockTabContainer extends TabContainer<TabContainerProps> {
|
|
17
|
+
handleSelect(event) {
|
|
18
|
+
handleSelectSpy(event.target.dataset.tabkey);
|
|
19
|
+
}
|
|
20
|
+
tabs() {
|
|
21
|
+
return {
|
|
22
|
+
tab1: <div className="tab1" />,
|
|
23
|
+
tab2: <div className="tab2" />,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const renderContainer = (tab: string = null) =>
|
|
29
|
+
render(
|
|
30
|
+
<MockTabContainer tab={tab} csrfToken="token">
|
|
31
|
+
<div className="test-child">Moby Dick</div>
|
|
32
|
+
</MockTabContainer>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
handleSelectSpy = jest.fn();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("shows nav tabs", () => {
|
|
40
|
+
const { container } = renderContainer();
|
|
41
|
+
const items = container.querySelectorAll("ul.nav-tabs li");
|
|
42
|
+
const linkTexts = Array.from(items).map((li) => li.textContent);
|
|
43
|
+
expect(linkTexts).toContain("Tab1");
|
|
44
|
+
expect(linkTexts).toContain("Tab2");
|
|
45
|
+
expect(items[0]).toHaveClass("active");
|
|
46
|
+
expect(items[1]).not.toHaveClass("active");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("shows default tab content", () => {
|
|
50
|
+
const { container } = renderContainer();
|
|
51
|
+
expect(container.querySelector("#tab1")).toHaveStyle({ display: "block" });
|
|
52
|
+
expect(container.querySelector("#tab2")).toHaveStyle({ display: "none" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("shows tab content from tab in props", () => {
|
|
56
|
+
const { container } = renderContainer("tab2");
|
|
57
|
+
expect(container.querySelector("#tab1")).toHaveStyle({ display: "none" });
|
|
58
|
+
expect(container.querySelector("#tab2")).toHaveStyle({ display: "block" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("uses handleSelect to navigate when tab is clicked", async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
renderContainer();
|
|
64
|
+
await user.click(screen.getByRole("link", { name: "Tab2" }));
|
|
65
|
+
expect(handleSelectSpy).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(handleSelectSpy).toHaveBeenCalledWith("tab2");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, act } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import TextWithEditMode from "../../../src/components/TextWithEditMode";
|
|
5
|
+
|
|
6
|
+
describe("TextWithEditMode", () => {
|
|
7
|
+
it("renders text", () => {
|
|
8
|
+
const { rerender } = render(
|
|
9
|
+
<TextWithEditMode
|
|
10
|
+
text="test"
|
|
11
|
+
placeholder="editable thing"
|
|
12
|
+
aria-label="label"
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getByText("test")).toBeInTheDocument();
|
|
16
|
+
expect(
|
|
17
|
+
screen.getByRole("button", { name: /Edit editable thing/ })
|
|
18
|
+
).toBeInTheDocument();
|
|
19
|
+
|
|
20
|
+
rerender(
|
|
21
|
+
<TextWithEditMode
|
|
22
|
+
text="other text"
|
|
23
|
+
placeholder="editable thing"
|
|
24
|
+
aria-label="label"
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
expect(screen.queryByText("test")).not.toBeInTheDocument();
|
|
28
|
+
expect(screen.getByText("other text")).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("starts in edit mode if there's no text", () => {
|
|
32
|
+
render(
|
|
33
|
+
<TextWithEditMode placeholder="editable thing" aria-label="label" />
|
|
34
|
+
);
|
|
35
|
+
const input = screen.getByRole("textbox");
|
|
36
|
+
expect(input).toHaveAttribute("placeholder", "editable thing");
|
|
37
|
+
expect(input).toHaveValue("");
|
|
38
|
+
expect(
|
|
39
|
+
screen.getByRole("button", { name: /Save editable thing/ })
|
|
40
|
+
).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("switches to edit mode", async () => {
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
render(
|
|
46
|
+
<TextWithEditMode
|
|
47
|
+
text="test"
|
|
48
|
+
placeholder="editable thing"
|
|
49
|
+
aria-label="label"
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
|
|
53
|
+
|
|
54
|
+
await user.click(
|
|
55
|
+
screen.getByRole("button", { name: /Edit editable thing/ })
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const input = screen.getByRole("textbox");
|
|
59
|
+
expect(input).toHaveAttribute("placeholder", "editable thing");
|
|
60
|
+
expect(input).toHaveValue("test");
|
|
61
|
+
expect(screen.getByRole("button", { name: /Save/ })).toBeInTheDocument();
|
|
62
|
+
expect(
|
|
63
|
+
screen.queryByRole("button", { name: /Edit/ })
|
|
64
|
+
).not.toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("switches out of edit mode", async () => {
|
|
68
|
+
const user = userEvent.setup();
|
|
69
|
+
const onUpdate = jest.fn();
|
|
70
|
+
render(
|
|
71
|
+
<TextWithEditMode
|
|
72
|
+
placeholder="editable thing"
|
|
73
|
+
onUpdate={onUpdate}
|
|
74
|
+
aria-label="label"
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
// Type the new value into the field; the component reads the real input
|
|
78
|
+
// value back through its ref on save (no prototype stubbing).
|
|
79
|
+
await user.type(screen.getByRole("textbox"), "new value");
|
|
80
|
+
await user.click(
|
|
81
|
+
screen.getByRole("button", { name: /Save editable thing/ })
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
|
|
85
|
+
expect(onUpdate).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(onUpdate).toHaveBeenCalledWith("new value");
|
|
87
|
+
expect(screen.getByText("new value")).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByRole("button", { name: /Edit/ })).toBeInTheDocument();
|
|
89
|
+
expect(
|
|
90
|
+
screen.queryByRole("button", { name: /Save/ })
|
|
91
|
+
).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("gets text via the ref API", async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
// In display mode, getText() returns the current text.
|
|
97
|
+
const displayRef = React.createRef<TextWithEditMode>();
|
|
98
|
+
const { unmount } = render(
|
|
99
|
+
<TextWithEditMode
|
|
100
|
+
text="test"
|
|
101
|
+
placeholder="editable thing"
|
|
102
|
+
aria-label="label"
|
|
103
|
+
ref={displayRef}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
expect(displayRef.current!.getText()).toBe("test");
|
|
107
|
+
unmount();
|
|
108
|
+
|
|
109
|
+
// From edit mode, getText() returns the current input value and exits edit mode.
|
|
110
|
+
const editRef = React.createRef<TextWithEditMode>();
|
|
111
|
+
render(
|
|
112
|
+
<TextWithEditMode
|
|
113
|
+
placeholder="editable thing"
|
|
114
|
+
aria-label="label"
|
|
115
|
+
ref={editRef}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
await user.type(screen.getByRole("textbox"), "new value");
|
|
119
|
+
let value = "";
|
|
120
|
+
act(() => {
|
|
121
|
+
value = editRef.current!.getText();
|
|
122
|
+
});
|
|
123
|
+
expect(value).toBe("new value");
|
|
124
|
+
expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("resets via the ref API", async () => {
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
const onUpdate = jest.fn();
|
|
130
|
+
|
|
131
|
+
// With no text prop, reset() returns to a blank edit mode.
|
|
132
|
+
const blankRef = React.createRef<TextWithEditMode>();
|
|
133
|
+
const { unmount } = render(
|
|
134
|
+
<TextWithEditMode
|
|
135
|
+
placeholder="editable thing"
|
|
136
|
+
onUpdate={onUpdate}
|
|
137
|
+
aria-label="label"
|
|
138
|
+
ref={blankRef}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
await user.type(screen.getByRole("textbox"), "new value");
|
|
142
|
+
await user.click(
|
|
143
|
+
screen.getByRole("button", { name: /Save editable thing/ })
|
|
144
|
+
);
|
|
145
|
+
expect(screen.getByText("new value")).toBeInTheDocument();
|
|
146
|
+
expect(onUpdate).toHaveBeenCalledTimes(1);
|
|
147
|
+
|
|
148
|
+
act(() => {
|
|
149
|
+
blankRef.current!.reset();
|
|
150
|
+
});
|
|
151
|
+
expect(screen.queryByText("new value")).not.toBeInTheDocument();
|
|
152
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
153
|
+
expect(onUpdate).toHaveBeenCalledTimes(2);
|
|
154
|
+
expect(onUpdate.mock.calls[1][0]).toBeUndefined();
|
|
155
|
+
unmount();
|
|
156
|
+
|
|
157
|
+
// With a text prop, reset() returns to displaying that text.
|
|
158
|
+
const textRef = React.createRef<TextWithEditMode>();
|
|
159
|
+
render(
|
|
160
|
+
<TextWithEditMode
|
|
161
|
+
text="test"
|
|
162
|
+
placeholder="editable thing"
|
|
163
|
+
onUpdate={onUpdate}
|
|
164
|
+
aria-label="label"
|
|
165
|
+
ref={textRef}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
await user.click(
|
|
169
|
+
screen.getByRole("button", { name: /Edit editable thing/ })
|
|
170
|
+
);
|
|
171
|
+
const input = screen.getByRole("textbox");
|
|
172
|
+
await user.clear(input);
|
|
173
|
+
await user.type(input, "new value");
|
|
174
|
+
await user.click(
|
|
175
|
+
screen.getByRole("button", { name: /Save editable thing/ })
|
|
176
|
+
);
|
|
177
|
+
expect(screen.queryByText("test")).not.toBeInTheDocument();
|
|
178
|
+
expect(screen.getByText("new value")).toBeInTheDocument();
|
|
179
|
+
expect(onUpdate).toHaveBeenCalledTimes(3);
|
|
180
|
+
|
|
181
|
+
act(() => {
|
|
182
|
+
textRef.current!.reset();
|
|
183
|
+
});
|
|
184
|
+
expect(screen.queryByText("new value")).not.toBeInTheDocument();
|
|
185
|
+
expect(screen.getByText("test")).toBeInTheDocument();
|
|
186
|
+
expect(onUpdate).toHaveBeenCalledTimes(4);
|
|
187
|
+
expect(onUpdate.mock.calls[3][0]).toBe("test");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("optionally disables the save button if the text is blank", async () => {
|
|
191
|
+
const user = userEvent.setup();
|
|
192
|
+
render(
|
|
193
|
+
<TextWithEditMode
|
|
194
|
+
placeholder="editable thing"
|
|
195
|
+
aria-label="label"
|
|
196
|
+
disableIfBlank
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
const saveButton = screen.getByRole("button", {
|
|
200
|
+
name: /Save editable thing/,
|
|
201
|
+
});
|
|
202
|
+
expect(saveButton).toBeDisabled();
|
|
203
|
+
|
|
204
|
+
await user.type(screen.getByRole("textbox"), "test");
|
|
205
|
+
expect(saveButton).toBeEnabled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("updates the displayed value as the user types", async () => {
|
|
209
|
+
const user = userEvent.setup();
|
|
210
|
+
render(
|
|
211
|
+
<TextWithEditMode placeholder="editable thing" aria-label="label" />
|
|
212
|
+
);
|
|
213
|
+
const input = screen.getByRole("textbox");
|
|
214
|
+
expect(input).toHaveValue("");
|
|
215
|
+
await user.type(input, "test");
|
|
216
|
+
expect(input).toHaveValue("test");
|
|
217
|
+
});
|
|
218
|
+
});
|