@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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/tests/jest/components/AccountPage.test.tsx +41 -0
  3. package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
  4. package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
  5. package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
  6. package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
  7. package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
  8. package/tests/jest/components/BookDetails.test.tsx +133 -0
  9. package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
  10. package/tests/jest/components/CatalogServices.test.tsx +63 -0
  11. package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
  12. package/tests/jest/components/ConfigPage.test.tsx +58 -0
  13. package/tests/jest/components/CustomListPage.test.tsx +57 -0
  14. package/tests/jest/components/CustomListSearch.test.tsx +275 -0
  15. package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
  16. package/tests/jest/components/DashboardPage.test.tsx +76 -0
  17. package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
  18. package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
  19. package/tests/jest/components/EditableConfigList.test.tsx +8 -2
  20. package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
  21. package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
  22. package/tests/jest/components/ErrorMessage.test.tsx +97 -0
  23. package/tests/jest/components/Footer.test.tsx +141 -0
  24. package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
  25. package/tests/jest/components/LaneEditor.test.tsx +2 -6
  26. package/tests/jest/components/LanePage.test.tsx +68 -0
  27. package/tests/jest/components/LanguageField.test.tsx +40 -0
  28. package/tests/jest/components/LoadButton.test.tsx +49 -0
  29. package/tests/jest/components/ManagePatrons.test.tsx +73 -0
  30. package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
  31. package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
  32. package/tests/jest/components/MetadataServices.test.tsx +59 -0
  33. package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
  34. package/tests/jest/components/PatronInfo.test.tsx +54 -0
  35. package/tests/jest/components/SelfTestResult.test.tsx +164 -0
  36. package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
  37. package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
  38. package/tests/jest/components/SetupPage.test.tsx +41 -0
  39. package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
  40. package/tests/jest/components/Stats.test.tsx +4 -8
  41. package/tests/jest/components/TabContainer.test.tsx +68 -0
  42. package/tests/jest/components/Timestamp.test.tsx +74 -0
  43. package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
  44. package/tests/jest/components/WelcomePage.test.tsx +43 -0
  45. package/tests/jest/components/WithEditButton.test.tsx +55 -0
  46. package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
  47. package/tests/jest/index.test.tsx +50 -0
  48. 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
- getAllByRole,
390
- getByRole,
391
- queryAllByRole,
392
- rerender,
393
- } = renderWithProviders(
394
- <InventoryReportRequestModal
395
- show={show}
396
- onHide={onHide}
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
+ });