@thepalaceproject/circulation-admin 1.43.0-post.7 → 1.43.0-post.9
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/package.json +1 -1
- 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/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/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/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/index.test.tsx +50 -0
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +0 -38
|
@@ -304,11 +304,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
304
304
|
const NO_EMAIL_CONTEXT_PROVIDER_PROPS = { email: undefined };
|
|
305
305
|
|
|
306
306
|
it("shows the modal when `show` is true", async () => {
|
|
307
|
-
const {
|
|
308
|
-
getAllByRole,
|
|
309
|
-
queryAllByRole,
|
|
310
|
-
rerender,
|
|
311
|
-
} = renderWithProviders(
|
|
307
|
+
const { getAllByRole, queryAllByRole, rerender } = renderWithProviders(
|
|
312
308
|
<InventoryReportRequestModal
|
|
313
309
|
show={false}
|
|
314
310
|
onHide={onHide}
|
|
@@ -385,19 +381,15 @@ describe("InventoryReportRequestModal", () => {
|
|
|
385
381
|
let show = true;
|
|
386
382
|
onHide.mockImplementation(() => (show = false));
|
|
387
383
|
|
|
388
|
-
const {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
library={LIBRARY}
|
|
398
|
-
/>,
|
|
399
|
-
{ queryClient }
|
|
400
|
-
);
|
|
384
|
+
const { getAllByRole, getByRole, queryAllByRole, rerender } =
|
|
385
|
+
renderWithProviders(
|
|
386
|
+
<InventoryReportRequestModal
|
|
387
|
+
show={show}
|
|
388
|
+
onHide={onHide}
|
|
389
|
+
library={LIBRARY}
|
|
390
|
+
/>,
|
|
391
|
+
{ queryClient }
|
|
392
|
+
);
|
|
401
393
|
getAllByRole("dialog");
|
|
402
394
|
expect(show).toBe(true);
|
|
403
395
|
expect(onHide).not.toHaveBeenCalled();
|
|
@@ -420,10 +412,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
420
412
|
});
|
|
421
413
|
|
|
422
414
|
it("updates confirmation message, if report info request arrives in time", async () => {
|
|
423
|
-
const {
|
|
424
|
-
getByRole,
|
|
425
|
-
rerender,
|
|
426
|
-
} = renderWithProviders(
|
|
415
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
427
416
|
<InventoryReportRequestModal
|
|
428
417
|
show={true}
|
|
429
418
|
onHide={onHide}
|
|
@@ -457,10 +446,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
457
446
|
)
|
|
458
447
|
);
|
|
459
448
|
|
|
460
|
-
const {
|
|
461
|
-
getByRole,
|
|
462
|
-
rerender,
|
|
463
|
-
} = renderWithProviders(
|
|
449
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
464
450
|
<InventoryReportRequestModal
|
|
465
451
|
show={true}
|
|
466
452
|
onHide={onHide}
|
|
@@ -505,10 +491,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
505
491
|
)
|
|
506
492
|
);
|
|
507
493
|
|
|
508
|
-
const {
|
|
509
|
-
getByRole,
|
|
510
|
-
rerender,
|
|
511
|
-
} = renderWithProviders(
|
|
494
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
512
495
|
<InventoryReportRequestModal
|
|
513
496
|
show={true}
|
|
514
497
|
onHide={onHide}
|
|
@@ -538,10 +521,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
538
521
|
it("requests report generation, if request confirmed", async () => {
|
|
539
522
|
const user = userEvent.setup();
|
|
540
523
|
|
|
541
|
-
const {
|
|
542
|
-
getByRole,
|
|
543
|
-
rerender,
|
|
544
|
-
} = renderWithProviders(
|
|
524
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
545
525
|
<InventoryReportRequestModal
|
|
546
526
|
show={true}
|
|
547
527
|
onHide={onHide}
|
|
@@ -574,10 +554,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
574
554
|
it("displays success message, when request is successful", async () => {
|
|
575
555
|
const user = userEvent.setup();
|
|
576
556
|
|
|
577
|
-
const {
|
|
578
|
-
getByRole,
|
|
579
|
-
rerender,
|
|
580
|
-
} = renderWithProviders(
|
|
557
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
581
558
|
<InventoryReportRequestModal
|
|
582
559
|
show={true}
|
|
583
560
|
onHide={onHide}
|
|
@@ -615,10 +592,7 @@ describe("InventoryReportRequestModal", () => {
|
|
|
615
592
|
);
|
|
616
593
|
const user = userEvent.setup();
|
|
617
594
|
|
|
618
|
-
const {
|
|
619
|
-
getByRole,
|
|
620
|
-
rerender,
|
|
621
|
-
} = renderWithProviders(
|
|
595
|
+
const { getByRole, rerender } = renderWithProviders(
|
|
622
596
|
<InventoryReportRequestModal
|
|
623
597
|
show={true}
|
|
624
598
|
onHide={onHide}
|
|
@@ -19,13 +19,9 @@ const customListsData = [
|
|
|
19
19
|
{ id: 1, name: "list 1", entries: [], is_owner: true, is_shared: false },
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
-
const editLane = stub().returns(
|
|
23
|
-
new Promise<void>((resolve) => resolve())
|
|
24
|
-
);
|
|
22
|
+
const editLane = stub().returns(new Promise<void>((resolve) => resolve()));
|
|
25
23
|
|
|
26
|
-
const deleteLane = stub().returns(
|
|
27
|
-
new Promise<void>((resolve) => resolve())
|
|
28
|
-
);
|
|
24
|
+
const deleteLane = stub().returns(new Promise<void>((resolve) => resolve()));
|
|
29
25
|
|
|
30
26
|
const toggleLaneVisibility = stub();
|
|
31
27
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen, waitForElementToBeRemoved } from "@testing-library/react";
|
|
3
|
+
import * as fetchMock from "fetch-mock-jest";
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
|
+
|
|
7
|
+
// Header and Footer are page chrome; Header pulls in the react-router v3 context
|
|
8
|
+
// that renderWithProviders does not supply. Mock them to markers.
|
|
9
|
+
jest.mock("../../../src/components/Header", () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: () => <div data-testid="header" />,
|
|
12
|
+
}));
|
|
13
|
+
jest.mock("../../../src/components/Footer", () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: () => <div data-testid="footer" />,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// The real Lanes child (and its sidebar/editor) renders React Router `Link`s,
|
|
19
|
+
// which need router context renderWithProviders does not supply. Mock `Link` to
|
|
20
|
+
// a marker, matching Lane.test.tsx.
|
|
21
|
+
jest.mock("react-router", () => ({
|
|
22
|
+
...jest.requireActual("react-router"),
|
|
23
|
+
Link: (props) => <div data-testid="Link">{props.children}</div>,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import LanePage from "../../../src/components/LanePage";
|
|
27
|
+
|
|
28
|
+
describe("LanePage", () => {
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
Object.assign(fetchMock.config, { fetch, Headers, Request, Response });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// Lanes fetches lanes and custom lists on mount; stub the network so no real
|
|
35
|
+
// request is made.
|
|
36
|
+
fetchMock.mock("*", { lanes: [], custom_lists: [] });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
fetchMock.restore();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("shows lanes with info from params", async () => {
|
|
44
|
+
const params = {
|
|
45
|
+
library: "library",
|
|
46
|
+
editOrCreate: "edit",
|
|
47
|
+
identifier: "5",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const { container } = renderWithProviders(<LanePage params={params} />);
|
|
51
|
+
|
|
52
|
+
// Header and Footer chrome are composed around the body.
|
|
53
|
+
expect(screen.getByTestId("header")).toBeInTheDocument();
|
|
54
|
+
expect(screen.getByTestId("footer")).toBeInTheDocument();
|
|
55
|
+
|
|
56
|
+
// The real Lanes child renders its container...
|
|
57
|
+
expect(container.querySelector("main.lanes-container")).toBeInTheDocument();
|
|
58
|
+
// ...and, because editOrCreate is "edit", its lane editor panel.
|
|
59
|
+
expect(container.querySelector(".lane-editor")).toBeInTheDocument();
|
|
60
|
+
|
|
61
|
+
// While its on-mount fetches (lanes and custom lists) are in flight, Lanes
|
|
62
|
+
// shows a loading indicator. Wait for it to clear so those fetches settle
|
|
63
|
+
// and their state updates flush, instead of racing a fixed-length timer.
|
|
64
|
+
await waitForElementToBeRemoved(() =>
|
|
65
|
+
screen.queryByRole("dialog", { name: "Loading" })
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import LanguageField from "../../../src/components/LanguageField";
|
|
5
|
+
|
|
6
|
+
describe("LanguageField", () => {
|
|
7
|
+
const languages = {
|
|
8
|
+
eng: ["English"],
|
|
9
|
+
es: ["Spanish", "Castilian"],
|
|
10
|
+
spa: ["Spanish", "Castilian"],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it("renders an autocomplete field", () => {
|
|
14
|
+
const { container } = render(
|
|
15
|
+
<LanguageField name="language" languages={languages} />
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// An <input> with a `list` attribute has the implicit ARIA role "combobox".
|
|
19
|
+
const input = screen.getByRole("combobox");
|
|
20
|
+
expect(input).toHaveAttribute("name", "language");
|
|
21
|
+
expect(input).toHaveAttribute("list", "language-autocomplete-list");
|
|
22
|
+
expect(
|
|
23
|
+
container.querySelector("datalist#language-autocomplete-list")
|
|
24
|
+
).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders a unique, ordered list of language names as options", () => {
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<LanguageField name="language" languages={languages} />
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const options = Array.from(
|
|
33
|
+
container.querySelectorAll<HTMLOptionElement>(
|
|
34
|
+
"#language-autocomplete-list option"
|
|
35
|
+
)
|
|
36
|
+
).map((option) => option.value);
|
|
37
|
+
|
|
38
|
+
expect(options).toStrictEqual(["English", "Spanish", "Castilian"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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 LoadButton from "../../../src/components/LoadButton";
|
|
6
|
+
|
|
7
|
+
describe("LoadButton", () => {
|
|
8
|
+
it("displays 'Load more' text when not fetching", () => {
|
|
9
|
+
render(<LoadButton isFetching={false} loadMore={jest.fn()} />);
|
|
10
|
+
|
|
11
|
+
expect(
|
|
12
|
+
screen.getByRole("button", { name: "Load more" })
|
|
13
|
+
).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders the MoreDotsIcon only while fetching", () => {
|
|
17
|
+
const { container, rerender } = render(
|
|
18
|
+
<LoadButton isFetching={false} loadMore={jest.fn()} />
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// The MoreDotsIcon is a Font Awesome-style <svg>, so query for it directly.
|
|
22
|
+
expect(container.querySelector("svg")).not.toBeInTheDocument();
|
|
23
|
+
|
|
24
|
+
rerender(<LoadButton isFetching={true} loadMore={jest.fn()} />);
|
|
25
|
+
|
|
26
|
+
expect(container.querySelector("svg")).toBeInTheDocument();
|
|
27
|
+
expect(screen.queryByText("Load more")).not.toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("calls loadMore when clicked", async () => {
|
|
31
|
+
const user = userEvent.setup();
|
|
32
|
+
const loadMore = jest.fn();
|
|
33
|
+
render(<LoadButton isFetching={false} loadMore={loadMore} />);
|
|
34
|
+
|
|
35
|
+
await user.click(screen.getByRole("button"));
|
|
36
|
+
|
|
37
|
+
expect(loadMore).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("does nothing when disabled", async () => {
|
|
41
|
+
const user = userEvent.setup();
|
|
42
|
+
const loadMore = jest.fn();
|
|
43
|
+
render(<LoadButton isFetching={true} loadMore={loadMore} />);
|
|
44
|
+
|
|
45
|
+
await user.click(screen.getByRole("button"));
|
|
46
|
+
|
|
47
|
+
expect(loadMore).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
5
|
+
|
|
6
|
+
// Header and Footer are page chrome; Header pulls in the react-router v3 context
|
|
7
|
+
// that renderWithProviders does not supply. Mock them to markers.
|
|
8
|
+
jest.mock("../../../src/components/Header", () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
default: () => <div data-testid="header" />,
|
|
11
|
+
}));
|
|
12
|
+
jest.mock("../../../src/components/Footer", () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: () => <div data-testid="footer" />,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import ManagePatrons from "../../../src/components/ManagePatrons";
|
|
18
|
+
|
|
19
|
+
describe("ManagePatrons", () => {
|
|
20
|
+
it("shows Header", () => {
|
|
21
|
+
renderWithProviders(<ManagePatrons params={{ library: "", tab: "" }} />);
|
|
22
|
+
|
|
23
|
+
expect(screen.getByTestId("header")).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should have a .manage-patrons-page class", () => {
|
|
27
|
+
const { container } = renderWithProviders(
|
|
28
|
+
<ManagePatrons params={{ library: "", tab: "" }} />
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(container.querySelector(".manage-patrons-page")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("shows heading", () => {
|
|
35
|
+
renderWithProviders(<ManagePatrons params={{ library: "", tab: "" }} />);
|
|
36
|
+
|
|
37
|
+
expect(
|
|
38
|
+
screen.getByRole("heading", { name: "Patron Manager" })
|
|
39
|
+
).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("shows ManagePatronsTabContainer", () => {
|
|
43
|
+
// A library manager for "NYPL" sees a tab per patron action, which proves
|
|
44
|
+
// both the library and tab props reached the tab container.
|
|
45
|
+
const { container } = renderWithProviders(
|
|
46
|
+
<ManagePatrons params={{ library: "NYPL", tab: "resetAdobeId" }} />,
|
|
47
|
+
{ appConfigSettings: { roles: [{ role: "manager", library: "NYPL" }] } }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const tabContainer = container.querySelector(".tab-container");
|
|
51
|
+
expect(tabContainer).toBeInTheDocument();
|
|
52
|
+
|
|
53
|
+
// The tab <li>s carry role="presentation", so query them directly.
|
|
54
|
+
const tabs = (tabContainer as HTMLElement).querySelectorAll(
|
|
55
|
+
"ul.nav-tabs > li"
|
|
56
|
+
);
|
|
57
|
+
expect(tabs).toHaveLength(2);
|
|
58
|
+
expect(
|
|
59
|
+
screen.getByRole("link", { name: "Reset Adobe ID" })
|
|
60
|
+
).toBeInTheDocument();
|
|
61
|
+
expect(
|
|
62
|
+
screen.getByRole("link", { name: "Debug Authentication" })
|
|
63
|
+
).toBeInTheDocument();
|
|
64
|
+
// The `tab` prop selects the active tab.
|
|
65
|
+
expect(tabs[0]).toHaveClass("active");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("shows Footer", () => {
|
|
69
|
+
renderWithProviders(<ManagePatrons params={{ library: "", tab: "" }} />);
|
|
70
|
+
|
|
71
|
+
expect(screen.getByTestId("footer")).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
|
+
import { installFormDataShim } from "../testUtils/formDataShim";
|
|
7
|
+
import buildStore from "../../../src/store";
|
|
8
|
+
import { ManagePatronsForm } from "../../../src/components/ManagePatronsForm";
|
|
9
|
+
import { PatronData } from "../../../src/interfaces";
|
|
10
|
+
|
|
11
|
+
// The reusable-components `Form` builds `new FormData(formElement)` on submit,
|
|
12
|
+
// which the unit jsdom environment's undici FormData rejects; install the shared
|
|
13
|
+
// shim that reads the form's successful controls.
|
|
14
|
+
installFormDataShim();
|
|
15
|
+
|
|
16
|
+
const patron: PatronData = {
|
|
17
|
+
authorization_expires: "",
|
|
18
|
+
username: "User Name",
|
|
19
|
+
personal_name: "Personal Name",
|
|
20
|
+
email_address: "user@email.com",
|
|
21
|
+
authorization_identifier: "1234",
|
|
22
|
+
authorization_identifiers: [""],
|
|
23
|
+
block_reason: "",
|
|
24
|
+
external_type: "",
|
|
25
|
+
fines: "",
|
|
26
|
+
permanent_id: "",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// `clearPatronData` is called on unmount (RTL cleanup), so it must always be a
|
|
30
|
+
// function; `patronLookup` too, so the connected dispatch props are stubbed.
|
|
31
|
+
const renderForm = (props: Record<string, unknown> = {}) =>
|
|
32
|
+
renderWithProviders(
|
|
33
|
+
<ManagePatronsForm
|
|
34
|
+
store={buildStore()}
|
|
35
|
+
csrfToken="token"
|
|
36
|
+
library="nypl"
|
|
37
|
+
patronLookup={jest.fn()}
|
|
38
|
+
clearPatronData={jest.fn()}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
describe("ManagePatronsForm", () => {
|
|
44
|
+
describe("rendering", () => {
|
|
45
|
+
it("has .manage-patrons-form class", () => {
|
|
46
|
+
const { container } = renderForm();
|
|
47
|
+
expect(
|
|
48
|
+
container.querySelector(".manage-patrons-form")
|
|
49
|
+
).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("shows an editable identifier input", () => {
|
|
53
|
+
renderForm();
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByPlaceholderText("Enter the patron's identifier")
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("has a form with an input field and a button", () => {
|
|
61
|
+
const { container } = renderForm();
|
|
62
|
+
expect(container.querySelectorAll("form")).toHaveLength(1);
|
|
63
|
+
expect(container.querySelectorAll("form input")).toHaveLength(1);
|
|
64
|
+
expect(container.querySelectorAll("form button")).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("doesn't initially show an alert or error message", () => {
|
|
68
|
+
const { container } = renderForm();
|
|
69
|
+
expect(container.querySelector(".alert")).not.toBeInTheDocument();
|
|
70
|
+
expect(screen.queryByText(/Patron found/)).not.toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("behavior", () => {
|
|
75
|
+
it("calls patronLookup on submit", async () => {
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
const patronLookup = jest.fn();
|
|
78
|
+
renderForm({ patronLookup });
|
|
79
|
+
|
|
80
|
+
expect(patronLookup).not.toHaveBeenCalled();
|
|
81
|
+
|
|
82
|
+
await user.type(
|
|
83
|
+
screen.getByPlaceholderText("Enter the patron's identifier"),
|
|
84
|
+
"1234"
|
|
85
|
+
);
|
|
86
|
+
await user.click(screen.getByRole("button"));
|
|
87
|
+
|
|
88
|
+
expect(patronLookup).toHaveBeenCalledTimes(1);
|
|
89
|
+
// The second argument is the library short name.
|
|
90
|
+
expect(patronLookup.mock.calls[0][1]).toBe("nypl");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("displays an error message if there is an error", () => {
|
|
94
|
+
const fetchError = { status: 400, response: "", url: "" };
|
|
95
|
+
const { container, rerender } = renderForm();
|
|
96
|
+
expect(container.querySelector(".alert-danger")).not.toBeInTheDocument();
|
|
97
|
+
|
|
98
|
+
rerender(
|
|
99
|
+
<ManagePatronsForm
|
|
100
|
+
store={buildStore()}
|
|
101
|
+
csrfToken="token"
|
|
102
|
+
library="nypl"
|
|
103
|
+
patronLookup={jest.fn()}
|
|
104
|
+
clearPatronData={jest.fn()}
|
|
105
|
+
fetchError={fetchError}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(container.querySelector(".alert-danger")).toBeInTheDocument();
|
|
110
|
+
expect(screen.getByText(/Error:/)).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should display a no-identifier message if the input field is blank when searching", () => {
|
|
114
|
+
const fetchError = {
|
|
115
|
+
status: 400,
|
|
116
|
+
response: "No patron identifier provided",
|
|
117
|
+
url: "",
|
|
118
|
+
};
|
|
119
|
+
const { rerender } = renderForm();
|
|
120
|
+
expect(
|
|
121
|
+
screen.queryByText("Error: No patron identifier provided")
|
|
122
|
+
).not.toBeInTheDocument();
|
|
123
|
+
|
|
124
|
+
rerender(
|
|
125
|
+
<ManagePatronsForm
|
|
126
|
+
store={buildStore()}
|
|
127
|
+
csrfToken="token"
|
|
128
|
+
library="nypl"
|
|
129
|
+
patronLookup={jest.fn()}
|
|
130
|
+
clearPatronData={jest.fn()}
|
|
131
|
+
fetchError={fetchError}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(
|
|
136
|
+
screen.getByText("Error: No patron identifier provided")
|
|
137
|
+
).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should display a success alert message when a patron is found", () => {
|
|
141
|
+
const { container, rerender } = renderForm();
|
|
142
|
+
expect(container.querySelector(".alert-success")).not.toBeInTheDocument();
|
|
143
|
+
|
|
144
|
+
rerender(
|
|
145
|
+
<ManagePatronsForm
|
|
146
|
+
store={buildStore()}
|
|
147
|
+
csrfToken="token"
|
|
148
|
+
library="nypl"
|
|
149
|
+
patronLookup={jest.fn()}
|
|
150
|
+
clearPatronData={jest.fn()}
|
|
151
|
+
patron={patron}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const alert = container.querySelector(".alert-success");
|
|
156
|
+
expect(alert).toBeInTheDocument();
|
|
157
|
+
expect(alert).toHaveTextContent(
|
|
158
|
+
`Patron found: ${patron.authorization_identifier}`
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should show the PatronInfo list when a patron is found", () => {
|
|
163
|
+
const { container, rerender } = renderForm();
|
|
164
|
+
expect(container.querySelector(".patron-info")).not.toBeInTheDocument();
|
|
165
|
+
|
|
166
|
+
rerender(
|
|
167
|
+
<ManagePatronsForm
|
|
168
|
+
store={buildStore()}
|
|
169
|
+
csrfToken="token"
|
|
170
|
+
library="nypl"
|
|
171
|
+
patronLookup={jest.fn()}
|
|
172
|
+
clearPatronData={jest.fn()}
|
|
173
|
+
patron={patron}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(container.querySelector(".patron-info")).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import buildStore from "../../../src/store";
|
|
7
|
+
import Admin from "../../../src/models/Admin";
|
|
8
|
+
|
|
9
|
+
// Both tab panels fetch on mount (ResetAdobeId is a connected component; Debug
|
|
10
|
+
// Authentication uses react-query). They're incidental to a tab-navigation test,
|
|
11
|
+
// so mock them to markers. ResetAdobeId echoes the csrfToken asserted below.
|
|
12
|
+
jest.mock("../../../src/components/ResetAdobeId", () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: (props: any) => (
|
|
15
|
+
<div data-testid="reset-adobe-id" data-csrf-token={props.csrfToken} />
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
jest.mock("../../../src/components/DebugAuthentication", () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: () => <div data-testid="debug-authentication" />,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import ManagePatronsTabContainer from "../../../src/components/ManagePatronsTabContainer";
|
|
24
|
+
|
|
25
|
+
// ManagePatronsTabContainer reads `admin`, `router`, and `pathFor` from the legacy
|
|
26
|
+
// React context the app supplies in production. Wrap it in a small provider that
|
|
27
|
+
// hands it those.
|
|
28
|
+
class LegacyContextProvider extends React.Component<{ context: any }> {
|
|
29
|
+
static childContextTypes = {
|
|
30
|
+
router: PropTypes.object,
|
|
31
|
+
pathFor: PropTypes.func,
|
|
32
|
+
admin: PropTypes.object,
|
|
33
|
+
};
|
|
34
|
+
getChildContext() {
|
|
35
|
+
return this.props.context;
|
|
36
|
+
}
|
|
37
|
+
render() {
|
|
38
|
+
return this.props.children;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("ManagePatronsTabContainer", () => {
|
|
43
|
+
const systemAdmin = new Admin([{ role: "system" }]);
|
|
44
|
+
const libraryManagerNYPL = new Admin([{ role: "manager", library: "NYPL" }]);
|
|
45
|
+
const libraryManagerA = new Admin([{ role: "manager", library: "a" }]);
|
|
46
|
+
const librarian = new Admin([{ role: "librarian", library: "NYPL" }]);
|
|
47
|
+
|
|
48
|
+
const renderWithAdmin = (admin: Admin, push: jest.Mock = jest.fn()) => {
|
|
49
|
+
const context = { router: { push }, pathFor: jest.fn(), admin };
|
|
50
|
+
const result = render(
|
|
51
|
+
<LegacyContextProvider context={context}>
|
|
52
|
+
<ManagePatronsTabContainer
|
|
53
|
+
tab={null}
|
|
54
|
+
csrfToken="token"
|
|
55
|
+
store={buildStore()}
|
|
56
|
+
library="NYPL"
|
|
57
|
+
/>
|
|
58
|
+
</LegacyContextProvider>
|
|
59
|
+
);
|
|
60
|
+
return { ...result, push };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("for system admin", () => {
|
|
64
|
+
it("shows tabs", () => {
|
|
65
|
+
renderWithAdmin(systemAdmin);
|
|
66
|
+
expect(
|
|
67
|
+
screen.getByRole("link", { name: "Reset Adobe ID" })
|
|
68
|
+
).toBeInTheDocument();
|
|
69
|
+
expect(
|
|
70
|
+
screen.getByRole("link", { name: "Debug Authentication" })
|
|
71
|
+
).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("shows components", () => {
|
|
75
|
+
renderWithAdmin(systemAdmin);
|
|
76
|
+
expect(screen.getByTestId("reset-adobe-id")).toHaveAttribute(
|
|
77
|
+
"data-csrf-token",
|
|
78
|
+
"token"
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("uses router to navigate when tab is clicked", async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
const { push } = renderWithAdmin(systemAdmin);
|
|
85
|
+
await user.click(screen.getByRole("link", { name: "Reset Adobe ID" }));
|
|
86
|
+
expect(push).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(push).toHaveBeenCalledWith("/admin/web/patrons/NYPL/resetAdobeId");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("for right library manager", () => {
|
|
92
|
+
it("shows tabs", () => {
|
|
93
|
+
renderWithAdmin(libraryManagerNYPL);
|
|
94
|
+
expect(
|
|
95
|
+
screen.getByRole("link", { name: "Reset Adobe ID" })
|
|
96
|
+
).toBeInTheDocument();
|
|
97
|
+
expect(
|
|
98
|
+
screen.getByRole("link", { name: "Debug Authentication" })
|
|
99
|
+
).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("marks the current tab as active", () => {
|
|
103
|
+
const { container } = renderWithAdmin(libraryManagerNYPL);
|
|
104
|
+
const tabItems = container.querySelectorAll("ul.nav-tabs li");
|
|
105
|
+
expect(tabItems).toHaveLength(2);
|
|
106
|
+
expect(tabItems[0]).toHaveClass("active");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("shows components", () => {
|
|
110
|
+
renderWithAdmin(libraryManagerNYPL);
|
|
111
|
+
expect(screen.getByTestId("reset-adobe-id")).toHaveAttribute(
|
|
112
|
+
"data-csrf-token",
|
|
113
|
+
"token"
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("uses router to navigate when tab is clicked", async () => {
|
|
118
|
+
const user = userEvent.setup();
|
|
119
|
+
const { push } = renderWithAdmin(libraryManagerNYPL);
|
|
120
|
+
await user.click(screen.getByRole("link", { name: "Reset Adobe ID" }));
|
|
121
|
+
expect(push).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(push).toHaveBeenCalledWith("/admin/web/patrons/NYPL/resetAdobeId");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("for wrong library manager", () => {
|
|
127
|
+
it("doesn't show tabs", () => {
|
|
128
|
+
renderWithAdmin(libraryManagerA);
|
|
129
|
+
expect(
|
|
130
|
+
screen.queryByRole("link", { name: "Reset Adobe ID" })
|
|
131
|
+
).not.toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("for librarian", () => {
|
|
136
|
+
it("doesn't show tabs", () => {
|
|
137
|
+
renderWithAdmin(librarian);
|
|
138
|
+
expect(
|
|
139
|
+
screen.queryByRole("link", { name: "Reset Adobe ID" })
|
|
140
|
+
).not.toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|