@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
@@ -0,0 +1,147 @@
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 buildStore from "../../../src/store";
6
+
7
+ // The active tab renders DiagnosticsServiceType, which recurses into the service
8
+ // tab machinery. The legacy test only asserted the `type`/`services` props it
9
+ // received, so mock it to a marker that echoes those.
10
+ jest.mock("../../../src/components/DiagnosticsServiceType", () => ({
11
+ __esModule: true,
12
+ default: (props: any) => (
13
+ <div
14
+ data-testid="service-type"
15
+ data-type={props.type}
16
+ data-has-services={String(props.services !== undefined)}
17
+ />
18
+ ),
19
+ }));
20
+
21
+ import { DiagnosticsTabContainer } from "../../../src/components/DiagnosticsTabContainer";
22
+
23
+ describe("DiagnosticsTabContainer", () => {
24
+ let goToTab: jest.Mock;
25
+ let fetchDiagnostics: jest.Mock;
26
+
27
+ const ts1 = {
28
+ service: "test_service",
29
+ id: "1",
30
+ start: "start_time_string",
31
+ duration: "0",
32
+ collection_name: "collection1",
33
+ };
34
+
35
+ const diagnostics = {
36
+ monitor: [{ test_service_1: [{ collection1: [ts1] }] }],
37
+ };
38
+
39
+ beforeEach(() => {
40
+ goToTab = jest.fn();
41
+ fetchDiagnostics = jest.fn();
42
+ });
43
+
44
+ const element = (overrides = {}) => {
45
+ const props = {
46
+ goToTab,
47
+ tab: "coverage_provider",
48
+ store: buildStore(),
49
+ fetchDiagnostics,
50
+ diagnostics: { coverage_provider: [] },
51
+ isLoaded: true,
52
+ ...overrides,
53
+ };
54
+ return <DiagnosticsTabContainer {...props} />;
55
+ };
56
+
57
+ const renderContainer = (overrides = {}) => render(element(overrides));
58
+
59
+ describe("rendering", () => {
60
+ it("renders a tab container", () => {
61
+ const { container } = renderContainer();
62
+ expect(container.firstChild).toHaveClass("tab-container");
63
+ });
64
+
65
+ it("renders tabs and defaults to showing the Coverage Providers tab", () => {
66
+ const { container } = renderContainer();
67
+ const tabs = container.querySelectorAll("ul.nav-tabs li");
68
+ expect(tabs).toHaveLength(4);
69
+
70
+ expect(tabs[0]).toHaveTextContent("Coverage Providers");
71
+ expect(tabs[0]).toHaveClass("active");
72
+
73
+ expect(tabs[1]).toHaveTextContent("Monitors");
74
+ expect(tabs[1]).not.toHaveClass("active");
75
+
76
+ expect(tabs[2]).toHaveTextContent("Scripts");
77
+ expect(tabs[2]).not.toHaveClass("active");
78
+
79
+ expect(tabs[3]).toHaveTextContent("Other");
80
+ expect(tabs[3]).not.toHaveClass("active");
81
+ });
82
+
83
+ it("renders tab content", () => {
84
+ renderContainer({ diagnostics });
85
+ const serviceTypes = screen.getAllByTestId("service-type");
86
+ expect(serviceTypes).toHaveLength(4);
87
+
88
+ expect(serviceTypes[0]).toHaveAttribute("data-type", "coverage_provider");
89
+ expect(serviceTypes[0]).toHaveAttribute("data-has-services", "false");
90
+
91
+ expect(serviceTypes[1]).toHaveAttribute("data-type", "monitor");
92
+ expect(serviceTypes[1]).toHaveAttribute("data-has-services", "true");
93
+
94
+ expect(serviceTypes[2]).toHaveAttribute("data-type", "script");
95
+ expect(serviceTypes[2]).toHaveAttribute("data-has-services", "false");
96
+
97
+ expect(serviceTypes[3]).toHaveAttribute("data-type", "other");
98
+ expect(serviceTypes[3]).toHaveAttribute("data-has-services", "false");
99
+ });
100
+ });
101
+
102
+ describe("behavior", () => {
103
+ it("calls fetchDiagnostics on mount", () => {
104
+ renderContainer();
105
+ expect(fetchDiagnostics).toHaveBeenCalled();
106
+ });
107
+
108
+ it("shows the loading indicator", () => {
109
+ const { container, rerender } = renderContainer();
110
+ expect(container.querySelectorAll(".loading")).toHaveLength(0);
111
+ rerender(element({ isLoaded: false }));
112
+ expect(container.querySelectorAll(".loading")).toHaveLength(4);
113
+ });
114
+
115
+ it("shows an error message", () => {
116
+ const { container, rerender } = renderContainer();
117
+ expect(container.querySelectorAll(".alert-danger")).toHaveLength(0);
118
+ rerender(
119
+ element({
120
+ fetchError: { status: 401, response: "error fetching diagnostics" },
121
+ })
122
+ );
123
+ expect(container.querySelectorAll(".alert-danger")).toHaveLength(4);
124
+ });
125
+
126
+ it("calls goToTab when a tab is clicked", async () => {
127
+ const user = userEvent.setup();
128
+ renderContainer({ diagnostics });
129
+ await user.click(screen.getByRole("link", { name: "Monitors" }));
130
+ expect(goToTab).toHaveBeenCalledTimes(1);
131
+ expect(goToTab).toHaveBeenCalledWith("monitor");
132
+ });
133
+
134
+ it("switches tabs when the tab prop changes", () => {
135
+ const { container, rerender } = renderContainer();
136
+ let tabs = container.querySelectorAll("ul.nav-tabs li");
137
+ expect(tabs[0]).toHaveClass("active");
138
+ expect(tabs[1]).not.toHaveClass("active");
139
+
140
+ rerender(element({ tab: "monitor" }));
141
+
142
+ tabs = container.querySelectorAll("ul.nav-tabs li");
143
+ expect(tabs[0]).not.toHaveClass("active");
144
+ expect(tabs[1]).toHaveClass("active");
145
+ });
146
+ });
147
+ });
@@ -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
+ });
@@ -0,0 +1,97 @@
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 ErrorMessage from "../../../src/components/ErrorMessage";
6
+
7
+ describe("ErrorMessage", () => {
8
+ it("shows logged out message for 401 error", () => {
9
+ render(<ErrorMessage error={{ status: 401, response: "", url: "" }} />);
10
+
11
+ expect(screen.getByRole("alert")).toHaveTextContent("logged out");
12
+ });
13
+
14
+ it("shows detail for problem detail", () => {
15
+ render(
16
+ <ErrorMessage
17
+ error={{
18
+ status: 500,
19
+ response: JSON.stringify({ status: 500, detail: "detail" }),
20
+ url: "",
21
+ }}
22
+ />
23
+ );
24
+
25
+ expect(screen.getByRole("alert")).toHaveTextContent("detail");
26
+ });
27
+
28
+ it("shows response for non-json response", () => {
29
+ render(
30
+ <ErrorMessage error={{ status: 500, response: "response", url: "" }} />
31
+ );
32
+
33
+ expect(screen.getByRole("alert")).toHaveTextContent("response");
34
+ });
35
+
36
+ it("parses non-JSON problem detail string", () => {
37
+ // prettier-ignore
38
+ const response = "Remote service returned a problem detail document: {\"status\": 502, \"detail\": problem text, \"title\": TITLE}";
39
+ render(<ErrorMessage error={{ status: 500, response, url: "" }} />);
40
+
41
+ expect(screen.getByRole("alert")).toHaveTextContent(
42
+ "Remote service returned a problem detail document with status 502: problem text"
43
+ );
44
+ expect(screen.getByText("TITLE")).toBeInTheDocument();
45
+ });
46
+
47
+ it("can handle missing detail property in non-JSON problem detail string", () => {
48
+ // prettier-ignore
49
+ const response = "Remote service returned a problem detail document: {\"status\": 502, \"title\": TITLE}";
50
+ render(<ErrorMessage error={{ status: 500, response, url: "" }} />);
51
+
52
+ expect(screen.getByRole("alert")).toHaveTextContent(
53
+ "Remote service returned a problem detail document with status 502:"
54
+ );
55
+ expect(screen.getByText("TITLE")).toBeInTheDocument();
56
+ });
57
+
58
+ it("can handle missing status property in non-JSON problem detail string", () => {
59
+ // prettier-ignore
60
+ const response = "Remote service returned a problem detail document: {\"detail\": problem text, \"title\": TITLE}";
61
+ render(<ErrorMessage error={{ status: 500, response, url: "" }} />);
62
+
63
+ expect(screen.getByRole("alert")).toHaveTextContent(
64
+ "Remote service returned a problem detail document: problem text"
65
+ );
66
+ expect(screen.getByText("TITLE")).toBeInTheDocument();
67
+ });
68
+
69
+ it("can handle missing title property in non-JSON problem detail string", () => {
70
+ // prettier-ignore
71
+ const response = "Remote service returned a problem detail document: {\"status\": 502, \"detail\": problem text}";
72
+ render(<ErrorMessage error={{ status: 500, response, url: "" }} />);
73
+
74
+ expect(screen.getByRole("alert")).toHaveTextContent(
75
+ "Remote service returned a problem detail document with status 502:"
76
+ );
77
+ expect(screen.getByText("Error")).toBeInTheDocument();
78
+ });
79
+
80
+ it("shows a try again button that fires the callback when clicked", async () => {
81
+ const user = userEvent.setup();
82
+ const tryAgain = jest.fn();
83
+ render(
84
+ <ErrorMessage
85
+ error={{ status: 500, response: "response", url: "" }}
86
+ tryAgain={tryAgain}
87
+ />
88
+ );
89
+
90
+ const tryAgainButton = screen.getByRole("button", { name: "Try again" });
91
+ expect(tryAgainButton).toBeInTheDocument();
92
+
93
+ await user.click(tryAgainButton);
94
+
95
+ expect(tryAgain).toHaveBeenCalledTimes(1);
96
+ });
97
+ });
@@ -0,0 +1,141 @@
1
+ import * as React from "react";
2
+ import { renderWithProviders } from "../testUtils/withProviders";
3
+ import Footer, {
4
+ makeFooterSupportContactText,
5
+ } from "../../../src/components/Footer";
6
+ import {
7
+ DEFAULT_SUPPORT_CONTACT_TEXT,
8
+ SupportContactLink,
9
+ } from "../../../src/context/appContext";
10
+
11
+ const expectedTosText = "Terms of Service Text";
12
+ const expectedTosHref = "terms_of_service";
13
+ const testSupportContactEmail = "helpdesk@example.com";
14
+ const expectedSupportContact: SupportContactLink = {
15
+ href: `mailto:${testSupportContactEmail}?subject=Support+request`,
16
+ text: `Email ${testSupportContactEmail}.`,
17
+ };
18
+ const expectedSupportContactText = makeFooterSupportContactText(
19
+ expectedSupportContact.text
20
+ );
21
+
22
+ // Builds the app config the ContextProvider transforms into terms-of-service and
23
+ // support-contact context, mirroring the legacy test's `getFooterProviders`.
24
+ const footerConfig = ({ hasSupportContactUrl = false } = {}) => ({
25
+ tos_link_text: expectedTosText,
26
+ tos_link_href: expectedTosHref,
27
+ // Include support contact URL only if we ask for it.
28
+ support_contact_url: hasSupportContactUrl
29
+ ? "https://example.com/link-from-deprecated-setting"
30
+ : undefined,
31
+ supportContactUrl: hasSupportContactUrl
32
+ ? expectedSupportContact.href
33
+ : undefined,
34
+ });
35
+
36
+ describe("Footer", () => {
37
+ it("accepts an optional className prop", () => {
38
+ const { container } = renderWithProviders(
39
+ <Footer className="customClass" />,
40
+ { appConfigSettings: footerConfig() }
41
+ );
42
+ expect(container.querySelector("footer")).toHaveClass("customClass");
43
+ });
44
+
45
+ it("displays a terms of service link", () => {
46
+ const { container } = renderWithProviders(<Footer />, {
47
+ appConfigSettings: footerConfig(),
48
+ });
49
+ const links = container.querySelectorAll("a");
50
+ expect(links).toHaveLength(1);
51
+
52
+ expect(links[0]).toHaveTextContent(expectedTosText);
53
+ expect(links[0]).toHaveAttribute("href", expectedTosHref);
54
+ });
55
+
56
+ describe("support contact link", () => {
57
+ it("displays a support contact link, when URL present", () => {
58
+ const { container } = renderWithProviders(<Footer />, {
59
+ appConfigSettings: footerConfig({ hasSupportContactUrl: true }),
60
+ });
61
+ const links = container.querySelectorAll("a");
62
+ expect(links).toHaveLength(2);
63
+
64
+ // The second link should be the support contact link.
65
+ expect(links[1]).toHaveAttribute("href", expectedSupportContact.href);
66
+ expect(links[1]).toHaveTextContent(expectedSupportContactText);
67
+ });
68
+
69
+ it("omits contact link, when no support URL provided", () => {
70
+ const { container } = renderWithProviders(<Footer />, {
71
+ appConfigSettings: footerConfig({ hasSupportContactUrl: false }),
72
+ });
73
+ // There should only be one link....
74
+ const links = container.querySelectorAll("a");
75
+ expect(links).toHaveLength(1);
76
+
77
+ // ... and it should be the TOS link, not a support contact link.
78
+ expect(links[0]).not.toHaveAttribute("href", expectedSupportContact.href);
79
+ expect(links[0]).toHaveAttribute("href", expectedTosHref);
80
+ });
81
+
82
+ it("displays a default label for an https url", () => {
83
+ const { container } = renderWithProviders(<Footer />, {
84
+ appConfigSettings: { supportContactUrl: "https://example.com/support" },
85
+ });
86
+ const links = container.querySelectorAll("a");
87
+ expect(links).toHaveLength(2);
88
+ expect(links[1]).toHaveAttribute("href", "https://example.com/support");
89
+ expect(links[1]).toHaveTextContent(
90
+ makeFooterSupportContactText(DEFAULT_SUPPORT_CONTACT_TEXT)
91
+ );
92
+ });
93
+
94
+ it("displays a custom label for an https url", () => {
95
+ const { container } = renderWithProviders(<Footer />, {
96
+ appConfigSettings: {
97
+ supportContactUrl: "https://example.com/support",
98
+ supportContactText: "Custom Support Text",
99
+ },
100
+ });
101
+ const links = container.querySelectorAll("a");
102
+ expect(links).toHaveLength(2);
103
+ expect(links[1]).toHaveAttribute("href", "https://example.com/support");
104
+ expect(links[1]).toHaveTextContent(
105
+ makeFooterSupportContactText("Custom Support Text")
106
+ );
107
+ });
108
+
109
+ it("uses the deprecated support_contact_url if supportContactUrl is missing", () => {
110
+ const { container } = renderWithProviders(<Footer />, {
111
+ appConfigSettings: {
112
+ support_contact_url: "https://example.com/deprecated-support",
113
+ },
114
+ });
115
+ const links = container.querySelectorAll("a");
116
+ expect(links).toHaveLength(2);
117
+ expect(links[1]).toHaveAttribute(
118
+ "href",
119
+ "https://example.com/deprecated-support"
120
+ );
121
+ expect(links[1]).toHaveTextContent(
122
+ makeFooterSupportContactText(DEFAULT_SUPPORT_CONTACT_TEXT)
123
+ );
124
+ });
125
+
126
+ it("displays a custom label for a mailto url", () => {
127
+ const { container } = renderWithProviders(<Footer />, {
128
+ appConfigSettings: {
129
+ supportContactUrl: "mailto:custom@example.com",
130
+ supportContactText: "Email Us Now",
131
+ },
132
+ });
133
+ const links = container.querySelectorAll("a");
134
+ expect(links).toHaveLength(2);
135
+ expect(links[1]).toHaveAttribute("href", "mailto:custom@example.com");
136
+ expect(links[1]).toHaveTextContent(
137
+ makeFooterSupportContactText("Email Us Now")
138
+ );
139
+ });
140
+ });
141
+ });