Skip to content

Commit

Permalink
feat(web): move core/PageNext to core/Page
Browse files Browse the repository at this point in the history
And also adds basic unit testing for such a new Page component.
  • Loading branch information
dgdavid committed Sep 12, 2024
1 parent 1ccb25f commit 682f411
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 7 deletions.
212 changes: 212 additions & 0 deletions web/src/components/core/Page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React from "react";
import { screen, within } from "@testing-library/react";
import { plainRender, mockNavigateFn } from "~/test-utils";
import { Page } from "~/components/core";
import { _ } from "~/i18n";

let consoleErrorSpy: jest.SpyInstance;

describe("Page", () => {
beforeAll(() => {
consoleErrorSpy = jest.spyOn(console, "error");
consoleErrorSpy.mockImplementation();
});

afterAll(() => {
consoleErrorSpy.mockRestore();
});

it("renders given children", () => {
plainRender(
<Page>
<h1>{_("The Page Component")}</h1>
</Page>,
);
screen.getByRole("heading", { name: "The Page Component" });
});

describe("Page.Actions", () => {
it("renders a footer sticky to bottom", () => {
plainRender(
<Page.Actions>
<Page.Action>Save</Page.Action>
<Page.Action>Discard</Page.Action>
</Page.Actions>,
);

const footer = screen.getByRole("contentinfo");
expect(footer.classList.contains("pf-m-sticky-bottom")).toBe(true);
});
});

describe("Page.Action", () => {
it("renders a button with given content", () => {
plainRender(<Page.Action>Save</Page.Action>);
screen.getByRole("button", { name: "Save" });
});

it("renders an 'lg' button when size prop is not given", () => {
plainRender(<Page.Action>Cancel</Page.Action>);
const button = screen.getByRole("button", { name: "Cancel" });
expect(button.classList.contains("pf-m-display-lg")).toBe(true);
});

describe("when user clicks on it", () => {
it("triggers given onClick handler, if valid", async () => {
const onClick = jest.fn();
const { user } = plainRender(<Page.Action onClick={onClick}>Cancel</Page.Action>);
const button = screen.getByRole("button", { name: "Cancel" });
await user.click(button);
expect(onClick).toHaveBeenCalled();
});

it("navigates to the path given through 'navigateTo' prop", async () => {
const { user } = plainRender(<Page.Action navigateTo="/somewhere">Cancel</Page.Action>);
const button = screen.getByRole("button", { name: "Cancel" });
await user.click(button);
expect(mockNavigateFn).toHaveBeenCalledWith("/somewhere");
});
});
});

describe("Page.Content", () => {
it("renders a node that fills all the available space", async () => {
plainRender(<Page.Content>{_("The Content")}</Page.Content>);
const content = screen.getByText("The Content");
expect(content.classList.contains("pf-m-fill")).toBe(true);
});
});

describe("Page.Cancel", () => {
it("renders a 'Cancel' button that navigates to the top level route by default", async () => {
const { user } = plainRender(<Page.Cancel />);
const button = screen.getByRole("button", { name: "Cancel" });
await user.click(button);
expect(mockNavigateFn).toHaveBeenCalledWith("..");
});
});

describe("Page.Back", () => {
it("renders a button for navigating back when user clicks on it", async () => {
const { user } = plainRender(<Page.Back />);
const button = screen.getByRole("button", { name: "Back" });
await user.click(button);
expect(mockNavigateFn).toHaveBeenCalledWith(-1);
});
});

describe("Page.Submit", () => {
it("triggers both, form submission of its associated form and onClick handler if given", async () => {
const onClick = jest.fn();
// NOTE: using preventDefault here to avoid a jsdom error
// Error: Not implemented: HTMLFormElement.prototype.requestSubmit
const onSubmit = jest.fn((e) => {
e.preventDefault();
});

const { user } = plainRender(
<>
<form onSubmit={onSubmit} id="fake-form" />
<Page.Submit form="fake-form" onClick={onClick}>
Send
</Page.Submit>
</>,
);
const button = screen.getByRole("button", { name: "Send" });
await user.click(button);
expect(onSubmit).toHaveBeenCalled();
expect(onClick).toHaveBeenCalled();
});
});
describe("Page.Header", () => {
it("renders a node that sticks to top", async () => {
plainRender(<Page.Header>{_("The Header")}</Page.Header>);
const content = screen.getByText("The Header");
const container = content.parentNode as HTMLElement;
expect(container.classList.contains("pf-m-sticky-top")).toBe(true);
});
});

describe("Page.Section", () => {
it("outputs to console.error if both are missing, title and aria-label", () => {
plainRender(<Page.Section>{_("Content")}</Page.Section>);
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("must have either"));
});

it("renders a section node", async () => {
plainRender(<Page.Section aria-label="A Page Section">{_("The Content")}</Page.Section>);
const section = screen.getByRole("region");
within(section).getByText("The Content");
});

it("adds the aria-labelledby attribute when title is given but aria-label is not", async () => {
const { rerender } = plainRender(
<Page.Section title="A Page Section">{_("The Content")}</Page.Section>,
);
const section = screen.getByRole("region");
expect(section).toHaveAttribute("aria-labelledby");

// aria-label is given through Page.Section props
rerender(
<Page.Section title="A Page Section" aria-label="An aria label">
{_("The Content")}
</Page.Section>,
);
expect(section).not.toHaveAttribute("aria-labelledby");

// aria-label is given through pfCardProps
rerender(
<Page.Section title="A Page Section" pfCardProps={{ "aria-label": "An aria label" }}>
{_("The Content")}
</Page.Section>,
);
expect(section).not.toHaveAttribute("aria-labelledby");

// None was given, title nor aria-label
rerender(<Page.Section>{_("The Content")}</Page.Section>);
expect(section).not.toHaveAttribute("aria-labelledby");
});

it("renders given content props (title, value, description, actions, and children (content)", async () => {
plainRender(
<Page.Section
title={_("A section")}
value={"Enabled"}
description={_("Testing section with title, value, description, content, and actions")}
actions={<Page.Action>{_("Disable")}</Page.Action>}
>
{_("The Content")}
</Page.Section>,
);
const section = screen.getByRole("region");
within(section).getByText("A section");
within(section).getByText("Enabled");
within(section).getByText(
"Testing section with title, value, description, content, and actions",
);
within(section).getByText("The Content");
within(section).getByRole("button", { name: "Disable" });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { isEmpty, isObject } from "~/utils";

type SectionProps = {
title?: string;
"aria-label"?: string;
value?: React.ReactNode;
description?: string;
actions?: React.ReactNode;
Expand All @@ -70,6 +71,7 @@ const defaultCardProps: CardProps = {
const STICK_TO_TOP = Object.freeze({ default: "top" });
const STICK_TO_BOTTOM = Object.freeze({ default: "bottom" });

// TODO: check if it should have the banner role
const Header = ({ hasGutter = true, children, ...props }) => {
return (
<PageSection variant="light" component="div" stickyOnBreakpoint={STICK_TO_TOP} {...props}>
Expand All @@ -88,6 +90,7 @@ const Header = ({ hasGutter = true, children, ...props }) => {
*/
const Section = ({
title,
"aria-label": ariaLabel,
value,
description,
actions,
Expand All @@ -102,9 +105,15 @@ const Section = ({
const hasValue = !isEmpty(value);
const hasDescription = !isEmpty(description);
const hasHeader = hasTitle || hasValue;
const hasAriaLabel = isObject(pfCardProps) && "aria-label" in pfCardProps;
const props = { ...defaultCardProps };
if (!hasAriaLabel && hasTitle) props["aria-labelledby"] = titleId;
const hasAriaLabel =
!isEmpty(ariaLabel) || (isObject(pfCardProps) && "aria-label" in pfCardProps);
const props = { ...defaultCardProps, "aria-label": ariaLabel };

if (!hasTitle && !hasAriaLabel) {
console.error("Page.Section must have either, a title or aria-label");
}

if (hasTitle && !hasAriaLabel) props["aria-labelledby"] = titleId;

return (
<Card {...props} {...pfCardProps}>
Expand Down Expand Up @@ -140,11 +149,18 @@ const Section = ({
* <Page>
* <UserSectionContent />
* </Page>
*
* TODO: check if it contentinfo role really should have the banner role
*/
const Actions = ({ children }: React.PropsWithChildren) => {
return (
<PageGroup hasShadowTop stickyOnBreakpoint={STICK_TO_BOTTOM} className={flexStyles.flexGrow_0}>
<PageSection variant="light">
<PageGroup
role="contentinfo"
hasShadowTop
stickyOnBreakpoint={STICK_TO_BOTTOM}
className={flexStyles.flexGrow_0}
>
<PageSection variant="light" component="div">
<Flex justifyContent="justifyContentFlexEnd">{children}</Flex>
</PageSection>
</PageGroup>
Expand Down
3 changes: 1 addition & 2 deletions web/src/components/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ export { default as ListSearch } from "./ListSearch";
export { default as LoginPage } from "./LoginPage";
export { default as LogsButton } from "./LogsButton";
export { default as RowActions } from "./RowActions";
// FIXME: rename PageNext to Page when all componentes are migrated
export { default as Page } from "./PageNext";
export { default as Page } from "./Page";
export { default as PasswordAndConfirmationInput } from "./PasswordAndConfirmationInput";
export { default as Popup } from "./Popup";
export { default as ProgressReport } from "./ProgressReport";
Expand Down

0 comments on commit 682f411

Please sign in to comment.