@thepalaceproject/circulation-admin 1.43.0-post.7 → 1.43.0-post.8

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 CHANGED
@@ -143,5 +143,5 @@
143
143
  "*.{js,jsx,ts,tsx,css,md}": "prettier --write",
144
144
  "*.{js,css,md}": "prettier --write"
145
145
  },
146
- "version": "1.43.0-post.7"
146
+ "version": "1.43.0-post.8"
147
147
  }
@@ -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
+
6
+ // Header and Footer are page chrome with no behavior worth exercising here, and
7
+ // Header pulls in the react-router v3 context that `renderWithProviders` does not
8
+ // supply. Mock them to markers so the test stays about what AccountPage composes.
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
+ import AccountPage from "../../../src/components/AccountPage";
19
+
20
+ describe("AccountPage", () => {
21
+ it("renders the header and footer chrome", () => {
22
+ renderWithProviders(<AccountPage />);
23
+
24
+ expect(screen.getByTestId("header")).toBeInTheDocument();
25
+ expect(screen.getByTestId("footer")).toBeInTheDocument();
26
+ });
27
+
28
+ it("renders the change-password form", () => {
29
+ renderWithProviders(<AccountPage />);
30
+
31
+ // ChangePasswordForm is connected; its heading and fields appearing proves
32
+ // it received a working store and csrf token from context. EditableInput
33
+ // renders its label and input as siblings with no htmlFor, so there is no
34
+ // accessible label association to query — assert the label text instead.
35
+ expect(
36
+ screen.getByRole("heading", { name: "Change Password" })
37
+ ).toBeInTheDocument();
38
+ expect(screen.getByText("New Password")).toBeInTheDocument();
39
+ expect(screen.getByText("Confirm New Password")).toBeInTheDocument();
40
+ });
41
+ });
@@ -0,0 +1,133 @@
1
+ import * as React from "react";
2
+ import { render } from "@testing-library/react";
3
+
4
+ import BookDetails from "../../../src/components/BookDetails";
5
+ import { BookData } from "@thepalaceproject/web-opds-client/lib/interfaces";
6
+
7
+ const book: BookData = {
8
+ id: "urn:librarysimplified.org/terms/id/3M%20ID/crrmnr9",
9
+ url: "http://circulation.librarysimplified.org/works/3M/crrmnr9",
10
+ title: "The Mayan Secrets",
11
+ authors: ["Clive Cussler", "Thomas Perry"],
12
+ contributors: ["contributor 1"],
13
+ summary:
14
+ "&lt;b&gt;Sam and Remi Fargo race for treasure&#8212;and survival&#8212;in this lightning-paced new adventure from #1&lt;i&gt; New York Times&lt;/i&gt; bestselling author Clive Cussler.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Husband-and-wife team Sam and Remi Fargo are in Mexico when they come upon a remarkable discovery&#8212;the mummified remainsof a man clutching an ancient sealed pot. Within the pot is a Mayan book larger than any known before.&lt;br /&gt;&lt;br /&gt;The book contains astonishing information about the Mayans, their cities, and about mankind itself. The secrets are so powerful that some people would do anything to possess them&#8212;as the Fargos are about to find out. Many men and women are going to die for that book.",
15
+ imageUrl: "https://dlotdqc6pnwqb.cloudfront.net/3M/crrmnr9/cover.jpg",
16
+ borrowUrl: "borrow url",
17
+ openAccessLinks: [{ url: "secrets.epub", type: "application/epub+zip" }],
18
+ publisher: "Penguin Publishing Group",
19
+ published: "February 29, 2016",
20
+ categories: ["Children", "10-12", "Fiction", "Adventure", "Fantasy"],
21
+ raw: {
22
+ category: [
23
+ {
24
+ $: {
25
+ scheme: { value: "http://schema.org/audience" },
26
+ label: { value: "Children" },
27
+ },
28
+ },
29
+ {
30
+ $: {
31
+ scheme: { value: "http://schema.org/typicalAgeRange" },
32
+ label: { value: "10-12" },
33
+ },
34
+ },
35
+ {
36
+ $: {
37
+ scheme: { value: "http://librarysimplified.org/terms/fiction/" },
38
+ label: { value: "Fiction" },
39
+ },
40
+ },
41
+ {
42
+ $: {
43
+ scheme: {
44
+ value: "http://librarysimplified.org/terms/genres/Simplified/",
45
+ },
46
+ label: { value: "Adventure" },
47
+ },
48
+ },
49
+ {
50
+ $: {
51
+ scheme: {
52
+ value: "http://librarysimplified.org/terms/genres/Simplified/",
53
+ },
54
+ label: { value: "Fantasy" },
55
+ },
56
+ },
57
+ ],
58
+ "bibframe:distribution": [
59
+ {
60
+ $: {
61
+ "bibframe:ProviderName": {
62
+ value: "Overdrive",
63
+ },
64
+ },
65
+ },
66
+ ],
67
+ link: [
68
+ {
69
+ $: {
70
+ rel: { value: "issues" },
71
+ href: { value: "http://example.com/report" },
72
+ },
73
+ },
74
+ {
75
+ $: {
76
+ rel: { value: "http://librarysimplified.org/terms/rel/revoke" },
77
+ href: { value: "http://example.com/revoke" },
78
+ },
79
+ },
80
+ ],
81
+ },
82
+ };
83
+
84
+ describe("BookDetails", () => {
85
+ const updateBook = jest.fn();
86
+
87
+ it("shows audience and target age", () => {
88
+ const { container } = render(
89
+ <BookDetails book={book} updateBook={updateBook} />
90
+ );
91
+ const audience = container.querySelector(".audience");
92
+ expect(audience).toBeInTheDocument();
93
+ expect(audience).toHaveTextContent("Audience: Children (age 10-12)");
94
+ });
95
+
96
+ it("shows categories", () => {
97
+ const { container } = render(
98
+ <BookDetails book={book} updateBook={updateBook} />
99
+ );
100
+ const categories = container.querySelector(".categories");
101
+ expect(categories).toBeInTheDocument();
102
+ expect(categories).toHaveTextContent("Categories: Adventure, Fantasy");
103
+ });
104
+
105
+ it("doesn't show categories when there aren't any", () => {
106
+ const bookCopy: BookData = {
107
+ ...book,
108
+ raw: { category: [], link: [] },
109
+ };
110
+ const { container } = render(
111
+ <BookDetails book={bookCopy} updateBook={updateBook} />
112
+ );
113
+ expect(container.querySelector(".categories")).not.toBeInTheDocument();
114
+ });
115
+
116
+ it("shows distributor", () => {
117
+ const { container } = render(
118
+ <BookDetails book={book} updateBook={updateBook} />
119
+ );
120
+ const distributor = container.querySelector(".distributed-by");
121
+ expect(distributor).toBeInTheDocument();
122
+ expect(distributor).toHaveTextContent("Distributed By: Overdrive");
123
+ });
124
+
125
+ it("doesn't render any circulation link content", () => {
126
+ const { container } = render(
127
+ <BookDetails book={book} updateBook={updateBook} />
128
+ );
129
+ const circulationLinks = container.querySelector(".circulation-links");
130
+ expect(circulationLinks).toBeInTheDocument();
131
+ expect(circulationLinks).toBeEmptyDOMElement();
132
+ });
133
+ });
@@ -0,0 +1,116 @@
1
+ import * as React from "react";
2
+ import * as PropTypes from "prop-types";
3
+ import { render, screen } from "@testing-library/react";
4
+
5
+ import buildStore from "../../../src/store";
6
+ import { BookData } from "@thepalaceproject/web-opds-client/lib/interfaces";
7
+
8
+ // BookDetailsTabContainer is a connected TabContainer that mounts every one of its
9
+ // tab panels on render (the metadata editor, classifications, cover, and lists
10
+ // editors), each of which fetches over the network on mount. That machinery is
11
+ // incidental to what BookDetailsContainer composes, so mock it to a marker that
12
+ // echoes the props it receives and renders the book child it is handed.
13
+ jest.mock("../../../src/components/BookDetailsTabContainer", () => ({
14
+ __esModule: true,
15
+ default: (props: any) => (
16
+ <div
17
+ data-testid="book-details-tab-container"
18
+ data-book-url={props.bookUrl}
19
+ data-collection-url={props.collectionUrl}
20
+ data-csrf-token={props.csrfToken}
21
+ data-has-store={String(!!props.store)}
22
+ data-has-library={String(!!props.library)}
23
+ data-has-refresh-catalog={String(!!props.refreshCatalog)}
24
+ >
25
+ {props.children}
26
+ </div>
27
+ ),
28
+ }));
29
+
30
+ import BookDetailsContainer from "../../../src/components/BookDetailsContainer";
31
+ import BookDetails from "../../../src/components/BookDetails";
32
+
33
+ // BookDetailsContainer reads its configuration from the legacy React context that
34
+ // the OPDS web client supplies in production. `renderWithProviders` does not supply
35
+ // the `library` (nor `tab`) keys it reads, so wrap it in a small legacy context
36
+ // provider that hands it everything it needs.
37
+ class LegacyContextProvider extends React.Component<{ context: any }> {
38
+ static childContextTypes = {
39
+ editorStore: PropTypes.object,
40
+ csrfToken: PropTypes.string,
41
+ tab: PropTypes.string,
42
+ library: PropTypes.func,
43
+ admin: PropTypes.object,
44
+ };
45
+ getChildContext() {
46
+ return this.props.context;
47
+ }
48
+ render() {
49
+ return this.props.children;
50
+ }
51
+ }
52
+
53
+ const bookData: BookData = {
54
+ id: "book id",
55
+ url: "book url",
56
+ title: "book title",
57
+ raw: { category: [], link: [] },
58
+ };
59
+
60
+ describe("BookDetailsContainer", () => {
61
+ let store;
62
+ let context;
63
+ let refreshCatalog;
64
+
65
+ beforeEach(() => {
66
+ store = buildStore();
67
+ refreshCatalog = jest.fn();
68
+ context = {
69
+ editorStore: store,
70
+ csrfToken: "token",
71
+ library: jest.fn(),
72
+ admin: { isLibraryManager: () => true },
73
+ };
74
+ });
75
+
76
+ const renderContainer = () =>
77
+ render(
78
+ <LegacyContextProvider context={context}>
79
+ <BookDetailsContainer
80
+ book={bookData}
81
+ bookUrl="book url"
82
+ collectionUrl="collection url"
83
+ refreshCatalog={refreshCatalog}
84
+ >
85
+ <BookDetails book={bookData} updateBook={jest.fn()} />
86
+ </BookDetailsContainer>
87
+ </LegacyContextProvider>
88
+ );
89
+
90
+ it("renders BookDetails with its child's props", () => {
91
+ renderContainer();
92
+ // The container recreates the real BookDetails from the child element's props,
93
+ // so the book it was handed shows up (its title is rendered as the heading).
94
+ expect(
95
+ screen.getByRole("heading", { name: "book title" })
96
+ ).toBeInTheDocument();
97
+ });
98
+
99
+ it("shows a tab container with initial tab", () => {
100
+ renderContainer();
101
+ const tabContainer = screen.getByTestId("book-details-tab-container");
102
+ expect(tabContainer).toBeInTheDocument();
103
+ expect(tabContainer).toHaveAttribute("data-book-url", "book url");
104
+ expect(tabContainer).toHaveAttribute(
105
+ "data-collection-url",
106
+ "collection url"
107
+ );
108
+ expect(tabContainer).toHaveAttribute("data-csrf-token", "token");
109
+ // The library function, redux store, and refreshCatalog callback are forwarded
110
+ // from context/props; they are not serializable to the DOM, so assert they were
111
+ // passed through.
112
+ expect(tabContainer).toHaveAttribute("data-has-store", "true");
113
+ expect(tabContainer).toHaveAttribute("data-has-library", "true");
114
+ expect(tabContainer).toHaveAttribute("data-has-refresh-catalog", "true");
115
+ });
116
+ });
@@ -0,0 +1,58 @@
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
+ // ConfigTabContainer mounts the connected configuration sections (Libraries,
18
+ // Collections, Individual Admins, ...) which fetch over the network on mount. That
19
+ // is incidental to what ConfigPage composes — the page's job is to hand the tab
20
+ // container the params and context it needs — so mock it to a marker that echoes
21
+ // the props it received.
22
+ jest.mock("../../../src/components/ConfigTabContainer", () => ({
23
+ __esModule: true,
24
+ default: (props: any) => (
25
+ <div
26
+ data-testid="config-tab-container"
27
+ data-tab={props.tab}
28
+ data-edit-or-create={props.editOrCreate}
29
+ data-identifier={props.identifier}
30
+ data-csrf-token={props.csrfToken}
31
+ data-has-store={String(!!props.store)}
32
+ />
33
+ ),
34
+ }));
35
+
36
+ import ConfigPage from "../../../src/components/ConfigPage";
37
+
38
+ describe("ConfigPage", () => {
39
+ const params = {
40
+ tab: "libraries",
41
+ editOrCreate: "edit",
42
+ identifier: "identifier",
43
+ };
44
+
45
+ it("shows a tab container with tab from params", () => {
46
+ renderWithProviders(<ConfigPage params={params} />, {
47
+ appConfigSettings: { csrfToken: "token" },
48
+ });
49
+
50
+ const tabContainer = screen.getByTestId("config-tab-container");
51
+ expect(tabContainer).toHaveAttribute("data-tab", "libraries");
52
+ expect(tabContainer).toHaveAttribute("data-edit-or-create", "edit");
53
+ expect(tabContainer).toHaveAttribute("data-identifier", "identifier");
54
+ expect(tabContainer).toHaveAttribute("data-csrf-token", "token");
55
+ // The redux store is forwarded from context; assert it was passed through.
56
+ expect(tabContainer).toHaveAttribute("data-has-store", "true");
57
+ });
58
+ });
@@ -0,0 +1,57 @@
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
+ // CustomLists is a connected component that fetches lists, collections, libraries,
18
+ // and languages over the network on mount. That is incidental to what
19
+ // CustomListPage composes — the page's job is to hand the editor the params and
20
+ // context it needs — so mock it to a marker that echoes the props it received.
21
+ jest.mock("../../../src/components/CustomLists", () => ({
22
+ __esModule: true,
23
+ default: (props: any) => (
24
+ <div
25
+ data-testid="custom-lists"
26
+ data-library={props.library}
27
+ data-edit-or-create={props.editOrCreate}
28
+ data-identifier={props.identifier}
29
+ data-csrf-token={props.csrfToken}
30
+ data-has-store={String(!!props.store)}
31
+ />
32
+ ),
33
+ }));
34
+
35
+ import CustomListPage from "../../../src/components/CustomListPage";
36
+
37
+ describe("CustomListPage", () => {
38
+ const params = {
39
+ library: "library",
40
+ editOrCreate: "edit",
41
+ identifier: "identifier",
42
+ };
43
+
44
+ it("shows custom lists with info from params", () => {
45
+ renderWithProviders(<CustomListPage params={params} />, {
46
+ appConfigSettings: { csrfToken: "token" },
47
+ });
48
+
49
+ const customLists = screen.getByTestId("custom-lists");
50
+ expect(customLists).toHaveAttribute("data-library", "library");
51
+ expect(customLists).toHaveAttribute("data-edit-or-create", "edit");
52
+ expect(customLists).toHaveAttribute("data-identifier", "identifier");
53
+ expect(customLists).toHaveAttribute("data-csrf-token", "token");
54
+ // The redux store is forwarded from context; assert it was passed through.
55
+ expect(customLists).toHaveAttribute("data-has-store", "true");
56
+ });
57
+ });
@@ -0,0 +1,76 @@
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
+ // Stats fetches statistics via RTK Query on mount; DashboardPage's job is just
18
+ // composition, so mock Stats to a marker that echoes the `library` prop it is
19
+ // handed rather than wiring up fetch mocking.
20
+ jest.mock("../../../src/components/Stats", () => ({
21
+ __esModule: true,
22
+ default: (props: any) => (
23
+ <div data-testid="stats" data-library={props.library} />
24
+ ),
25
+ }));
26
+
27
+ // CirculationEventsDownload gates its output on a feature flag (off by default) and
28
+ // keeps the `library` prop inside a collapsed form, so it is not DOM-observable when
29
+ // rendered for real. Mock it to a marker that echoes `library` so the prop-flow the
30
+ // original test checked stays observable.
31
+ jest.mock("../../../src/components/CirculationEventsDownload", () => ({
32
+ __esModule: true,
33
+ default: (props: any) => (
34
+ <div
35
+ data-testid="circulation-events-download"
36
+ data-library={props.library}
37
+ />
38
+ ),
39
+ }));
40
+
41
+ import DashboardPage from "../../../src/components/DashboardPage";
42
+
43
+ describe("DashboardPage", () => {
44
+ it("shows Header", () => {
45
+ renderWithProviders(<DashboardPage params={{}} />);
46
+ expect(screen.getByTestId("header")).toBeInTheDocument();
47
+ });
48
+
49
+ it("shows CirculationEventsDownload", () => {
50
+ const { rerender } = renderWithProviders(<DashboardPage params={{}} />);
51
+
52
+ let events = screen.getByTestId("circulation-events-download");
53
+ expect(events).toBeInTheDocument();
54
+ expect(events).not.toHaveAttribute("data-library");
55
+
56
+ rerender(<DashboardPage params={{ library: "NYPL" }} />);
57
+ events = screen.getByTestId("circulation-events-download");
58
+ expect(events).toHaveAttribute("data-library", "NYPL");
59
+ });
60
+
61
+ it("shows Stats", () => {
62
+ const { rerender } = renderWithProviders(<DashboardPage params={{}} />);
63
+
64
+ let stats = screen.getByTestId("stats");
65
+ expect(stats).not.toHaveAttribute("data-library");
66
+
67
+ rerender(<DashboardPage params={{ library: "NYPL" }} />);
68
+ stats = screen.getByTestId("stats");
69
+ expect(stats).toHaveAttribute("data-library", "NYPL");
70
+ });
71
+
72
+ it("shows Footer", () => {
73
+ renderWithProviders(<DashboardPage params={{}} />);
74
+ expect(screen.getByTestId("footer")).toBeInTheDocument();
75
+ });
76
+ });
@@ -212,11 +212,7 @@ describe("DebugAuthentication", () => {
212
212
  )
213
213
  );
214
214
 
215
- const {
216
- getByLabelText,
217
- queryByLabelText,
218
- rerender,
219
- } = renderWithProviders(
215
+ const { getByLabelText, queryByLabelText, rerender } = renderWithProviders(
220
216
  <DebugAuthentication library={LIBRARY} csrfToken="test-csrf-token" />,
221
217
  { queryClient }
222
218
  );
@@ -383,7 +383,11 @@ describe("EditableConfigList - library association disclosure", () => {
383
383
  it("falls back to short_name when allLibraries is absent from the data", () => {
384
384
  const { container } = renderWithContext(
385
385
  <TestServiceList
386
- data={{ services: [{ id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] }] }}
386
+ data={{
387
+ services: [
388
+ { id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] },
389
+ ],
390
+ }}
387
391
  fetchData={jest.fn()}
388
392
  editItem={jest.fn().mockResolvedValue(undefined)}
389
393
  deleteItem={jest.fn().mockResolvedValue(undefined)}
@@ -393,7 +397,9 @@ describe("EditableConfigList - library association disclosure", () => {
393
397
  config
394
398
  );
395
399
  fireEvent.click(container.querySelector(".association-toggle"));
396
- expect(container.querySelector(".associated-items li").textContent).toBe("nypl");
400
+ expect(container.querySelector(".associated-items li").textContent).toBe(
401
+ "nypl"
402
+ );
397
403
  });
398
404
  });
399
405
  });
@@ -0,0 +1,97 @@
1
+ import * as React from "react";
2
+ import { screen } from "@testing-library/react";
3
+
4
+ import { renderWithProviders } from "../testUtils/withProviders";
5
+
6
+ // The wrapped `Collection` (from web-opds-client) is incidental here — the
7
+ // container's job is to pull the "Formats" facets out and hand them to
8
+ // `EntryPointsTabs`. Mock Collection to a marker so we can render the real
9
+ // EntryPointsTabs and assert the tabs it produces.
10
+ jest.mock(
11
+ "@thepalaceproject/web-opds-client/lib/components/Collection",
12
+ () => ({
13
+ __esModule: true,
14
+ default: () => <div data-testid="collection" />,
15
+ })
16
+ );
17
+
18
+ // EntryPointsTabs renders CatalogLink -> React Router `Link`, which needs router
19
+ // context renderWithProviders does not supply. Mock `Link` to a marker.
20
+ jest.mock("react-router", () => ({
21
+ ...jest.requireActual("react-router"),
22
+ Link: (props) => (
23
+ <div data-testid="Link" data-to={props.to}>
24
+ {props.children}
25
+ </div>
26
+ ),
27
+ }));
28
+
29
+ import EntryPointsContainer from "../../../src/components/EntryPointsContainer";
30
+
31
+ // Stands in for the OPDS Collection child whose `collection` prop the container
32
+ // reads. It is never rendered by the container (only its props are inspected).
33
+ class CollectionChild extends React.Component<{ collection: unknown }> {
34
+ render() {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ describe("EntryPointsContainer", () => {
40
+ it("should not render entry-point tabs if no facets were passed", () => {
41
+ const collection = {
42
+ facetGroups: [],
43
+ lanes: [],
44
+ books: [],
45
+ navigationLinks: [],
46
+ };
47
+
48
+ const { container } = renderWithProviders(
49
+ <EntryPointsContainer>
50
+ <CollectionChild collection={collection} />
51
+ </EntryPointsContainer>
52
+ );
53
+
54
+ expect(
55
+ container.querySelector(".entry-points-tab-container")
56
+ ).toBeInTheDocument();
57
+ // EntryPointsTabs renders nothing when there are no format facets.
58
+ expect(container.querySelector(".nav-tabs")).toBeNull();
59
+ });
60
+
61
+ it("shows a tab container with facets when the label is 'Formats'", () => {
62
+ const facetGroups = [
63
+ {
64
+ facets: [],
65
+ label: "Some other facets",
66
+ },
67
+ {
68
+ facets: [
69
+ {
70
+ label: "Ebooks",
71
+ href: "http://circulation.librarysimplified.org/groups/?entrypoint=Book",
72
+ active: false,
73
+ },
74
+ {
75
+ label: "Audiobooks",
76
+ href: "http://circulation.librarysimplified.org/groups/?entrypoint=Audio",
77
+ active: false,
78
+ },
79
+ ],
80
+ label: "Formats",
81
+ },
82
+ ];
83
+ const collection = { facetGroups };
84
+
85
+ const { container } = renderWithProviders(
86
+ <EntryPointsContainer>
87
+ <CollectionChild collection={collection} />
88
+ </EntryPointsContainer>
89
+ );
90
+
91
+ const list = container.querySelector("ul.nav-tabs");
92
+ expect(list).toBeInTheDocument();
93
+ expect(list.querySelectorAll("li")).toHaveLength(2);
94
+ expect(screen.getByText("Ebooks")).toBeInTheDocument();
95
+ expect(screen.getByText("Audiobooks")).toBeInTheDocument();
96
+ });
97
+ });
@@ -0,0 +1,88 @@
1
+ import * as React from "react";
2
+ import { screen } from "@testing-library/react";
3
+ import { FacetData } from "@thepalaceproject/web-opds-client/lib/interfaces";
4
+
5
+ import { renderWithProviders } from "../testUtils/withProviders";
6
+
7
+ // Each tab is a CatalogLink, which renders a React Router `Link`. Router `Link`
8
+ // needs router context that `renderWithProviders` does not supply, so mock it to
9
+ // a marker that exposes its navigation target and children (matching Lane.test.tsx).
10
+ jest.mock("react-router", () => ({
11
+ ...jest.requireActual("react-router"),
12
+ Link: (props) => (
13
+ <div data-testid="Link" data-to={props.to}>
14
+ {props.children}
15
+ </div>
16
+ ),
17
+ }));
18
+
19
+ import { EntryPointsTabs } from "../../../src/components/EntryPointsTabs";
20
+
21
+ const twoFacets: FacetData[] = [
22
+ {
23
+ label: "Ebooks",
24
+ href: "http://circulation.librarysimplified.org/groups/?entrypoint=Book",
25
+ active: true,
26
+ },
27
+ {
28
+ label: "Audiobooks",
29
+ href: "http://circulation.librarysimplified.org/groups/?entrypoint=Audio",
30
+ active: false,
31
+ },
32
+ ];
33
+
34
+ describe("EntryPointsTabs", () => {
35
+ it("should not generate any tabs", () => {
36
+ const { container } = renderWithProviders(<EntryPointsTabs />);
37
+
38
+ expect(container.querySelector(".nav-tabs")).toBeNull();
39
+ });
40
+
41
+ it("should generate two tabs with two entry points", () => {
42
+ const { container } = renderWithProviders(
43
+ <EntryPointsTabs facets={twoFacets} />
44
+ );
45
+
46
+ const list = container.querySelector("ul.nav-tabs");
47
+ expect(list).toBeInTheDocument();
48
+
49
+ const items = list.querySelectorAll("li");
50
+ expect(items).toHaveLength(2);
51
+ expect(items[0]).toHaveClass("active");
52
+ expect(items[1]).not.toHaveClass("active");
53
+
54
+ expect(screen.getByText("Ebooks")).toBeInTheDocument();
55
+ expect(screen.getByText("Audiobooks")).toBeInTheDocument();
56
+ });
57
+
58
+ it("uses router to navigate when a tab is clicked", () => {
59
+ renderWithProviders(<EntryPointsTabs facets={twoFacets} />);
60
+
61
+ const links = screen.getAllByTestId("Link");
62
+ expect(links).toHaveLength(2);
63
+ expect(links[0]).toHaveTextContent("Ebooks");
64
+ expect(links[1]).toHaveTextContent("Audiobooks");
65
+ // Each tab links somewhere for the router to navigate to.
66
+ expect(links[0]).toHaveAttribute("data-to");
67
+ expect(links[1]).toHaveAttribute("data-to");
68
+ });
69
+
70
+ it("should generate one tab with an active entry point", () => {
71
+ const oneFacet: FacetData[] = [
72
+ {
73
+ label: "Audiobooks",
74
+ href: "http://circulation.librarysimplified.org/groups/?entrypoint=Audio",
75
+ active: true,
76
+ },
77
+ ];
78
+ const { container } = renderWithProviders(
79
+ <EntryPointsTabs facets={oneFacet} />
80
+ );
81
+
82
+ const items = container.querySelectorAll("ul.nav-tabs li");
83
+ expect(items).toHaveLength(1);
84
+ expect(items[0]).toHaveClass("active");
85
+
86
+ expect(screen.getByText("Audiobooks")).toBeInTheDocument();
87
+ });
88
+ });
@@ -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,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
+ });
@@ -612,7 +612,9 @@ describe("PatronBlockingRulesEditor", () => {
612
612
  });
613
613
 
614
614
  it("disables all editing inputs and buttons when disabled prop is true", () => {
615
- renderEditor(<PatronBlockingRulesEditor value={existingRules} disabled={true} />);
615
+ renderEditor(
616
+ <PatronBlockingRulesEditor value={existingRules} disabled={true} />
617
+ );
616
618
 
617
619
  // The Help button stays enabled even in disabled mode (read-only affordance).
618
620
  const editingButtons = screen
@@ -0,0 +1,54 @@
1
+ import * as React from "react";
2
+ import { render } from "@testing-library/react";
3
+
4
+ import PatronInfo from "../../../src/components/PatronInfo";
5
+ import { PatronData } from "../../../src/interfaces";
6
+
7
+ const patrons: PatronData[] = [
8
+ {
9
+ authorization_expires: "",
10
+ username: "User Name",
11
+ personal_name: "Personal Name",
12
+ email_address: "user@email.com",
13
+ authorization_identifier: "1234",
14
+ authorization_identifiers: [""],
15
+ block_reason: "",
16
+ external_type: "",
17
+ fines: "",
18
+ permanent_id: "",
19
+ },
20
+ {
21
+ authorization_expires: "",
22
+ personal_name: "Personal Name2",
23
+ authorization_identifier: "5678",
24
+ authorization_identifiers: [""],
25
+ block_reason: "",
26
+ external_type: "",
27
+ fines: "",
28
+ permanent_id: "",
29
+ },
30
+ ];
31
+
32
+ describe("PatronInfo", () => {
33
+ it("should display the patron's information", () => {
34
+ const { container } = render(<PatronInfo patron={patrons[0]} />);
35
+
36
+ const items = container.querySelectorAll("ul.patron-data-list > li");
37
+ expect(items).toHaveLength(4);
38
+ expect(items[0]).toHaveTextContent("Username:User Name");
39
+ expect(items[1]).toHaveTextContent("Personal Name:Personal Name");
40
+ expect(items[2]).toHaveTextContent("Email Address:user@email.com");
41
+ expect(items[3]).toHaveTextContent("Identifier:1234");
42
+ });
43
+
44
+ it("should update the patron's information if a new patron was searched and found", () => {
45
+ const { container, rerender } = render(<PatronInfo patron={patrons[0]} />);
46
+
47
+ rerender(<PatronInfo patron={patrons[1]} />);
48
+
49
+ const items = container.querySelectorAll("ul.patron-data-list > li");
50
+ expect(items).toHaveLength(2);
51
+ expect(items[0]).toHaveTextContent("Personal Name:Personal Name2");
52
+ expect(items[1]).toHaveTextContent("Identifier:5678");
53
+ });
54
+ });
@@ -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
+ });
@@ -382,14 +382,10 @@ describe("Dashboard Statistics", () => {
382
382
  roles,
383
383
  quicksightPagePath: fakeQuickSightHref,
384
384
  };
385
- const {
386
- container,
387
- getByRole,
388
- queryByRole,
389
- queryByText,
390
- } = renderWithProviders(<Stats library={sampleLibraryKey} />, {
391
- appConfigSettings,
392
- });
385
+ const { container, getByRole, queryByRole, queryByText } =
386
+ renderWithProviders(<Stats library={sampleLibraryKey} />, {
387
+ appConfigSettings,
388
+ });
393
389
 
394
390
  // We should always render a Usage reports group when a library is specified.
395
391
  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
+ // The tab content (TroubleshootingCategoryPage) renders the connected
6
+ // Diagnostics/Self-tests containers, which need Redux store + csrf context this
7
+ // test does not supply. That content is incidental here — the test is about the
8
+ // tab navigation — so mock it to a marker.
9
+ jest.mock("../../../src/components/TroubleshootingCategoryPage", () => ({
10
+ __esModule: true,
11
+ default: () => <div data-testid="category-page" />,
12
+ }));
13
+
14
+ import TroubleshootingTabContainer from "../../../src/components/TroubleshootingTabContainer";
15
+
16
+ describe("TroubleshootingTabContainer", () => {
17
+ let goToTab: jest.Mock;
18
+
19
+ beforeEach(() => {
20
+ goToTab = jest.fn();
21
+ });
22
+
23
+ it("renders tabs and defaults to showing the Diagnostics tab", () => {
24
+ const { container } = render(
25
+ <TroubleshootingTabContainer goToTab={goToTab} tab="diagnostics" />
26
+ );
27
+
28
+ const nav = container.querySelector("ul.nav-tabs");
29
+ expect(nav).not.toBeNull();
30
+
31
+ const tabs = nav.querySelectorAll("li");
32
+ expect(tabs).toHaveLength(2);
33
+
34
+ expect(tabs[0]).toHaveTextContent("Diagnostics");
35
+ expect(tabs[0]).toHaveClass("active");
36
+
37
+ expect(tabs[1]).toHaveTextContent("Self-tests");
38
+ expect(tabs[1]).not.toHaveClass("active");
39
+ });
40
+
41
+ it("calls goToTab when another tab is clicked", async () => {
42
+ const user = userEvent.setup();
43
+ render(<TroubleshootingTabContainer goToTab={goToTab} tab="diagnostics" />);
44
+
45
+ await user.click(screen.getByRole("link", { name: "Self-tests" }));
46
+
47
+ expect(goToTab).toHaveBeenCalledTimes(1);
48
+ expect(goToTab).toHaveBeenCalledWith("self-tests");
49
+ });
50
+
51
+ it("switches the active tab when the tab prop changes", () => {
52
+ const { container, rerender } = render(
53
+ <TroubleshootingTabContainer goToTab={goToTab} tab="diagnostics" />
54
+ );
55
+
56
+ let tabs = container.querySelectorAll("ul.nav-tabs li");
57
+ expect(tabs[0]).toHaveClass("active");
58
+ expect(tabs[1]).not.toHaveClass("active");
59
+
60
+ rerender(
61
+ <TroubleshootingTabContainer goToTab={goToTab} tab="self-tests" />
62
+ );
63
+
64
+ tabs = container.querySelectorAll("ul.nav-tabs li");
65
+ expect(tabs[0]).not.toHaveClass("active");
66
+ expect(tabs[1]).toHaveClass("active");
67
+ });
68
+ });
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ // Header pulls in the react-router v3 context a plain render does not supply, and
5
+ // both Header and Footer are page chrome with no behavior worth exercising here.
6
+ // Mock them to markers so the test stays about what WelcomePage composes.
7
+ jest.mock("../../../src/components/Header", () => ({
8
+ __esModule: true,
9
+ default: () => <div data-testid="header" />,
10
+ }));
11
+ jest.mock("../../../src/components/Footer", () => ({
12
+ __esModule: true,
13
+ default: () => <div data-testid="footer" />,
14
+ }));
15
+
16
+ import WelcomePage from "../../../src/components/WelcomePage";
17
+
18
+ describe("WelcomePage", () => {
19
+ it("shows the header", () => {
20
+ render(<WelcomePage />);
21
+ expect(screen.getByTestId("header")).toBeInTheDocument();
22
+ });
23
+
24
+ it("shows the welcome message", () => {
25
+ render(<WelcomePage />);
26
+ expect(
27
+ screen.getByRole("heading", {
28
+ name: "Welcome to the Circulation Admin Interface!",
29
+ })
30
+ ).toBeInTheDocument();
31
+ });
32
+
33
+ it("shows a link to the library create form", () => {
34
+ render(<WelcomePage />);
35
+ const link = screen.getByRole("link", { name: /create a new library/i });
36
+ expect(link).toHaveAttribute("href", "/admin/web/config/libraries/create");
37
+ });
38
+
39
+ it("shows the footer", () => {
40
+ render(<WelcomePage />);
41
+ expect(screen.getByTestId("footer")).toBeInTheDocument();
42
+ });
43
+ });